chloejsnpm i chloejs

Docs

Asking a person

A job stops, sends its question to whoever should answer it, and carries on when they do.


This is the part worth building the rest for.

ask parks the run, writes down where it stopped and what it asked, and sends the question out to an address, which is channel:who. The question arrives wherever that channel reaches, so usually on a phone, and the next message back from that person is the answer.

Nothing reads that message with a model. An answer is understood against the shape the ask named, and "yes" against z.boolean() is a yes. Anything that does not fit is asked again in plain words rather than guessed at. The whole point of stopping to ask was to take the judgement out of the machine, so do not put a model back in there.

The process can restart while it waits. When the answer arrives the job runs again from the top, every finished step hands back what it returned, and ask returns the answer instead of parking.

Three rules:

  • An ask names who is being asked. who: is an address, and without one it is the run's owner.
  • A run says who it was for. One column, filled in from the first run.
  • A parked run expires. A question nobody answers is a job that never finishes and a job nothing releases, so within: is how long it waits, two hours by default, and otherwise: is what to carry on with. Without an otherwise the job stops and says nobody answered. The clock sweeps for these on every tick.

While a job is waiting it does not start again. A second run would ask the same question twice and act on whichever came back first.

One that does it

The rules around the question are code, and only the question itself is the person's. A run with nothing to ask about ends without asking.

export default defineJob({
  id: "keep-watching",
  cron: every.friday.at("17:00"),
  timezone: "America/New_York",
  description: "Asks whether a site that keeps failing is still worth watching.",
  run: async (work) => {
    const seen = await work.step("read what the check wrote", () => note(work.agentName, "sites", Sites).read());
    const dropped = note(work.agentName, "dropped", Dropped);
    const already = await work.step("read what was let go", () => dropped.read());

    const failing = seen.answers.filter((one) => down(one) && !already.sites[one.site]);
    if (failing.length === 0) return { asked: 0, dropped: [] };

    const letGo: string[] = [];
    for (const one of failing) {
      // Two hours is the default, and a week is the point here: the question can
      // sit until somebody is at a desk. Nobody answering means carry on
      // watching, which is the answer that changes nothing.
      const keep = await work.ask(`keep watching ${one.site}?`, {
        question: `${one.site} answered ${one.status}. Keep watching it?`,
        answer: z.boolean(),
        within: "7d",
        otherwise: true,
      });
      if (!keep) letGo.push(one.site);
    }

    if (letGo.length > 0) {
      const at = new Date().toISOString();
      await work.step("write down what was let go", () =>
        dropped.write({ sites: { ...already.sites, ...Object.fromEntries(letGo.map((site) => [site, at])) } }),
      );
    }

    return { asked: failing.length, dropped: letGo };
  },
  summary: (r) =>
    r.asked === 0 ? "nothing to ask about" : `asked about ${r.asked}, let go of ${r.dropped.length}`,
});

A yes or no question arrives with two buttons on Telegram. Pressing one answers the job and the buttons are taken away, so the same question cannot be answered twice.