@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.
- package/herd.mjs +179 -68
- 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.
|
|
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/
|
|
10608
|
-
|
|
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
|
-
|
|
10618
|
-
return
|
|
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
|
|
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 (
|
|
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, {
|
|
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
|
-
|
|
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")
|
|
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.
|
|
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": "
|
|
25
|
+
"gitHead": "ac28f0e97b92fbe2178387595a511d35c3ae4953"
|
|
26
26
|
}
|