harnery 0.8.0 → 0.10.0
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/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +8 -0
- package/dist/commands/agents.d.ts +24 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +244 -13
- package/dist/commands/backup.d.ts +5 -4
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +15 -14
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +27 -1
- package/dist/commands/claude-desktop.d.ts +19 -0
- package/dist/commands/claude-desktop.d.ts.map +1 -0
- package/dist/commands/claude-desktop.js +168 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +149 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +37 -0
- package/dist/commands/eml.d.ts +32 -0
- package/dist/commands/eml.d.ts.map +1 -1
- package/dist/commands/eml.js +16 -3
- package/dist/commands/grep.d.ts +35 -2
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +427 -139
- package/dist/commands/harness.d.ts +11 -0
- package/dist/commands/harness.d.ts.map +1 -0
- package/dist/commands/harness.js +118 -0
- package/dist/commands/init.d.ts +15 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +125 -14
- package/dist/commands/presence.d.ts +9 -4
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +88 -5
- package/dist/commands/relay.d.ts +9 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +143 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -0
- package/dist/commands/workflow.d.ts +4 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +82 -0
- package/dist/core/agents/canonical-emit.d.ts +15 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +41 -3
- package/dist/core/agents/cli.js +14 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +44 -3
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +26 -1
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +26 -1
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +4 -37
- package/dist/core/agents/rules/stop-hook.d.ts +8 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +8 -0
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +16 -43
- package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +24 -4
- package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +24 -7
- package/dist/core/agents/state/names.d.ts +38 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +46 -7
- package/dist/core/agents/state/pidmap.d.ts +5 -1
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +58 -2
- package/dist/core/agents/state/stale-sweep.d.ts +3 -2
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +6 -6
- package/dist/core/config.d.ts +77 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +210 -25
- package/dist/core/context/index.d.ts +144 -0
- package/dist/core/context/index.d.ts.map +1 -0
- package/dist/core/context/index.js +380 -0
- package/dist/core/harnesses/bench.d.ts +29 -0
- package/dist/core/harnesses/bench.d.ts.map +1 -0
- package/dist/core/harnesses/bench.js +151 -0
- package/dist/core/harnesses/index.d.ts +9 -0
- package/dist/core/harnesses/index.d.ts.map +1 -0
- package/dist/core/harnesses/index.js +4 -0
- package/dist/core/harnesses/profiles.d.ts +62 -0
- package/dist/core/harnesses/profiles.d.ts.map +1 -0
- package/dist/core/harnesses/profiles.js +115 -0
- package/dist/core/harnesses/registry.d.ts +15 -0
- package/dist/core/harnesses/registry.d.ts.map +1 -0
- package/dist/core/harnesses/registry.js +113 -0
- package/dist/core/harnesses/types.d.ts +64 -0
- package/dist/core/harnesses/types.d.ts.map +1 -0
- package/dist/core/harnesses/types.js +20 -0
- package/dist/core/hooks/cli.js +309 -66
- package/dist/core/hooks/events/schema.d.ts +42 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts +7 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +4 -0
- package/dist/core/hooks/harness/parse.d.ts +1 -1
- package/dist/core/hooks/harness/parse.d.ts.map +1 -1
- package/dist/core/hooks/harness/parse.js +4 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
- package/dist/core/hooks/resolve/coord-root.js +28 -6
- package/dist/core/presence/blob.d.ts +40 -0
- package/dist/core/presence/blob.d.ts.map +1 -0
- package/dist/core/presence/blob.js +91 -0
- package/dist/core/presence/git.d.ts +68 -0
- package/dist/core/presence/git.d.ts.map +1 -0
- package/dist/core/presence/git.js +173 -0
- package/dist/core/presence/index.d.ts +74 -0
- package/dist/core/presence/index.d.ts.map +1 -0
- package/dist/core/presence/index.js +224 -0
- package/dist/core/presence/relay-client.d.ts +39 -0
- package/dist/core/presence/relay-client.d.ts.map +1 -0
- package/dist/core/presence/relay-client.js +295 -0
- package/dist/core/presence/relay-protocol.d.ts +94 -0
- package/dist/core/presence/relay-protocol.d.ts.map +1 -0
- package/dist/core/presence/relay-protocol.js +169 -0
- package/dist/core/workflow/billing.d.ts +48 -0
- package/dist/core/workflow/billing.d.ts.map +1 -0
- package/dist/core/workflow/billing.js +108 -0
- package/dist/core/workflow/child-env.d.ts +30 -0
- package/dist/core/workflow/child-env.d.ts.map +1 -0
- package/dist/core/workflow/child-env.js +43 -0
- package/dist/core/workflow/engine.d.ts +21 -0
- package/dist/core/workflow/engine.d.ts.map +1 -0
- package/dist/core/workflow/engine.js +349 -0
- package/dist/core/workflow/harnesses.d.ts +17 -0
- package/dist/core/workflow/harnesses.d.ts.map +1 -0
- package/dist/core/workflow/harnesses.js +21 -0
- package/dist/core/workflow/spawn-claude.d.ts +25 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
- package/dist/core/workflow/spawn-claude.js +103 -0
- package/dist/core/workflow/spawn-codex.d.ts +22 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
- package/dist/core/workflow/spawn-codex.js +88 -0
- package/dist/core/workflow/spawn-cursor.d.ts +29 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
- package/dist/core/workflow/spawn-cursor.js +91 -0
- package/dist/core/workflow/types.d.ts +152 -0
- package/dist/core/workflow/types.d.ts.map +1 -0
- package/dist/core/workflow/types.js +9 -0
- package/dist/core/workflow/validate.d.ts +15 -0
- package/dist/core/workflow/validate.d.ts.map +1 -0
- package/dist/core/workflow/validate.js +70 -0
- package/dist/lib/browser/client.d.ts +16 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +22 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -0
- package/dist/lib/browser/launch-args.d.ts +21 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -0
- package/dist/lib/browser/launch-args.js +32 -0
- package/dist/lib/claude-desktop.d.ts +105 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -0
- package/dist/lib/claude-desktop.js +217 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +5 -2
- package/dist/lib/identities/assume.d.ts +51 -0
- package/dist/lib/identities/assume.d.ts.map +1 -0
- package/dist/lib/identities/assume.js +274 -0
- package/dist/lib/identities/index.d.ts +7 -7
- package/dist/lib/identities/index.d.ts.map +1 -1
- package/dist/lib/identities/index.js +24 -24
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +15 -2
- package/package.json +12 -1
- package/schemas/config.schema.json +88 -19
- package/src/commander.ts +8 -0
- package/src/commands/agents.ts +280 -14
- package/src/commands/backup.ts +17 -16
- package/src/commands/browse.ts +32 -0
- package/src/commands/claude-desktop.ts +215 -0
- package/src/commands/context.ts +174 -1
- package/src/commands/doctor.ts +44 -0
- package/src/commands/eml.ts +22 -4
- package/src/commands/grep.ts +535 -142
- package/src/commands/harness.ts +147 -0
- package/src/commands/init.ts +138 -17
- package/src/commands/presence.ts +111 -5
- package/src/commands/relay.ts +160 -0
- package/src/commands/sync.ts +4 -0
- package/src/commands/workflow.ts +123 -0
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +15 -6
- package/src/core/agents/render/prompt-context.ts +48 -3
- package/src/core/agents/render/session-context.ts +27 -1
- package/src/core/agents/rules/claim-conflict.ts +26 -1
- package/src/core/agents/rules/commit-conflict.ts +4 -33
- package/src/core/agents/rules/stop-hook.ts +17 -0
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +23 -2
- package/src/core/agents/state/heartbeat-writer.ts +33 -7
- package/src/core/agents/state/names.ts +68 -9
- package/src/core/agents/state/pidmap.ts +59 -2
- package/src/core/agents/state/stale-sweep.ts +6 -11
- package/src/core/config.ts +271 -24
- package/src/core/context/index.ts +575 -0
- package/src/core/harnesses/bench.ts +214 -0
- package/src/core/harnesses/index.ts +31 -0
- package/src/core/harnesses/profiles.ts +141 -0
- package/src/core/harnesses/registry.ts +140 -0
- package/src/core/harnesses/types.ts +90 -0
- package/src/core/hooks/cli.ts +361 -65
- package/src/core/hooks/events/schema.ts +72 -0
- package/src/core/hooks/harness/events.ts +11 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- package/src/core/hooks/resolve/coord-root.ts +28 -6
- package/src/core/presence/blob.ts +125 -0
- package/src/core/presence/git.ts +191 -0
- package/src/core/presence/index.ts +274 -0
- package/src/core/presence/relay-client.ts +322 -0
- package/src/core/presence/relay-protocol.ts +246 -0
- package/src/core/workflow/billing.ts +152 -0
- package/src/core/workflow/child-env.ts +47 -0
- package/src/core/workflow/engine.ts +403 -0
- package/src/core/workflow/harnesses.ts +33 -0
- package/src/core/workflow/spawn-claude.ts +118 -0
- package/src/core/workflow/spawn-codex.ts +93 -0
- package/src/core/workflow/spawn-cursor.ts +108 -0
- package/src/core/workflow/types.ts +160 -0
- package/src/core/workflow/validate.ts +75 -0
- package/src/lib/browser/client.ts +37 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/launch-args.ts +33 -0
- package/src/lib/claude-desktop.ts +293 -0
- package/src/lib/docs-lint.ts +5 -2
- package/src/lib/identities/assume.ts +363 -0
- package/src/lib/identities/index.ts +28 -24
- package/src/lib/instructions/templates.ts +16 -2
|
@@ -24,6 +24,8 @@ export interface V2Heartbeat {
|
|
|
24
24
|
platform?: string;
|
|
25
25
|
subagent_call_id?: string;
|
|
26
26
|
parent_session_id?: string;
|
|
27
|
+
/** Set iff this owner is a `workflow run` child (joins to the run journal). */
|
|
28
|
+
workflow_run_id?: string;
|
|
27
29
|
started_at?: string;
|
|
28
30
|
last_heartbeat: string;
|
|
29
31
|
last_tool?: string;
|
|
@@ -144,7 +146,8 @@ function seed(ev: CanonicalEvent, coordRoot: string): V2Heartbeat {
|
|
|
144
146
|
if (resolved) {
|
|
145
147
|
hb.name = resolved.name;
|
|
146
148
|
hb.kind = resolved.kind;
|
|
147
|
-
|
|
149
|
+
hb.agent_id =
|
|
150
|
+
resolved.agent_id ?? (resolved.kind === "subagent" ? ev.instance_id : undefined);
|
|
148
151
|
}
|
|
149
152
|
} catch {
|
|
150
153
|
/* name-history unavailable: seed stays nameless; sweep + render guards cope */
|
|
@@ -184,6 +187,8 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
184
187
|
if (subagentCallId) hb.subagent_call_id = subagentCallId;
|
|
185
188
|
const parentSession = pickStr(d, "parent_session_id");
|
|
186
189
|
if (parentSession) hb.parent_session_id = parentSession;
|
|
190
|
+
const workflowRunId = pickStr(d, "workflow_run_id");
|
|
191
|
+
if (workflowRunId) hb.workflow_run_id = workflowRunId;
|
|
187
192
|
if (!hb.files_touched) hb.files_touched = [];
|
|
188
193
|
}
|
|
189
194
|
break;
|
|
@@ -248,8 +253,15 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
248
253
|
if (toolName === "Edit" || toolName === "Write" || toolName === "NotebookEdit") {
|
|
249
254
|
const target = extractFilePath(d);
|
|
250
255
|
if (target) {
|
|
256
|
+
// Canonicalize to repo-relative before storing: the claim guard
|
|
257
|
+
// writes canonical paths directly, so an absolute entry here would
|
|
258
|
+
// double-count the same file (inflated "N files" display) and
|
|
259
|
+
// defeat exact-match pruning on commit.
|
|
260
|
+
const canonical = target.startsWith(`${coordRoot}/`)
|
|
261
|
+
? target.slice(coordRoot.length + 1)
|
|
262
|
+
: target;
|
|
251
263
|
if (!hb.files_touched) hb.files_touched = [];
|
|
252
|
-
if (!hb.files_touched.includes(
|
|
264
|
+
if (!hb.files_touched.includes(canonical)) hb.files_touched.push(canonical);
|
|
253
265
|
}
|
|
254
266
|
}
|
|
255
267
|
break;
|
|
@@ -276,6 +288,14 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
276
288
|
hb.last_status_at = ev.ts;
|
|
277
289
|
break;
|
|
278
290
|
|
|
291
|
+
case "identity.assumed": {
|
|
292
|
+
const name = pickStr(d, "name");
|
|
293
|
+
const agentId = pickStr(d, "agent_id");
|
|
294
|
+
if (name) hb.name = name;
|
|
295
|
+
if (agentId) hb.agent_id = agentId;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
|
|
279
299
|
case "state.presence_change": {
|
|
280
300
|
const to = pickStr(d, "to");
|
|
281
301
|
if (to === "mobile" || to === "office") hb.presence = to;
|
|
@@ -437,6 +457,7 @@ function writeHeartbeat(coordRoot: string, instanceId: string, hb: V2Heartbeat):
|
|
|
437
457
|
setIfDefined(merged, "kind", hb.kind);
|
|
438
458
|
setIfDefined(merged, "agent_id", hb.agent_id);
|
|
439
459
|
setIfDefined(merged, "subagent_call_id", hb.subagent_call_id);
|
|
460
|
+
setIfDefined(merged, "workflow_run_id", hb.workflow_run_id);
|
|
440
461
|
setIfDefined(merged, "model", hb.model);
|
|
441
462
|
setIfDefined(merged, "platform", hb.platform);
|
|
442
463
|
setIfDefined(merged, "started_at", hb.started_at);
|
|
@@ -184,20 +184,39 @@ export function releaseClaim(
|
|
|
184
184
|
}));
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/** A heartbeat that actually dropped a path during a group unclaim. */
|
|
188
|
+
export interface GroupUnclaimHit {
|
|
189
|
+
instance_id: string;
|
|
190
|
+
session_id?: string;
|
|
191
|
+
platform?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
187
194
|
/**
|
|
188
195
|
* Session-group-wide unclaim. Walks every heartbeat sharing `groupId`
|
|
189
196
|
* (parent's session_id == group_id;
|
|
190
197
|
* subagents inherit it) and removes the path from each one's files_touched.
|
|
191
|
-
* Idempotent: heartbeats that don't hold the path are untouched.
|
|
198
|
+
* Idempotent: heartbeats that don't hold the path are untouched. Returns the
|
|
199
|
+
* heartbeats that actually dropped the path so the caller can emit the
|
|
200
|
+
* durable `claim.release` events — a file-only prune is silently reverted by
|
|
201
|
+
* the next projector replay.
|
|
202
|
+
*
|
|
203
|
+
* files_touched can hold either absolute-under-coordRoot or canonical
|
|
204
|
+
* repo-relative entries (legacy projections stored the raw tool_input path),
|
|
205
|
+
* so both sides are normalized before comparing — an exact-string match
|
|
206
|
+
* silently no-ops on the mixed-form case and the claim never releases.
|
|
192
207
|
*
|
|
193
208
|
* This is the Option B fix for post-commit's pid-map attribution hole: a
|
|
194
209
|
* subagent-held claim that doesn't live on the parent's heartbeat still gets
|
|
195
210
|
* pruned because the walk covers the whole group.
|
|
196
211
|
*/
|
|
197
|
-
export function groupUnclaim(coordRoot: string, groupId: string, path: string):
|
|
198
|
-
|
|
212
|
+
export function groupUnclaim(coordRoot: string, groupId: string, path: string): GroupUnclaimHit[] {
|
|
213
|
+
const hits: GroupUnclaimHit[] = [];
|
|
214
|
+
if (!groupId || !path) return hits;
|
|
199
215
|
const activeDir = join(coordRoot, ".harnery", "active");
|
|
200
|
-
if (!existsSync(activeDir)) return;
|
|
216
|
+
if (!existsSync(activeDir)) return hits;
|
|
217
|
+
const norm = (p: string): string =>
|
|
218
|
+
p.startsWith(`${coordRoot}/`) ? p.slice(coordRoot.length + 1) : p;
|
|
219
|
+
const target = norm(path);
|
|
201
220
|
for (const f of readdirSync(activeDir)) {
|
|
202
221
|
if (!f.endsWith(".json")) continue;
|
|
203
222
|
const hbPath = join(activeDir, f);
|
|
@@ -211,17 +230,23 @@ export function groupUnclaim(coordRoot: string, groupId: string, path: string):
|
|
|
211
230
|
(body.session_id as string | undefined) ?? (body.instance_id as string | undefined);
|
|
212
231
|
if (peerSession !== groupId) continue;
|
|
213
232
|
const files = (body.files_touched as string[] | undefined) ?? [];
|
|
214
|
-
const next = files.filter((p) => p !==
|
|
233
|
+
const next = files.filter((p) => norm(p) !== target);
|
|
215
234
|
if (next.length === files.length) continue;
|
|
216
235
|
body.files_touched = next;
|
|
217
236
|
try {
|
|
218
237
|
const tmp = `${hbPath}.tmp.${process.pid}`;
|
|
219
238
|
writeFileSync(tmp, JSON.stringify(body, null, 2), "utf8");
|
|
220
239
|
renameSync(tmp, hbPath);
|
|
240
|
+
hits.push({
|
|
241
|
+
instance_id: (body.instance_id as string | undefined) ?? f.replace(/\.json$/, ""),
|
|
242
|
+
session_id: body.session_id as string | undefined,
|
|
243
|
+
platform: body.platform as string | undefined,
|
|
244
|
+
});
|
|
221
245
|
} catch {
|
|
222
246
|
/* silent */
|
|
223
247
|
}
|
|
224
248
|
}
|
|
249
|
+
return hits;
|
|
225
250
|
}
|
|
226
251
|
|
|
227
252
|
export function killHeartbeat(coordRoot: string, instanceId: string): boolean {
|
|
@@ -286,8 +311,9 @@ export function healHeartbeat(
|
|
|
286
311
|
if (resolved) {
|
|
287
312
|
name = resolved.name;
|
|
288
313
|
kind = resolved.kind;
|
|
289
|
-
//
|
|
290
|
-
|
|
314
|
+
// An explicitly assumed session carries its durable persona UUID in
|
|
315
|
+
// name-history. Native subagents continue to use instance_id.
|
|
316
|
+
agentId = resolved.agent_id ?? (resolved.kind === "subagent" ? instanceId : "");
|
|
291
317
|
}
|
|
292
318
|
} catch {
|
|
293
319
|
/* names module unavailable, fall back to empty */
|
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
* Durable persistence: `.harnery/.name-history` (JSONL, one row per assignment)
|
|
9
9
|
* + `.harnery/.name-counter` (current counter, atomic temp+rename).
|
|
10
10
|
*
|
|
11
|
-
* Recreation rule
|
|
12
|
-
* 1. Own instance_id in name-history → (
|
|
13
|
-
* 2. session_id in name-history (owner != session) →
|
|
11
|
+
* Recreation rule:
|
|
12
|
+
* 1. Own instance_id in name-history → latest (name, kind, agent_id)
|
|
13
|
+
* 2. session_id in name-history (owner != session) → latest parent's
|
|
14
|
+
* (name, agent_id) with kind="transient"
|
|
15
|
+
*
|
|
16
|
+
* Multiple rows for one instance are intentional. `identity assume` appends a
|
|
17
|
+
* new binding instead of rewriting history; readers therefore resolve from the
|
|
18
|
+
* end of the file. Older one-row histories retain their original behavior.
|
|
14
19
|
* 3. Else: new assignment, consume a counter slot.
|
|
15
20
|
*/
|
|
16
21
|
|
|
@@ -297,11 +302,16 @@ if (COORD_NAMES.length !== 260) {
|
|
|
297
302
|
|
|
298
303
|
export type NameKind = "session" | "subagent" | "transient";
|
|
299
304
|
|
|
300
|
-
interface NameHistoryRow {
|
|
305
|
+
export interface NameHistoryRow {
|
|
301
306
|
instance_id: string;
|
|
302
307
|
name: string;
|
|
303
308
|
kind: NameKind;
|
|
304
309
|
ts: string;
|
|
310
|
+
/** Durable persona UUID. Present after `agents identity assume`. */
|
|
311
|
+
agent_id?: string;
|
|
312
|
+
/** Audit marker distinguishing an explicit role adoption from pool assignment. */
|
|
313
|
+
source?: "pool" | "identity.assume";
|
|
314
|
+
previous_name?: string;
|
|
305
315
|
}
|
|
306
316
|
|
|
307
317
|
function atomicWrite(path: string, content: string): void {
|
|
@@ -351,19 +361,29 @@ export function resolveName(
|
|
|
351
361
|
coordRoot: string,
|
|
352
362
|
instanceId: string,
|
|
353
363
|
sessionId?: string,
|
|
354
|
-
): { name: string; kind: NameKind } | null {
|
|
364
|
+
): { name: string; kind: NameKind; agent_id?: string } | null {
|
|
355
365
|
const history = readHistory(coordRoot);
|
|
356
366
|
|
|
357
|
-
for (
|
|
367
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
368
|
+
const row = history[i]!;
|
|
358
369
|
if (row.instance_id === instanceId) {
|
|
359
|
-
return {
|
|
370
|
+
return {
|
|
371
|
+
name: row.name,
|
|
372
|
+
kind: row.kind,
|
|
373
|
+
...(row.agent_id ? { agent_id: row.agent_id } : {}),
|
|
374
|
+
};
|
|
360
375
|
}
|
|
361
376
|
}
|
|
362
377
|
|
|
363
378
|
if (sessionId && sessionId !== instanceId) {
|
|
364
|
-
for (
|
|
379
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
380
|
+
const row = history[i]!;
|
|
365
381
|
if (row.instance_id === sessionId) {
|
|
366
|
-
return {
|
|
382
|
+
return {
|
|
383
|
+
name: row.name,
|
|
384
|
+
kind: "transient",
|
|
385
|
+
...(row.agent_id ? { agent_id: row.agent_id } : {}),
|
|
386
|
+
};
|
|
367
387
|
}
|
|
368
388
|
}
|
|
369
389
|
}
|
|
@@ -371,6 +391,44 @@ export function resolveName(
|
|
|
371
391
|
return null;
|
|
372
392
|
}
|
|
373
393
|
|
|
394
|
+
export interface NameAssumptionResult {
|
|
395
|
+
changed: boolean;
|
|
396
|
+
previous: { name: string; kind: NameKind; agent_id?: string } | null;
|
|
397
|
+
current: { name: string; kind: NameKind; agent_id: string };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Append an explicit instance → durable-persona binding. Latest-row-wins makes
|
|
402
|
+
* this auditable and retry-safe without mutating prior assignment history.
|
|
403
|
+
*/
|
|
404
|
+
export function recordNameAssumption(
|
|
405
|
+
coordRoot: string,
|
|
406
|
+
instanceId: string,
|
|
407
|
+
name: string,
|
|
408
|
+
agentId: string,
|
|
409
|
+
kind: NameKind = "session",
|
|
410
|
+
): NameAssumptionResult {
|
|
411
|
+
const previous = resolveName(coordRoot, instanceId);
|
|
412
|
+
const current = { name, kind, agent_id: agentId };
|
|
413
|
+
if (
|
|
414
|
+
previous?.name === current.name &&
|
|
415
|
+
previous.kind === current.kind &&
|
|
416
|
+
previous.agent_id === current.agent_id
|
|
417
|
+
) {
|
|
418
|
+
return { changed: false, previous, current };
|
|
419
|
+
}
|
|
420
|
+
appendHistory(coordRoot, {
|
|
421
|
+
instance_id: instanceId,
|
|
422
|
+
name,
|
|
423
|
+
kind,
|
|
424
|
+
agent_id: agentId,
|
|
425
|
+
source: "identity.assume",
|
|
426
|
+
previous_name: previous?.name,
|
|
427
|
+
ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
428
|
+
});
|
|
429
|
+
return { changed: true, previous, current };
|
|
430
|
+
}
|
|
431
|
+
|
|
374
432
|
/**
|
|
375
433
|
* Assign a name to <instanceId> with the given <kind>. Counter-consuming when
|
|
376
434
|
* the owner is new. Idempotent: returns existing name on resume.
|
|
@@ -393,6 +451,7 @@ export function assignName(coordRoot: string, instanceId: string, kind: NameKind
|
|
|
393
451
|
instance_id: instanceId,
|
|
394
452
|
name,
|
|
395
453
|
kind,
|
|
454
|
+
source: "pool",
|
|
396
455
|
ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
397
456
|
});
|
|
398
457
|
return name;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pid-map writer.
|
|
2
|
+
* Pid-map writer + liveness helpers.
|
|
3
3
|
*
|
|
4
4
|
* Per-harness pid-map at `.harnery/pid-map/<pid>` containing
|
|
5
5
|
* `<instance_id>\t<platform>`. `harn agents whoami` walks ppid up 20 hops looking
|
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
* Atomic temp+rename. Idempotent: re-writing the same row is a no-op.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
renameSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from "node:fs";
|
|
12
20
|
import { dirname, join } from "node:path";
|
|
13
21
|
|
|
14
22
|
function atomicWrite(path: string, content: string): void {
|
|
@@ -36,3 +44,52 @@ export function writePidmapRow(
|
|
|
36
44
|
}
|
|
37
45
|
atomicWrite(path, row);
|
|
38
46
|
}
|
|
47
|
+
|
|
48
|
+
/** True when any pid-map row for `instanceId` still belongs to a live process. */
|
|
49
|
+
export function instanceHasLivePid(coordRoot: string, instanceId: string): boolean {
|
|
50
|
+
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
51
|
+
if (!existsSync(dir)) return false;
|
|
52
|
+
for (const f of readdirSync(dir)) {
|
|
53
|
+
let row = "";
|
|
54
|
+
try {
|
|
55
|
+
row = readFileSync(join(dir, f), "utf8").trim();
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const owner = row.split("\t")[0]?.trim() ?? "";
|
|
60
|
+
if (owner !== instanceId) continue;
|
|
61
|
+
const pid = Number.parseInt(f, 10);
|
|
62
|
+
if (!Number.isFinite(pid)) continue;
|
|
63
|
+
try {
|
|
64
|
+
process.kill(pid, 0); // signal 0 = liveness probe
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
// ESRCH (no such process): pid-map entry is stale, keep scanning
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Drop every pid-map row owned by `instanceId`. Returns how many files were removed. */
|
|
74
|
+
export function removePidmapRowsForInstance(coordRoot: string, instanceId: string): number {
|
|
75
|
+
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
76
|
+
if (!existsSync(dir)) return 0;
|
|
77
|
+
let removed = 0;
|
|
78
|
+
for (const f of readdirSync(dir)) {
|
|
79
|
+
let row = "";
|
|
80
|
+
try {
|
|
81
|
+
row = readFileSync(join(dir, f), "utf8").trim();
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const owner = row.split("\t")[0]?.trim() ?? "";
|
|
86
|
+
if (owner !== instanceId) continue;
|
|
87
|
+
try {
|
|
88
|
+
unlinkSync(join(dir, f));
|
|
89
|
+
removed += 1;
|
|
90
|
+
} catch {
|
|
91
|
+
/* best-effort cleanup */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return removed;
|
|
95
|
+
}
|
|
@@ -5,17 +5,16 @@
|
|
|
5
5
|
* Fires at session.start to clean up crashed-peer detritus before the new
|
|
6
6
|
* session's UX layer reads peer state.
|
|
7
7
|
*
|
|
8
|
-
* Freshness threshold defaults to 600s; configurable via
|
|
9
|
-
* HARNERY_AGENT_COORD_FRESHNESS env var.
|
|
8
|
+
* Freshness threshold defaults to 600s; configurable via the
|
|
9
|
+
* HARNERY_AGENT_COORD_FRESHNESS env var or `.harnery/config.jsonc`
|
|
10
|
+
* `coord.freshness_seconds` (see `coordFreshnessSeconds`).
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
13
14
|
import { join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
+
import { coordFreshnessSeconds } from "../../config.ts";
|
|
15
16
|
import { emit } from "../events/emit.ts";
|
|
16
17
|
|
|
17
|
-
const DEFAULT_FRESHNESS_SECS = 600;
|
|
18
|
-
|
|
19
18
|
/** platform → harness, for the swept-event envelope (mirrors heartbeat-writer's harnessOf). */
|
|
20
19
|
function harnessFromPlatform(platform: unknown): "claude-code" | "cursor" | "codex" {
|
|
21
20
|
if (platform === "cursor") return "cursor";
|
|
@@ -65,13 +64,9 @@ export function staleSweep(coordRoot: string): {
|
|
|
65
64
|
let pidmapsRemoved = 0;
|
|
66
65
|
let peerHashesRemoved = 0;
|
|
67
66
|
|
|
68
|
-
const freshness =
|
|
69
|
-
coordEnv("AGENT_COORD_FRESHNESS") ?? String(DEFAULT_FRESHNESS_SECS),
|
|
70
|
-
10,
|
|
71
|
-
);
|
|
67
|
+
const freshness = coordFreshnessSeconds(coordRoot);
|
|
72
68
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
73
|
-
const cutoff =
|
|
74
|
-
nowSec - (Number.isFinite(freshness) && freshness > 0 ? freshness : DEFAULT_FRESHNESS_SECS);
|
|
69
|
+
const cutoff = nowSec - freshness;
|
|
75
70
|
|
|
76
71
|
// 1. Prune stale heartbeats from the canonical `.harnery/active/` dir.
|
|
77
72
|
//
|