@herdingchats/friday 0.1.567 → 0.1.569

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/herd.mjs +179 -68
  2. package/package.json +2 -2
package/herd.mjs CHANGED
@@ -18,7 +18,7 @@ var __export = (target, all) => {
18
18
  var define_HERD_PACKAGE_default;
19
19
  var init_define_HERD_PACKAGE = __esm({
20
20
  "<define:HERD_PACKAGE>"() {
21
- define_HERD_PACKAGE_default = { version: "0.1.567", gitHead: "816afa2c448b0a5330ddece39cdb0567ef65a3c5" };
21
+ define_HERD_PACKAGE_default = { version: "0.1.569", gitHead: "ac28f0e97b92fbe2178387595a511d35c3ae4953" };
22
22
  }
23
23
  });
24
24
 
@@ -1319,6 +1319,19 @@ var init_events = __esm({
1319
1319
  }),
1320
1320
  event("terminal.exited", { terminalId: nonEmpty6, exitCode: z12.number().int().nullable() }),
1321
1321
  event("terminal.notice_failed", { terminalId: nonEmpty6, reason: nonEmpty6 }),
1322
+ /**
1323
+ * The agent's start didn't check in with `herd inbox` within its window, so it looks like it
1324
+ * needs someone at its terminal. `agent` is its CLI's name, and `detail` what the screen showed,
1325
+ * when herd recognized it.
1326
+ */
1327
+ event("terminal.needs_attention", {
1328
+ terminalId: nonEmpty6,
1329
+ agent: nonEmpty6,
1330
+ machineName: nonEmpty6,
1331
+ detail: z12.string().max(200).optional()
1332
+ }),
1333
+ /** A start that needed attention checked in after all. */
1334
+ event("terminal.checked_in", { terminalId: nonEmpty6 }),
1322
1335
  /** The daemon closed one of Claude Code's own dialogs that it has recorded, named by its title. */
1323
1336
  event("terminal.dialog_closed", { terminalId: nonEmpty6, dialog: nonEmpty6 }),
1324
1337
  // A subscribed event that didn't wake the conversation, with the reason. One that woke it is a
@@ -1466,6 +1479,9 @@ var init_conversation_state = __esm({
1466
1479
  "author_not_linked",
1467
1480
  // The only agents that could take it showed their own sign-in, so a person has to sign in.
1468
1481
  "agent_signed_out",
1482
+ // Its agent started and never checked in with `herd inbox`, so it looks like it needs someone
1483
+ // at its terminal.
1484
+ "agent_needs_attention",
1469
1485
  // The only machines that could take it have a folder that isn't there, such as a drive not
1470
1486
  // yet mounted.
1471
1487
  "folder_unavailable",
@@ -1568,6 +1584,7 @@ var init_conversation_state = __esm({
1568
1584
  author_not_linked: "the author's account isn't linked",
1569
1585
  delegate_waiting: "a delegate is waiting on someone",
1570
1586
  agent_signed_out: "its agent isn't signed in on its machine",
1587
+ agent_needs_attention: "its agent looks like it needs someone at its terminal",
1571
1588
  folder_unavailable: "its machine's folder isn't there",
1572
1589
  no_suitable_worker: "no worker may take it under its configuration",
1573
1590
  notice_failed: "a notice didn't reach the agent's terminal",
@@ -4473,6 +4490,8 @@ var localRequestSchema = z19.object({
4473
4490
  "repo",
4474
4491
  "activate"
4475
4492
  ]),
4493
+ // The key of the agent's start, from `HERD_START`, which `herd inbox` proves the start with.
4494
+ start: z19.string().min(1).max(100).optional(),
4476
4495
  // `herd repo`: the repo it added to the conversation's folder, for the daemon to record.
4477
4496
  repo: z19.object({
4478
4497
  remote: z19.string().min(1).max(500),
@@ -4821,6 +4840,50 @@ async function permissionHook(raw, options = {}) {
4821
4840
  }
4822
4841
  }
4823
4842
 
4843
+ // src/timing.ts
4844
+ init_define_HERD_PACKAGE();
4845
+ var CHECK_IN_MS = 15e3;
4846
+ var VARIABLES = {
4847
+ settleMs: "HERD_SETTLE_MS",
4848
+ pollMs: "HERD_POLL_MS",
4849
+ confirmMs: "HERD_CONFIRM_MS",
4850
+ pasteMs: "HERD_PASTE_MS",
4851
+ renewMs: "HERD_RENEW_MS",
4852
+ hookSettleMs: "HERD_HOOK_SETTLE_MS",
4853
+ agyIdleSettleMs: "HERD_AGY_IDLE_SETTLE_MS",
4854
+ promptPollMs: "HERD_PROMPT_POLL_MS",
4855
+ noticeRetryMs: "HERD_NOTICE_RETRY_MS",
4856
+ investigateAfterMs: "HERD_INVESTIGATE_AFTER_MS",
4857
+ idleMs: "HERD_IDLE_MS",
4858
+ watchMs: "HERD_WATCH_MS",
4859
+ usageReportMs: "HERD_USAGE_REPORT_MS",
4860
+ usageStaleMs: "HERD_USAGE_STALE_MS",
4861
+ activationMs: "HERD_ACTIVATION_MS",
4862
+ activationPollMs: "HERD_ACTIVATION_POLL_MS",
4863
+ signInRestartMs: "HERD_SIGN_IN_RESTART_MS",
4864
+ checkInMs: "HERD_CHECK_IN_MS",
4865
+ machineCheckMs: "HERD_MACHINE_CHECK_MS"
4866
+ };
4867
+ function timingFromEnv(env) {
4868
+ const timing = {};
4869
+ for (const [key, name] of Object.entries(VARIABLES)) {
4870
+ const raw = env[name];
4871
+ if (raw === void 0 || raw === "") continue;
4872
+ const value = Number(raw);
4873
+ if (!Number.isFinite(value) || value < 0) {
4874
+ throw new HerdError(`${name} must be a number of milliseconds, not "${raw}".`);
4875
+ }
4876
+ timing[key] = value;
4877
+ }
4878
+ return timing;
4879
+ }
4880
+ function driverTiming(timing) {
4881
+ const { settleMs, pollMs, confirmMs } = timing;
4882
+ return Object.fromEntries(
4883
+ Object.entries({ settleMs, pollMs, confirmMs }).filter(([, value]) => value !== void 0)
4884
+ );
4885
+ }
4886
+
4824
4887
  // src/activation.ts
4825
4888
  var ACTIVATION_DEADLINE_MS = 3 * 60 * 1e3;
4826
4889
  var ACTIVATION_POLL_MS = 500;
@@ -5027,6 +5090,7 @@ var Activator = class {
5027
5090
  const { agent } = current;
5028
5091
  const deadline = Date.now() + (this.options.deadlineMs ?? ACTIVATION_DEADLINE_MS);
5029
5092
  const restartMs = this.options.signInRestartMs ?? SIGN_IN_RESTART_MS;
5093
+ const checkInMs = this.options.checkInMs ?? CHECK_IN_MS;
5030
5094
  const pollMs = this.options.pollMs ?? ACTIVATION_POLL_MS;
5031
5095
  let seen = "didnt_start";
5032
5096
  let startedAt = 0;
@@ -5061,7 +5125,7 @@ var Activator = class {
5061
5125
  await start();
5062
5126
  while (!current.checkedIn && Date.now() < deadline && !this.stopped) {
5063
5127
  const reading = await driver.read(current.terminal).catch(() => null);
5064
- if (reading?.state === "signed_out") {
5128
+ if (reading?.state === "signed_out" && Date.now() - startedAt >= checkInMs) {
5065
5129
  seen = "signed_out";
5066
5130
  if (!signingIn) {
5067
5131
  signingIn = true;
@@ -6616,6 +6680,10 @@ init_define_HERD_PACKAGE();
6616
6680
  // src/commands/terminal.ts
6617
6681
  init_define_HERD_PACKAGE();
6618
6682
  var DAEMON_WAIT_MS = Number(process.env.HERD_DAEMON_WAIT_MS ?? 3e4);
6683
+ function withStart(request) {
6684
+ const start = process.env.HERD_START;
6685
+ return start ? { start, ...request } : request;
6686
+ }
6619
6687
  async function fromTerminal(request) {
6620
6688
  const socket = process.env.HERD_SOCKET;
6621
6689
  const secret = process.env.HERD_TERMINAL;
@@ -6625,7 +6693,7 @@ async function fromTerminal(request) {
6625
6693
  let reply = null;
6626
6694
  const deadline = Date.now() + DAEMON_WAIT_MS;
6627
6695
  while (!reply) {
6628
- reply = await askDaemon(socket, { secret, ...request }).catch(() => null);
6696
+ reply = await askDaemon(socket, { secret, ...withStart(request) }).catch(() => null);
6629
6697
  if (reply) break;
6630
6698
  if (Date.now() > deadline) throw new HerdError("Couldn't reach herd's daemon. Is it running?");
6631
6699
  await new Promise((resolve7) => setTimeout(resolve7, 500));
@@ -7448,6 +7516,9 @@ var ownPrompt = (draft) => OWN_PROMPTS.find(
7448
7516
  ) ?? (draft.startsWith(DELEGATE_START) || draft.length >= 40 && DELEGATE_START.startsWith(draft) ? draft : null);
7449
7517
  var SESSION_ID = /^[A-Za-z0-9-]{1,100}$/;
7450
7518
  var SUBMIT_RETRIES = 3;
7519
+ function withStartKey(command, startKey) {
7520
+ return startKey ? `HERD_START=${startKey} ${command}` : command;
7521
+ }
7451
7522
  var DEFAULT_TIMING = { settleMs: 800, pollMs: 150, startMs: 6e4, confirmMs: 3e3 };
7452
7523
  var sleep3 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
7453
7524
  var EXIT_WAIT_MS = 5e3;
@@ -7504,12 +7575,12 @@ var AgentDriver = class {
7504
7575
  * `claude --resume <id>`. Resuming without an ID types `/resume` and picks the newest
7505
7576
  * conversation, which is this worktree's only agent.
7506
7577
  */
7507
- async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2 } = {}) {
7578
+ async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2, startKey = null } = {}) {
7508
7579
  const known = sessionId !== null && SESSION_ID.test(sessionId);
7509
7580
  const byId = resume && known;
7510
7581
  const command = byId ? `claude --resume ${sessionId}` : !resume && known ? `claude --session-id ${sessionId}` : "claude";
7511
7582
  await this.settle(name);
7512
- await this.terminals.type(name, command);
7583
+ await this.terminals.type(name, withStartKey(command, startKey));
7513
7584
  await this.terminals.press(name, "enter");
7514
7585
  const first = await this.until(
7515
7586
  name,
@@ -7802,11 +7873,11 @@ var AgyDriver = class {
7802
7873
  * waits for its prompt box. It trusts the folder only in a worktree the daemon made. agy chooses
7803
7874
  * a session's ID itself, and its `SessionStart` hook reports it.
7804
7875
  */
7805
- async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2 } = {}) {
7876
+ async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2, startKey = null } = {}) {
7806
7877
  const byId = resume && sessionId !== null && SESSION_ID.test(sessionId);
7807
7878
  const command = byId ? `agy --conversation=${sessionId} ${AGY_FLAGS}` : `agy ${AGY_FLAGS}`;
7808
7879
  await this.settle(name);
7809
- await this.terminals.type(name, command);
7880
+ await this.terminals.type(name, withStartKey(command, startKey));
7810
7881
  await this.terminals.press(name, "enter");
7811
7882
  const first = await this.until(
7812
7883
  name,
@@ -8793,11 +8864,11 @@ var CodexDriver = class {
8793
8864
  * Types `codex` into the settled shell, or `codex resume <id>` to continue a session, and waits
8794
8865
  * for its composer. Codex chooses a session's ID itself, when it takes the first prompt.
8795
8866
  */
8796
- async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2 } = {}) {
8867
+ async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2, startKey = null } = {}) {
8797
8868
  const byId = resume && sessionId !== null && SESSION_ID.test(sessionId);
8798
8869
  const command = byId ? `codex resume ${sessionId} ${CODEX_FLAGS}` : `codex ${CODEX_FLAGS}`;
8799
8870
  await this.settle(name);
8800
- await this.terminals.type(name, command);
8871
+ await this.terminals.type(name, withStartKey(command, startKey));
8801
8872
  await this.terminals.press(name, "enter");
8802
8873
  const first = await this.until(
8803
8874
  name,
@@ -9853,48 +9924,6 @@ var HostTerminals = class {
9853
9924
  }
9854
9925
  };
9855
9926
 
9856
- // src/timing.ts
9857
- init_define_HERD_PACKAGE();
9858
- var VARIABLES = {
9859
- settleMs: "HERD_SETTLE_MS",
9860
- pollMs: "HERD_POLL_MS",
9861
- confirmMs: "HERD_CONFIRM_MS",
9862
- pasteMs: "HERD_PASTE_MS",
9863
- renewMs: "HERD_RENEW_MS",
9864
- hookSettleMs: "HERD_HOOK_SETTLE_MS",
9865
- agyIdleSettleMs: "HERD_AGY_IDLE_SETTLE_MS",
9866
- promptPollMs: "HERD_PROMPT_POLL_MS",
9867
- noticeRetryMs: "HERD_NOTICE_RETRY_MS",
9868
- investigateAfterMs: "HERD_INVESTIGATE_AFTER_MS",
9869
- idleMs: "HERD_IDLE_MS",
9870
- watchMs: "HERD_WATCH_MS",
9871
- usageReportMs: "HERD_USAGE_REPORT_MS",
9872
- usageStaleMs: "HERD_USAGE_STALE_MS",
9873
- activationMs: "HERD_ACTIVATION_MS",
9874
- activationPollMs: "HERD_ACTIVATION_POLL_MS",
9875
- signInRestartMs: "HERD_SIGN_IN_RESTART_MS",
9876
- machineCheckMs: "HERD_MACHINE_CHECK_MS"
9877
- };
9878
- function timingFromEnv(env) {
9879
- const timing = {};
9880
- for (const [key, name] of Object.entries(VARIABLES)) {
9881
- const raw = env[name];
9882
- if (raw === void 0 || raw === "") continue;
9883
- const value = Number(raw);
9884
- if (!Number.isFinite(value) || value < 0) {
9885
- throw new HerdError(`${name} must be a number of milliseconds, not "${raw}".`);
9886
- }
9887
- timing[key] = value;
9888
- }
9889
- return timing;
9890
- }
9891
- function driverTiming(timing) {
9892
- const { settleMs, pollMs, confirmMs } = timing;
9893
- return Object.fromEntries(
9894
- Object.entries({ settleMs, pollMs, confirmMs }).filter(([, value]) => value !== void 0)
9895
- );
9896
- }
9897
-
9898
9927
  // src/usage-reporter.ts
9899
9928
  init_define_HERD_PACKAGE();
9900
9929
  var USAGE_REPORT_MS = 5 * 60 * 1e3;
@@ -10159,6 +10188,17 @@ var workspaceFields = z25.object({
10159
10188
  codexOffset: z25.number().default(0),
10160
10189
  /** When the agent was last started, so Codex's new session file is told from older ones. */
10161
10190
  agentStartedAt: z25.number().default(0),
10191
+ /**
10192
+ * The key the agent was last started with, as `HERD_START`. Its `herd inbox` carrying this key
10193
+ * is what proves the start works: never a hook, a prompt being taken, or the screen.
10194
+ */
10195
+ startKey: z25.string().nullable().default(null),
10196
+ /** Whether the current start has checked in. One from before keys counts as proven. */
10197
+ startProven: z25.boolean().default(true),
10198
+ /** When the check-in window of an unproven start began, or null once it's proven or for none. */
10199
+ checkInFrom: z25.number().nullable().default(null),
10200
+ /** Whether this start has been flagged as needing someone at its terminal, so it's said once. */
10201
+ needsAttention: z25.boolean().default(false),
10162
10202
  /** What agy's status line last said its agent was doing, and when. */
10163
10203
  agentState: z25.enum(["working", "idle"]).nullable().default(null),
10164
10204
  agentStateAt: z25.number().default(0)
@@ -10235,7 +10275,7 @@ async function claimChange(ws, workspace, change) {
10235
10275
 
10236
10276
  // src/workspaces/delivery.ts
10237
10277
  init_define_HERD_PACKAGE();
10238
- import { randomUUID as randomUUID6 } from "node:crypto";
10278
+ import { randomBytes as randomBytes6, randomUUID as randomUUID6 } from "node:crypto";
10239
10279
  import { join as join30 } from "node:path";
10240
10280
 
10241
10281
  // src/workspaces/repos.ts
@@ -10535,6 +10575,10 @@ async function setUp(ws, conversationId) {
10535
10575
  codexFile: null,
10536
10576
  codexOffset: 0,
10537
10577
  agentStartedAt: 0,
10578
+ startKey: null,
10579
+ startProven: true,
10580
+ checkInFrom: null,
10581
+ needsAttention: false,
10538
10582
  agentState: null,
10539
10583
  agentStateAt: 0
10540
10584
  };
@@ -10578,6 +10622,12 @@ async function openLead(ws, workspace) {
10578
10622
  });
10579
10623
  }
10580
10624
 
10625
+ // src/workspaces/watch.ts
10626
+ init_define_HERD_PACKAGE();
10627
+
10628
+ // src/workspaces/check-in.ts
10629
+ init_define_HERD_PACKAGE();
10630
+
10581
10631
  // src/workspaces/signed-out.ts
10582
10632
  init_define_HERD_PACKAGE();
10583
10633
  init_src();
@@ -10598,14 +10648,50 @@ async function releaseSignedOut(ws, workspace) {
10598
10648
  terminalNotice: false,
10599
10649
  idleSince: null,
10600
10650
  noticeFailedAt: null,
10601
- noticeFailure: null
10651
+ noticeFailure: null,
10652
+ checkInFrom: null,
10653
+ needsAttention: false
10602
10654
  });
10603
10655
  if (workspace.status !== "exited") await ws.report(id2, "exited");
10604
10656
  await ws.post(`${ws.conversationPath(id2)}/release`, { reason: "agent_signed_out" });
10605
10657
  }
10606
10658
 
10607
- // src/workspaces/watch.ts
10608
- init_define_HERD_PACKAGE();
10659
+ // src/workspaces/check-in.ts
10660
+ async function checkIn(ws, workspace, grant, start) {
10661
+ if (workspace.startProven || !start || start !== workspace.startKey) return;
10662
+ if (grant.terminalId !== workspace.lead) return;
10663
+ const id2 = workspace.conversationId;
10664
+ ws.update(id2, { startProven: true, checkInFrom: null, needsAttention: false });
10665
+ if (!workspace.needsAttention) return;
10666
+ ws.log(`${id2}: the agent checked in after all.`);
10667
+ await ws.terminalEvent(id2, {
10668
+ type: "terminal.checked_in",
10669
+ payload: { terminalId: workspace.lead }
10670
+ });
10671
+ }
10672
+ function checkInOverdue(ws, workspace, now = Date.now()) {
10673
+ return !workspace.startProven && workspace.checkInFrom !== null && now - workspace.checkInFrom >= (ws.options.checkInMs ?? CHECK_IN_MS);
10674
+ }
10675
+ async function followCheckIn(ws, workspace, reading) {
10676
+ if (!checkInOverdue(ws, workspace)) return false;
10677
+ if (reading?.state === "signed_out") {
10678
+ await releaseSignedOut(ws, workspace);
10679
+ return true;
10680
+ }
10681
+ if (workspace.needsAttention) return false;
10682
+ const id2 = workspace.conversationId;
10683
+ ws.update(id2, { needsAttention: true });
10684
+ ws.log(`${id2}: ${workspace.agent} started but didn't check in, so it needs someone.`);
10685
+ await ws.terminalEvent(id2, {
10686
+ type: "terminal.needs_attention",
10687
+ payload: {
10688
+ terminalId: workspace.lead,
10689
+ agent: workspace.agent,
10690
+ machineName: ws.options.machineName()
10691
+ }
10692
+ });
10693
+ return false;
10694
+ }
10609
10695
 
10610
10696
  // src/workspaces/investigation.ts
10611
10697
  init_define_HERD_PACKAGE();
@@ -10613,11 +10699,19 @@ import { randomUUID as randomUUID5 } from "node:crypto";
10613
10699
  import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync21 } from "node:fs";
10614
10700
  import { join as join29 } from "node:path";
10615
10701
  function needsInvestigator(ws, workspace, reading) {
10702
+ const allowed = ws.options.canInvestigate ? ws.options.canInvestigate() : workspace.agent === "claude-code";
10703
+ if (!allowed || !reading || workspace.investigation) return false;
10704
+ if (reading.state !== "unknown" || reading.knownDialog || reading.program === "shell") {
10705
+ return false;
10706
+ }
10707
+ const trigger = investigationTrigger(ws, workspace);
10708
+ return trigger !== null && workspace.investigatedFailure !== trigger;
10709
+ }
10710
+ function investigationTrigger(ws, workspace) {
10711
+ if (workspace.needsAttention && checkInOverdue(ws, workspace)) return workspace.checkInFrom;
10616
10712
  const failedAt = workspace.noticeFailedAt;
10617
- if (workspace.agent === "codex" || workspace.agent === "agy") return false;
10618
- return Boolean(
10619
- reading && failedAt !== null && workspace.deferred && !workspace.investigation && workspace.investigatedFailure !== failedAt && Date.now() - failedAt >= (ws.options.investigateAfterMs ?? INVESTIGATE_AFTER_MS) && reading.state === "unknown" && !reading.knownDialog && reading.program !== "shell"
10620
- );
10713
+ const after = ws.options.investigateAfterMs ?? INVESTIGATE_AFTER_MS;
10714
+ return failedAt !== null && workspace.deferred && Date.now() - failedAt >= after ? failedAt : null;
10621
10715
  }
10622
10716
  async function openInvestigator(ws, workspace) {
10623
10717
  const id2 = workspace.conversationId;
@@ -10627,10 +10721,11 @@ async function openInvestigator(ws, workspace) {
10627
10721
  );
10628
10722
  const secret = ws.options.local().grant({ terminalId: terminal, conversationId: id2, investigates: workspace.lead });
10629
10723
  ws.update(id2, {
10630
- investigatedFailure: workspace.noticeFailedAt,
10724
+ investigatedFailure: investigationTrigger(ws, workspace),
10631
10725
  investigation: { terminal, secret, startedAt: Date.now(), screen, key: null, promptId: null }
10632
10726
  });
10633
10727
  ws.log(`${id2}: opened an investigator for a screen herd can't read.`);
10728
+ if (workspace.agent !== "claude-code") configureClaude(ws, workspace);
10634
10729
  const shell = ws.options.shell ?? process.env.SHELL ?? "/bin/sh";
10635
10730
  try {
10636
10731
  await ws.options.terminals.create({
@@ -10801,7 +10896,8 @@ async function watchOne(ws, conversationId, terminals, rows = []) {
10801
10896
  lead = (await ws.options.terminals.list()).find((t) => t.name === workspace?.lead);
10802
10897
  }
10803
10898
  let reading = lead && !lead.exited ? await ws.driverFor(workspace).read(workspace.lead) : null;
10804
- if (reading?.state === "signed_out") return releaseSignedOut(ws, workspace);
10899
+ if (await followCheckIn(ws, workspace, reading)) return;
10900
+ workspace = ws.get(conversationId) ?? workspace;
10805
10901
  if (reading && !reading.knownDialog && workspace.closedDialog) {
10806
10902
  workspace = ws.update(conversationId, { closedDialog: null });
10807
10903
  }
@@ -10983,11 +11079,18 @@ async function deliver(ws, conversationId, delivery) {
10983
11079
  });
10984
11080
  }
10985
11081
  const resume = workspace.started && (!ownId || workspace.sessionId !== null);
10986
- workspace = ws.update(conversationId, { agentStartedAt: Date.now() });
11082
+ workspace = ws.update(conversationId, {
11083
+ agentStartedAt: Date.now(),
11084
+ startKey: randomBytes6(12).toString("hex"),
11085
+ startProven: false,
11086
+ checkInFrom: null,
11087
+ needsAttention: false
11088
+ });
10987
11089
  await ws.driverFor(workspace).start(workspace.lead, workspace.folder, {
10988
11090
  resume,
10989
11091
  sessionId: workspace.sessionId,
10990
- ownFolder: isHerdFolder(workspace.folder, rootOf(ws))
11092
+ ownFolder: isHerdFolder(workspace.folder, rootOf(ws)),
11093
+ startKey: workspace.startKey
10991
11094
  });
10992
11095
  workspace = ws.update(conversationId, {
10993
11096
  started: true,
@@ -10996,16 +11099,15 @@ async function deliver(ws, conversationId, delivery) {
10996
11099
  askedToReply: false,
10997
11100
  handledThisTurn: false
10998
11101
  });
10999
- if ((await ws.driverFor(workspace).read(workspace.lead)).state === "signed_out") {
11000
- return releaseSignedOut(ws, workspace);
11001
- }
11002
11102
  if (!await claim(ws, workspace)) return;
11003
11103
  const result2 = await submitKeepingClaim(
11004
11104
  ws,
11005
11105
  conversationId,
11006
11106
  workspace.readSeq === 0 ? workspace.parent ? delegatePrompt(workspace.parent) : STARTING_PROMPT : NOTICE
11007
11107
  );
11008
- return afterSubmit(ws, ws.get(conversationId) ?? workspace, result2);
11108
+ workspace = ws.get(conversationId) ?? workspace;
11109
+ if (!workspace.startProven) workspace = ws.update(conversationId, { checkInFrom: Date.now() });
11110
+ return afterSubmit(ws, workspace, result2);
11009
11111
  }
11010
11112
  let reading = await ws.driverFor(workspace).read(workspace.lead);
11011
11113
  if (reading.knownDialog) {
@@ -11396,7 +11498,10 @@ async function answer(ws, grant, request, gone = new AbortController().signal) {
11396
11498
  if (request.op === "activate") {
11397
11499
  return { ok: false, error: "Only an agent's activation terminal can check in." };
11398
11500
  }
11399
- if (request.op === "inbox") return inbox(ws, workspace);
11501
+ if (request.op === "inbox") {
11502
+ await checkIn(ws, workspace, grant, request.start);
11503
+ return inbox(ws, ws.get(grant.conversationId) ?? workspace);
11504
+ }
11400
11505
  if (request.op === "send") return send(ws, workspace, request);
11401
11506
  if (request.op === "channels") return channels(ws, workspace);
11402
11507
  if (request.op === "watch") return watch(ws, workspace, request);
@@ -12687,6 +12792,11 @@ async function runDaemon(args) {
12687
12792
  activations: {
12688
12793
  fromTerminal: async (grant, request) => activator?.fromTerminal(grant, request) ?? { ok: false, error: "This activation is over." }
12689
12794
  },
12795
+ // The investigator is Claude Code, so any agent's stuck screen gets one where it's activated.
12796
+ canInvestigate: () => Boolean(
12797
+ daemon?.chosenClis().some((cli) => cli.name === "claude-code") && activator?.states()["claude-code"]?.state === "activated"
12798
+ ),
12799
+ ...timing.checkInMs === void 0 ? {} : { checkInMs: timing.checkInMs },
12690
12800
  // The agent stops being offered before its conversation goes back, then activates again.
12691
12801
  agentSignedOut: async (agent) => {
12692
12802
  const version = daemon?.chosenClis().find((cli) => cli.name === agent)?.version ?? "unknown";
@@ -12724,6 +12834,7 @@ async function runDaemon(args) {
12724
12834
  ...timing.activationMs === void 0 ? {} : { deadlineMs: timing.activationMs },
12725
12835
  ...timing.activationPollMs === void 0 ? {} : { pollMs: timing.activationPollMs },
12726
12836
  ...timing.signInRestartMs === void 0 ? {} : { signInRestartMs: timing.signInRestartMs },
12837
+ ...timing.checkInMs === void 0 ? {} : { checkInMs: timing.checkInMs },
12727
12838
  log
12728
12839
  });
12729
12840
  checks = new MachineChecks({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herdingchats/friday",
3
- "version": "0.1.567",
3
+ "version": "0.1.569",
4
4
  "description": "Friday, HerdingChats' agent daemon, and the herd command it runs agents with.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,5 +22,5 @@
22
22
  "satori": "0.32.0",
23
23
  "zod": "4.6.7"
24
24
  },
25
- "gitHead": "816afa2c448b0a5330ddece39cdb0567ef65a3c5"
25
+ "gitHead": "ac28f0e97b92fbe2178387595a511d35c3ae4953"
26
26
  }