harnery 0.34.0 → 0.35.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/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/agents.d.ts +15 -4
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +91 -162
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +26 -2
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +2 -2
- package/dist/commands/events.d.ts.map +1 -1
- package/dist/commands/events.js +18 -1
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +2 -2
- package/dist/commands/semantic.d.ts +4 -0
- package/dist/commands/semantic.d.ts.map +1 -0
- package/dist/commands/semantic.js +147 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +17 -0
- package/dist/core/agents/canonical-emit.d.ts +3 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +12 -0
- package/dist/core/agents/claim-path.d.ts +11 -0
- package/dist/core/agents/claim-path.d.ts.map +1 -0
- package/dist/core/agents/claim-path.js +22 -0
- package/dist/core/agents/coord-client.d.ts +29 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +88 -9
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +1 -3
- package/dist/core/agents/live-lifecycle-v3.js +3 -3
- package/dist/core/agents/live-observation-v3.d.ts.map +1 -1
- package/dist/core/agents/live-observation-v3.js +2 -2
- package/dist/core/agents/render/prompt-context.js +12 -10
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +23 -11
- package/dist/core/agents/session-name-display.d.ts +2 -1
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +10 -3
- package/dist/core/agents/state/heartbeat-reader.d.ts +46 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-reader.js +25 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts +3 -48
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +16 -34
- package/dist/core/agents/state/live-coordination-view.d.ts +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +25 -1
- package/dist/core/agents/state/names.d.ts +9 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +190 -182
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +23 -5
- package/dist/core/artifacts/constants.d.ts +5 -0
- package/dist/core/artifacts/constants.d.ts.map +1 -0
- package/dist/core/artifacts/constants.js +4 -0
- package/dist/core/artifacts/index.d.ts +3 -3
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +50 -7
- package/dist/core/events/v3/base-contract.d.ts +795 -0
- package/dist/core/events/v3/base-contract.d.ts.map +1 -1
- package/dist/core/events/v3/base-contract.js +33 -0
- package/dist/core/events/v3/capabilities.d.ts +11 -0
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +23 -0
- package/dist/core/events/v3/generated.d.ts +1 -1
- package/dist/core/events/v3/generated.js +1 -1
- package/dist/core/events/v3/index.d.ts +4 -3
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +4 -3
- package/dist/core/events/v3/latency.d.ts +31 -1
- package/dist/core/events/v3/latency.d.ts.map +1 -1
- package/dist/core/events/v3/latency.js +127 -3
- package/dist/core/events/v3/live-routing.d.ts +30 -0
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +109 -1
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +10 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +14 -3
- package/dist/core/events/v3/producers/hook-base.d.ts +17 -1
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +82 -1
- package/dist/core/events/v3/producers/intake.d.ts +6 -0
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +11 -0
- package/dist/core/events/v3/producers/recorder.d.ts +62 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +654 -18
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +51 -0
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -0
- package/dist/core/events/v3/runtime-telemetry-capabilities.js +54 -0
- package/dist/core/events/v3/turn-telemetry.d.ts +9 -0
- package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.d.ts +3 -0
- package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.js +14 -0
- package/dist/core/hooks/adapter/runtime-telemetry.d.ts +119 -0
- package/dist/core/hooks/adapter/runtime-telemetry.d.ts.map +1 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +568 -0
- package/dist/core/hooks/cli.js +114 -38
- package/dist/core/hooks/remediation-cap.d.ts +34 -0
- package/dist/core/hooks/remediation-cap.d.ts.map +1 -0
- package/dist/core/hooks/remediation-cap.js +71 -0
- package/dist/core/hooks/resolve/transcript.d.ts +25 -9
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +35 -17
- package/dist/core/semantic/adapters.d.ts +34 -0
- package/dist/core/semantic/adapters.d.ts.map +1 -0
- package/dist/core/semantic/adapters.js +222 -0
- package/dist/core/semantic/contract.d.ts +588 -0
- package/dist/core/semantic/contract.d.ts.map +1 -0
- package/dist/core/semantic/contract.js +230 -0
- package/dist/core/semantic/evidence.d.ts +11 -0
- package/dist/core/semantic/evidence.d.ts.map +1 -0
- package/dist/core/semantic/evidence.js +262 -0
- package/dist/core/semantic/index.d.ts +10 -0
- package/dist/core/semantic/index.d.ts.map +1 -0
- package/dist/core/semantic/index.js +9 -0
- package/dist/core/semantic/once.d.ts +33 -0
- package/dist/core/semantic/once.d.ts.map +1 -0
- package/dist/core/semantic/once.js +293 -0
- package/dist/core/semantic/prompt.d.ts +12 -0
- package/dist/core/semantic/prompt.d.ts.map +1 -0
- package/dist/core/semantic/prompt.js +70 -0
- package/dist/core/semantic/scheduler.d.ts +16 -0
- package/dist/core/semantic/scheduler.d.ts.map +1 -0
- package/dist/core/semantic/scheduler.js +67 -0
- package/dist/core/semantic/service-status.d.ts +31 -0
- package/dist/core/semantic/service-status.d.ts.map +1 -0
- package/dist/core/semantic/service-status.js +84 -0
- package/dist/core/semantic/service.d.ts +51 -0
- package/dist/core/semantic/service.d.ts.map +1 -0
- package/dist/core/semantic/service.js +470 -0
- package/dist/core/semantic/storage.d.ts +71 -0
- package/dist/core/semantic/storage.d.ts.map +1 -0
- package/dist/core/semantic/storage.js +179 -0
- package/dist/core/semantic/validate.d.ts +16 -0
- package/dist/core/semantic/validate.d.ts.map +1 -0
- package/dist/core/semantic/validate.js +174 -0
- package/dist/lib/browser/client.d.ts +11 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +13 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/standalone-html.d.ts +25 -0
- package/dist/lib/browser/standalone-html.d.ts.map +1 -0
- package/dist/lib/browser/standalone-html.js +284 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +1 -0
- package/dist/lib/instructions/templates.d.ts +2 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +144 -4
- package/package.json +6 -1
- package/schemas/event-v3.schema.json +2758 -478
- package/src/commander.ts +2 -0
- package/src/commands/agents.ts +104 -152
- package/src/commands/browse.ts +25 -2
- package/src/commands/decision.ts +8 -2
- package/src/commands/events.ts +21 -1
- package/src/commands/presence.ts +7 -2
- package/src/commands/semantic.ts +179 -0
- package/src/commands/web.ts +15 -0
- package/src/core/agents/canonical-emit.ts +15 -0
- package/src/core/agents/claim-path.ts +23 -0
- package/src/core/agents/coord-client.ts +92 -8
- package/src/core/agents/live-authority-v3.ts +1 -4
- package/src/core/agents/live-lifecycle-v3.ts +8 -3
- package/src/core/agents/live-observation-v3.ts +7 -2
- package/src/core/agents/render/prompt-context.ts +13 -10
- package/src/core/agents/rules/stop-hook.ts +24 -11
- package/src/core/agents/session-name-display.ts +11 -3
- package/src/core/agents/state/heartbeat-reader.ts +69 -0
- package/src/core/agents/state/heartbeat-writer.ts +20 -84
- package/src/core/agents/state/live-coordination-view.ts +26 -1
- package/src/core/agents/state/names.ts +190 -182
- package/src/core/agents/state/stale-sweep.ts +25 -5
- package/src/core/artifacts/constants.ts +4 -0
- package/src/core/artifacts/index.ts +49 -7
- package/src/core/events/v3/base-contract.ts +39 -0
- package/src/core/events/v3/capabilities.ts +34 -0
- package/src/core/events/v3/generated.ts +1 -1
- package/src/core/events/v3/index.ts +21 -0
- package/src/core/events/v3/latency.ts +213 -2
- package/src/core/events/v3/live-routing.ts +120 -1
- package/src/core/events/v3/producers/command-recorder.ts +10 -1
- package/src/core/events/v3/producers/coordination-recorder.ts +17 -5
- package/src/core/events/v3/producers/hook-base.ts +115 -1
- package/src/core/events/v3/producers/intake.ts +18 -0
- package/src/core/events/v3/producers/recorder.ts +856 -21
- package/src/core/events/v3/runtime-telemetry-capabilities.ts +137 -0
- package/src/core/events/v3/turn-telemetry.ts +10 -0
- package/src/core/hooks/adapter/parse.ts +16 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +805 -0
- package/src/core/hooks/cli.ts +126 -41
- package/src/core/hooks/remediation-cap.ts +77 -0
- package/src/core/hooks/resolve/transcript.ts +49 -17
- package/src/core/semantic/adapters.ts +293 -0
- package/src/core/semantic/contract.ts +298 -0
- package/src/core/semantic/evidence.ts +336 -0
- package/src/core/semantic/index.ts +127 -0
- package/src/core/semantic/once.ts +410 -0
- package/src/core/semantic/prompt.ts +84 -0
- package/src/core/semantic/scheduler.ts +107 -0
- package/src/core/semantic/service-status.ts +118 -0
- package/src/core/semantic/service.ts +580 -0
- package/src/core/semantic/storage.ts +286 -0
- package/src/core/semantic/validate.ts +212 -0
- package/src/lib/browser/client.ts +21 -0
- package/src/lib/browser/index.ts +4 -0
- package/src/lib/browser/standalone-html.ts +312 -0
- package/src/lib/instructions/apply.ts +1 -0
- package/src/lib/instructions/templates.ts +149 -4
package/src/core/hooks/cli.ts
CHANGED
|
@@ -36,10 +36,14 @@ import {
|
|
|
36
36
|
isSessionNameRemediationCommand,
|
|
37
37
|
sessionNameDisplayInstruction,
|
|
38
38
|
sessionNameDisplayPending,
|
|
39
|
+
sessionNameDisplayRecoveryInstruction,
|
|
39
40
|
toolResponseMintedSessionName,
|
|
40
41
|
} from "../agents/session-name-display.ts";
|
|
41
42
|
import { stampSessionNameSeen } from "../agents/state/heartbeat-writer.ts";
|
|
42
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
ensureLiveCoordinationHeartbeat,
|
|
45
|
+
readLiveCoordinationRow,
|
|
46
|
+
} from "../agents/state/live-coordination-view.ts";
|
|
43
47
|
import { writePidmapRow } from "../agents/state/pidmap.ts";
|
|
44
48
|
import { agentsRequireGitFinalization, resolveBinName } from "../config.ts";
|
|
45
49
|
import {
|
|
@@ -54,6 +58,8 @@ import {
|
|
|
54
58
|
} from "../context/index.ts";
|
|
55
59
|
import { tryWriteLiveDisplayV3 } from "../events/v3/live-feed.ts";
|
|
56
60
|
import {
|
|
61
|
+
liveHookSignalDefersDrainV3,
|
|
62
|
+
recordLiveDelegatedChildSessionV3,
|
|
57
63
|
recordLiveHookSignalV3,
|
|
58
64
|
resolveLiveEventLedgerRouteV3,
|
|
59
65
|
} from "../events/v3/live-routing.ts";
|
|
@@ -69,6 +75,7 @@ import {
|
|
|
69
75
|
type ParsedPayload,
|
|
70
76
|
parsePayload,
|
|
71
77
|
} from "./adapter/parse.ts";
|
|
78
|
+
import { discoverCodexSessionTranscript } from "./adapter/runtime-telemetry.ts";
|
|
72
79
|
import {
|
|
73
80
|
codexWslFileLinkTelemetry,
|
|
74
81
|
inspectCodexWslBridge,
|
|
@@ -99,7 +106,7 @@ import {
|
|
|
99
106
|
scanAssistantTextIncludes,
|
|
100
107
|
scanSessionNameDisplayedImmediately,
|
|
101
108
|
scanStatusBoxPresent,
|
|
102
|
-
|
|
109
|
+
scanTranscriptRuntime,
|
|
103
110
|
} from "./resolve/transcript.ts";
|
|
104
111
|
import { sessionNamePresence } from "./session-name-presence.ts";
|
|
105
112
|
import { unsafeCrossShellReason } from "./unsafe-cross-shell.ts";
|
|
@@ -312,7 +319,7 @@ function buildEventData(
|
|
|
312
319
|
// Claude Code's SessionStart payload omits `model` (Codex + Cursor
|
|
313
320
|
// supply it). Fall back to the transcript, populated on `resume`, and
|
|
314
321
|
// backfilled later by `turn.completed` for a fresh startup session.
|
|
315
|
-
model: p?.model ??
|
|
322
|
+
model: p?.model ?? scanTranscriptRuntime(p?.transcript_path)?.model,
|
|
316
323
|
pid: adapterPid,
|
|
317
324
|
source: p?.source,
|
|
318
325
|
platform: adapterPlatform,
|
|
@@ -379,7 +386,7 @@ function buildEventData(
|
|
|
379
386
|
// Backfill the model for adapters that omit it at session.started
|
|
380
387
|
// (Claude Code). The transcript is populated with assistant turns by
|
|
381
388
|
// Stop-hook time, so this resolves even for fresh `startup` sessions.
|
|
382
|
-
model: p?.model ??
|
|
389
|
+
model: p?.model ?? scanTranscriptRuntime(p?.transcript_path)?.model,
|
|
383
390
|
// Phase 2: tool_call_count + text_length aren't cheaply available
|
|
384
391
|
// from the Stop payload alone (they'd require a transcript scan that
|
|
385
392
|
// races with the JSONL flush). Emit `-1` / `0` sentinels and let
|
|
@@ -394,15 +401,13 @@ function buildEventData(
|
|
|
394
401
|
// (the previous path passed those via the no-history fail-open).
|
|
395
402
|
status_box_present:
|
|
396
403
|
scanStatusBoxPresent(p?.transcript_path) || lastAssistantMessage.includes("┌─ agent-"),
|
|
397
|
-
//
|
|
404
|
+
// Assistant-text evidence for the rule-2/3 detector: the loose scan above
|
|
398
405
|
// matches the box prefix ANYWHERE in the transcript tail, including
|
|
399
406
|
// the tool_result row the status command itself writes, so it can
|
|
400
407
|
// report a paste that never happened. The strict variant counts
|
|
401
408
|
// assistant text only (same window, same scanner as the session-name
|
|
402
|
-
// detection).
|
|
403
|
-
//
|
|
404
|
-
// is known, the gate either flips to strict or the rule gets
|
|
405
|
-
// reconsidered; either way this field decides it with data.
|
|
409
|
+
// detection). The Stop verdict enforces this field; the loose field is
|
|
410
|
+
// retained as diagnostic telemetry for the measured detector divergence.
|
|
406
411
|
status_box_present_strict:
|
|
407
412
|
scanAssistantTextIncludes(p?.transcript_path, "┌─ agent-") ||
|
|
408
413
|
lastAssistantMessage.includes("┌─ agent-"),
|
|
@@ -412,7 +417,12 @@ function buildEventData(
|
|
|
412
417
|
// transcript scan itself stops once the name has been sighted.
|
|
413
418
|
...sessionNamePresence(ctx.coordRoot, ctx.instanceId, (name) =>
|
|
414
419
|
scanSessionNameDisplayedImmediately(
|
|
415
|
-
|
|
420
|
+
// Codex stops carry no transcript_path; discover the rollout by
|
|
421
|
+
// session id so the naming ritual can verify and stamp there too.
|
|
422
|
+
p?.transcript_path ??
|
|
423
|
+
(ctx.adapter === "codex"
|
|
424
|
+
? discoverCodexSessionTranscript(p?.session_id ?? ctx.instanceId)
|
|
425
|
+
: undefined),
|
|
416
426
|
name,
|
|
417
427
|
assistantTextStartsWithSessionNameBlock,
|
|
418
428
|
),
|
|
@@ -428,7 +438,8 @@ function buildEventData(
|
|
|
428
438
|
// Subagents inherit parent's name via the resolve-name session_id path
|
|
429
439
|
// (agent-coord/state/names.ts → kind=transient). Use the call ID as the
|
|
430
440
|
// instance_id input; assignName falls through to transient.
|
|
431
|
-
const
|
|
441
|
+
const childInstanceId = subagentCallId ?? ctx.instanceId;
|
|
442
|
+
const assigned = assignNameViaAgentCoord(ctx.coordRoot, childInstanceId, "subagent");
|
|
432
443
|
return {
|
|
433
444
|
agent_type:
|
|
434
445
|
(p?.raw.agent_type as string | undefined) ??
|
|
@@ -437,7 +448,7 @@ function buildEventData(
|
|
|
437
448
|
prompt_summary: p?.raw.prompt_summary as string | undefined,
|
|
438
449
|
name: assigned?.name,
|
|
439
450
|
kind: "subagent",
|
|
440
|
-
agent_id:
|
|
451
|
+
agent_id: childInstanceId,
|
|
441
452
|
subagent_call_id: subagentCallId,
|
|
442
453
|
parent_session_id: p?.parent_session_id,
|
|
443
454
|
};
|
|
@@ -599,13 +610,24 @@ async function main(): Promise<number> {
|
|
|
599
610
|
const repoRoot = findCoordRoot(process.cwd());
|
|
600
611
|
if (repoRoot) {
|
|
601
612
|
let sid = "";
|
|
613
|
+
let stopContinuation = false;
|
|
602
614
|
try {
|
|
603
|
-
const j = JSON.parse(raw) as {
|
|
615
|
+
const j = JSON.parse(raw) as {
|
|
616
|
+
session_id?: string;
|
|
617
|
+
conversation_id?: string;
|
|
618
|
+
stop_hook_active?: boolean;
|
|
619
|
+
};
|
|
604
620
|
sid = j.session_id ?? j.conversation_id ?? "";
|
|
621
|
+
// A stop with stop_hook_active is a remediation continuation of a
|
|
622
|
+
// turn that already chimed; replaying the sound on every bounce
|
|
623
|
+
// turns a blocked-stop loop into a repeating alarm.
|
|
624
|
+
stopContinuation = eventName === "stop" && j.stop_hook_active === true;
|
|
605
625
|
} catch {
|
|
606
626
|
// non-JSON payload: play unkeyed (rate-limit just won't dedup)
|
|
607
627
|
}
|
|
608
|
-
|
|
628
|
+
if (!stopContinuation) {
|
|
629
|
+
playSound(repoRoot, s.sound, sid, s.maxPlays);
|
|
630
|
+
}
|
|
609
631
|
}
|
|
610
632
|
}
|
|
611
633
|
}
|
|
@@ -706,6 +728,10 @@ async function main(): Promise<number> {
|
|
|
706
728
|
}
|
|
707
729
|
}
|
|
708
730
|
|
|
731
|
+
const deferV3Drain = liveHookSignalDefersDrainV3(
|
|
732
|
+
eventName,
|
|
733
|
+
coordEnv("EXPERIMENT_DEFER_V3_DRAIN"),
|
|
734
|
+
);
|
|
709
735
|
const v3Result = recordLiveHookSignalV3({
|
|
710
736
|
coordRoot,
|
|
711
737
|
route: ledgerRoute,
|
|
@@ -728,7 +754,7 @@ async function main(): Promise<number> {
|
|
|
728
754
|
monotonic_ns: hookClock.monotonic_ns,
|
|
729
755
|
...(stopRemediation ? { stop_remediation: true } : {}),
|
|
730
756
|
...(turnRitual ? { turn_ritual: turnRitual } : {}),
|
|
731
|
-
...(
|
|
757
|
+
...(deferV3Drain ? { defer_drain: true } : {}),
|
|
732
758
|
});
|
|
733
759
|
const v3EventId =
|
|
734
760
|
v3Result && "event" in v3Result
|
|
@@ -737,6 +763,36 @@ async function main(): Promise<number> {
|
|
|
737
763
|
? v3Result.event_id
|
|
738
764
|
: undefined;
|
|
739
765
|
|
|
766
|
+
if (
|
|
767
|
+
norm.event_type === "agent.started" &&
|
|
768
|
+
payload &&
|
|
769
|
+
v3Result?.state === "recorded" &&
|
|
770
|
+
v3Result.event.event_type === "agent.started"
|
|
771
|
+
) {
|
|
772
|
+
try {
|
|
773
|
+
const childResult = recordLiveDelegatedChildSessionV3({
|
|
774
|
+
coordRoot,
|
|
775
|
+
route: ledgerRoute,
|
|
776
|
+
parentEvent: v3Result.event,
|
|
777
|
+
payload,
|
|
778
|
+
adapter,
|
|
779
|
+
...(adapter === "codex" && isWslUncPath(payload.cwd)
|
|
780
|
+
? { bridge: "codex-wsl" as const }
|
|
781
|
+
: {}),
|
|
782
|
+
monotonic_ns: hookClock.monotonic_ns,
|
|
783
|
+
});
|
|
784
|
+
const nativeChild = payload.subagent_id ?? payload.agent_id;
|
|
785
|
+
if (
|
|
786
|
+
nativeChild &&
|
|
787
|
+
(childResult.state === "recorded" || childResult.state === "already_started")
|
|
788
|
+
) {
|
|
789
|
+
ensureLiveCoordinationHeartbeat(coordRoot, nativeChild, sessionId, adapter, payload.model);
|
|
790
|
+
}
|
|
791
|
+
} catch (err) {
|
|
792
|
+
logError(coordRoot, err, { phase: "subagent-child-session-start" });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
740
796
|
if (v3Result?.state === "recorded" && capturedImages.length > 0) {
|
|
741
797
|
try {
|
|
742
798
|
recordImageArtifactsV3(coordRoot, v3Result.event, capturedImages);
|
|
@@ -889,11 +945,12 @@ async function main(): Promise<number> {
|
|
|
889
945
|
}
|
|
890
946
|
}
|
|
891
947
|
|
|
892
|
-
// Phase 8: SubagentStart:
|
|
893
|
-
//
|
|
894
|
-
//
|
|
948
|
+
// Phase 8: SubagentStart: the child generation was opened above from the
|
|
949
|
+
// native delegation identity. Refresh its disposable projection, log the
|
|
950
|
+
// lifecycle event, and emit a context message announcing the subagent.
|
|
895
951
|
if (norm.event_type === "agent.started") {
|
|
896
952
|
try {
|
|
953
|
+
const childInstanceId = (data.agent_id as string | undefined) ?? owner.instance_id;
|
|
897
954
|
const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
898
955
|
if (existsSync(agentCoordBin)) {
|
|
899
956
|
spawnSync(agentCoordBin, ["project"], {
|
|
@@ -905,14 +962,14 @@ async function main(): Promise<number> {
|
|
|
905
962
|
agentCoordBin,
|
|
906
963
|
[
|
|
907
964
|
"log",
|
|
908
|
-
`SUBAGENT_START agent_type=${(data.agent_type as string) ?? "unknown"} agent_id=${
|
|
965
|
+
`SUBAGENT_START agent_type=${(data.agent_type as string) ?? "unknown"} agent_id=${childInstanceId.slice(0, 8)} platform=${adapterPlatform(adapter)}`,
|
|
909
966
|
"--instance",
|
|
910
|
-
|
|
967
|
+
childInstanceId,
|
|
911
968
|
],
|
|
912
969
|
{ encoding: "utf8", timeout: 2000, env: childEnv(coordRoot) },
|
|
913
970
|
);
|
|
914
971
|
}
|
|
915
|
-
emitSubagentStartContext(coordRoot,
|
|
972
|
+
emitSubagentStartContext(coordRoot, childInstanceId, sessionId, data, adapter);
|
|
916
973
|
} catch (err) {
|
|
917
974
|
logError(coordRoot, err, { phase: "subagent-start-project" });
|
|
918
975
|
}
|
|
@@ -921,20 +978,23 @@ async function main(): Promise<number> {
|
|
|
921
978
|
// Phase 8: SubagentStop: delete subagent heartbeat + log.
|
|
922
979
|
if (norm.event_type === "agent.completed") {
|
|
923
980
|
try {
|
|
924
|
-
|
|
981
|
+
const childInstanceId = payload?.subagent_id ?? payload?.agent_id ?? owner.instance_id;
|
|
982
|
+
cleanupSessionEnd(coordRoot, childInstanceId, (data.reason as string) ?? "unknown");
|
|
925
983
|
const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
926
984
|
if (existsSync(agentCoordBin)) {
|
|
927
985
|
spawnSync(
|
|
928
986
|
agentCoordBin,
|
|
929
987
|
[
|
|
930
988
|
"log",
|
|
931
|
-
`SUBAGENT_STOP agent_id=${
|
|
989
|
+
`SUBAGENT_STOP agent_id=${childInstanceId.slice(0, 8)} platform=${adapterPlatform(adapter)}`,
|
|
932
990
|
"--instance",
|
|
933
|
-
|
|
991
|
+
childInstanceId,
|
|
934
992
|
],
|
|
935
993
|
{ encoding: "utf8", timeout: 2000, env: childEnv(coordRoot) },
|
|
936
994
|
);
|
|
937
995
|
}
|
|
996
|
+
const { reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.ts");
|
|
997
|
+
reconcileSessionFinalizationV3(coordRoot, { archive_observations: [] });
|
|
938
998
|
} catch (err) {
|
|
939
999
|
logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
|
|
940
1000
|
}
|
|
@@ -1013,12 +1073,30 @@ async function main(): Promise<number> {
|
|
|
1013
1073
|
workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
|
|
1014
1074
|
});
|
|
1015
1075
|
if (!verdict.allow) {
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
// `
|
|
1019
|
-
//
|
|
1020
|
-
|
|
1021
|
-
|
|
1076
|
+
// Backstop for sessions whose evidence can never land (e.g. a headless
|
|
1077
|
+
// child whose status command resolves to a different owner): after
|
|
1078
|
+
// `cap` consecutive blocked stops in one cycle, allow the stop instead
|
|
1079
|
+
// of bouncing the model forever. Cycle boundaries come from the
|
|
1080
|
+
// adapter's continuation flag, so adapters without one are unaffected.
|
|
1081
|
+
const { remediationCapExceeded } = await import("./remediation-cap.ts");
|
|
1082
|
+
if (remediationCapExceeded(sessionId, payload?.stop_hook_active === true)) {
|
|
1083
|
+
appendDebug(coordRoot, {
|
|
1084
|
+
...debugBase,
|
|
1085
|
+
skipped: "stop-remediation-cap-exhausted",
|
|
1086
|
+
blocked_rule: verdict.rule,
|
|
1087
|
+
session_id: sessionId,
|
|
1088
|
+
});
|
|
1089
|
+
} else {
|
|
1090
|
+
// Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
|
|
1091
|
+
// as a turn block; Cursor ignores exit codes and re-prompts only via a
|
|
1092
|
+
// `followup_message` it auto-submits. Codex returned observe-only above.
|
|
1093
|
+
// emitStopBlock writes the right shape and returns the exit code to use.
|
|
1094
|
+
const { emitStopBlock } = await import("./adapter/output.ts");
|
|
1095
|
+
return emitStopBlock(adapter, verdict, coordRoot);
|
|
1096
|
+
}
|
|
1097
|
+
} else {
|
|
1098
|
+
const { clearRemediationCount } = await import("./remediation-cap.ts");
|
|
1099
|
+
clearRemediationCount(sessionId);
|
|
1022
1100
|
}
|
|
1023
1101
|
|
|
1024
1102
|
// An explicit end requested from inside this turn cannot be authoritative
|
|
@@ -1117,8 +1195,9 @@ async function main(): Promise<number> {
|
|
|
1117
1195
|
}
|
|
1118
1196
|
|
|
1119
1197
|
if (norm.event_type === "tool.completed" && eventName === "post-tool-use") {
|
|
1120
|
-
// Inject only at the successful tool boundary that actually minted
|
|
1121
|
-
// the first non-empty set-task
|
|
1198
|
+
// Inject only at the successful tool boundary that actually minted or
|
|
1199
|
+
// retried a name: the first non-empty set-task, a pending-name set-task
|
|
1200
|
+
// retry, or the transition to lifecycle done. The
|
|
1122
1201
|
// coordination row intentionally remains pending until transcript evidence
|
|
1123
1202
|
// catches up; reading that latch alone here would re-inject after every
|
|
1124
1203
|
// later tool and make the agent print the same block repeatedly.
|
|
@@ -1159,14 +1238,21 @@ async function enforcePendingSessionNameDisplay(
|
|
|
1159
1238
|
if (isSessionNameRemediationCommand(command, resolveBinName(coordRoot))) return true;
|
|
1160
1239
|
|
|
1161
1240
|
const inspection =
|
|
1162
|
-
payload?.agent_message !== undefined
|
|
1241
|
+
adapter === "cursor" && payload?.agent_message !== undefined
|
|
1163
1242
|
? {
|
|
1164
1243
|
state: assistantTextStartsWithSessionNameBlock(payload.agent_message, name)
|
|
1165
1244
|
? ("present" as const)
|
|
1166
1245
|
: ("absent" as const),
|
|
1167
1246
|
}
|
|
1168
1247
|
: inspectSessionNameDisplayImmediately(
|
|
1169
|
-
|
|
1248
|
+
// Codex hook payloads omit transcript_path on every event, which
|
|
1249
|
+
// left this inspection permanently unavailable and the latch never
|
|
1250
|
+
// stamped. Discover the rollout by session id; the scan only runs
|
|
1251
|
+
// while a name is pending, so the latch closes after one success.
|
|
1252
|
+
payload?.transcript_path ??
|
|
1253
|
+
(adapter === "codex"
|
|
1254
|
+
? discoverCodexSessionTranscript(payload?.session_id ?? instanceId)
|
|
1255
|
+
: undefined),
|
|
1170
1256
|
name,
|
|
1171
1257
|
assistantTextStartsWithSessionNameBlock,
|
|
1172
1258
|
);
|
|
@@ -1176,17 +1262,16 @@ async function enforcePendingSessionNameDisplay(
|
|
|
1176
1262
|
}
|
|
1177
1263
|
|
|
1178
1264
|
if (inspection.state === "unavailable") {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
);
|
|
1265
|
+
// PostToolUse already delivered the exact block for this mint. An
|
|
1266
|
+
// unavailable transcript cannot become evidence, and repeating a generic
|
|
1267
|
+
// reminder on every tool creates an unbounded Codex loop. Keep the latch
|
|
1268
|
+
// honestly pending and fail open without another instruction. A later
|
|
1269
|
+
// readable transcript can still verify it.
|
|
1185
1270
|
return true;
|
|
1186
1271
|
}
|
|
1187
1272
|
|
|
1188
1273
|
const { emitDeny } = await import("./adapter/output.ts");
|
|
1189
|
-
emitDeny(adapter,
|
|
1274
|
+
emitDeny(adapter, sessionNameDisplayRecoveryInstruction(name, resolveBinName(coordRoot)));
|
|
1190
1275
|
return false;
|
|
1191
1276
|
}
|
|
1192
1277
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session cap on consecutive blocked Stop remediations.
|
|
3
|
+
*
|
|
4
|
+
* The Stop verdict re-blocks until the ledger shows the end-of-turn evidence,
|
|
5
|
+
* which assumes the agent CAN produce it. A session whose evidence can never
|
|
6
|
+
* land (e.g. a headless child whose status command resolves to a different
|
|
7
|
+
* owner) bounces forever: every retry replays the stop cycle until the budget
|
|
8
|
+
* or the operator kills it. This counter is the backstop: once a single stop
|
|
9
|
+
* cycle has been blocked `cap` times in a row, the verdict is allowed through
|
|
10
|
+
* so the session can terminate.
|
|
11
|
+
*
|
|
12
|
+
* A "stop cycle" is delimited by the adapter's continuation flag (Claude
|
|
13
|
+
* Code's `stop_hook_active`): a stop without the flag starts a fresh cycle and
|
|
14
|
+
* resets the count. Adapters that never set the flag (Cursor) reset on every
|
|
15
|
+
* stop, so the cap never engages there and behavior is unchanged.
|
|
16
|
+
*
|
|
17
|
+
* State is a tmpdir counter file keyed by session id — deliberately outside
|
|
18
|
+
* the ledger, because remediation stops do not land fresh `turn.completed`
|
|
19
|
+
* events (the turn span is already closed), so ledger evidence cannot count
|
|
20
|
+
* them.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_STOP_REMEDIATION_CAP = 5;
|
|
28
|
+
|
|
29
|
+
function counterPath(sessionId: string): string {
|
|
30
|
+
const key = (sessionId || "default").replace(/[^A-Za-z0-9_-]/g, "_");
|
|
31
|
+
return join(os.tmpdir(), `harnery-stop-remediation-${key}.count`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record one blocked stop and report whether the cap is exhausted.
|
|
36
|
+
*
|
|
37
|
+
* Call only when the Stop verdict blocked. `continuation` is the adapter's
|
|
38
|
+
* "this stop follows a stop-hook block" flag; false starts a fresh cycle.
|
|
39
|
+
* Returns true when this cycle has already been blocked `cap` times, meaning
|
|
40
|
+
* the caller should allow the stop instead of blocking again.
|
|
41
|
+
*/
|
|
42
|
+
export function remediationCapExceeded(
|
|
43
|
+
sessionId: string,
|
|
44
|
+
continuation: boolean,
|
|
45
|
+
cap = DEFAULT_STOP_REMEDIATION_CAP,
|
|
46
|
+
): boolean {
|
|
47
|
+
const path = counterPath(sessionId);
|
|
48
|
+
let count = 1;
|
|
49
|
+
if (continuation) {
|
|
50
|
+
let prior = 0;
|
|
51
|
+
try {
|
|
52
|
+
prior = Number.parseInt(readFileSync(path, "utf8"), 10);
|
|
53
|
+
} catch {
|
|
54
|
+
// No counter yet (cycle started before the counter existed): treat the
|
|
55
|
+
// continuation itself as evidence of one earlier blocked stop, so an
|
|
56
|
+
// already-running loop still converges on the cap.
|
|
57
|
+
prior = 1;
|
|
58
|
+
}
|
|
59
|
+
count = Number.isFinite(prior) && prior >= 0 ? prior + 1 : 1;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(path, String(count), "utf8");
|
|
63
|
+
} catch {
|
|
64
|
+
// Counter IO must never turn into a block or a crash; fail open (no cap).
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return count > cap;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Clear the cycle counter after an allowed stop. */
|
|
71
|
+
export function clearRemediationCount(sessionId: string): void {
|
|
72
|
+
try {
|
|
73
|
+
rmSync(counterPath(sessionId), { force: true });
|
|
74
|
+
} catch {
|
|
75
|
+
// best-effort
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { toolResponseMintedSessionName } from "../../agents/session-name-display.ts";
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Scan a CC-style JSONL transcript for the `┌─ agent-` status-box prefix in
|
|
6
8
|
* the most-recent assistant turn. Used by `turn.completed` events to populate
|
|
@@ -131,19 +133,23 @@ export function inspectSessionNameDisplayImmediately(
|
|
|
131
133
|
const text = tailText(readablePath);
|
|
132
134
|
if (!text) return { state: "unavailable", reason: "transcript_not_ready" };
|
|
133
135
|
let sawMintResult = false;
|
|
136
|
+
let awaitingReply = false;
|
|
134
137
|
for (const line of text.split("\n")) {
|
|
135
|
-
if (!sawMintResult) {
|
|
136
|
-
if (line.includes(name) && line.includes("suggested_session_name")) sawMintResult = true;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
138
|
const row = parseTranscriptRow(line);
|
|
140
139
|
if (!row) continue;
|
|
140
|
+
if (toolResponseMintedSessionName(row, name)) {
|
|
141
|
+
sawMintResult = true;
|
|
142
|
+
awaitingReply = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!awaitingReply) continue;
|
|
141
146
|
const assistantText = assistantTextFromRow(row);
|
|
142
147
|
if (assistantText !== null) {
|
|
143
|
-
|
|
148
|
+
if (startsWithBlock(assistantText, name)) return { state: "present" };
|
|
149
|
+
awaitingReply = false;
|
|
144
150
|
}
|
|
145
151
|
}
|
|
146
|
-
return sawMintResult
|
|
152
|
+
return sawMintResult && awaitingReply
|
|
147
153
|
? { state: "unavailable", reason: "transcript_not_ready" }
|
|
148
154
|
: { state: "absent" };
|
|
149
155
|
}
|
|
@@ -219,19 +225,38 @@ function textFromContent(content: unknown): string {
|
|
|
219
225
|
return parts.join("\n");
|
|
220
226
|
}
|
|
221
227
|
|
|
228
|
+
/** Runtime identity read from one transcript row: the model plus the tuning
|
|
229
|
+
* values that row reported alongside it. */
|
|
230
|
+
export interface TranscriptRuntimeScan {
|
|
231
|
+
model: string;
|
|
232
|
+
/** CC stamps the effective effort level on each assistant row; absent when
|
|
233
|
+
* the model has no effort dial (verified: haiku rows omit it entirely). */
|
|
234
|
+
effort?: string;
|
|
235
|
+
/** CC reports the response speed tier inside `message.usage`. */
|
|
236
|
+
speed?: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
222
239
|
/**
|
|
223
|
-
* Resolve the agent's
|
|
224
|
-
* most-recent assistant
|
|
225
|
-
*
|
|
226
|
-
* fallback that lets `session.started` / `turn.completed` populate
|
|
227
|
-
* model
|
|
240
|
+
* Resolve the agent's runtime identity from a CC-style JSONL transcript by
|
|
241
|
+
* reading the most-recent assistant row. Claude Code's SessionStart payload
|
|
242
|
+
* omits `model` and `effort` (Codex + Cursor supply model directly), so this
|
|
243
|
+
* is the fallback that lets `session.started` / `turn.completed` populate
|
|
244
|
+
* model and tuning once the transcript has at least one assistant turn.
|
|
245
|
+
*
|
|
246
|
+
* Model, effort, and speed are read from the SAME row, never independently:
|
|
247
|
+
* effort varies with model inside one transcript (a mid-session model swap
|
|
248
|
+
* changes both), so pairing the newest model with a separately scanned newest
|
|
249
|
+
* effort can mislabel the session.
|
|
228
250
|
*
|
|
229
251
|
* Tail-reads the same 256KB window as the status-box scan and walks lines from
|
|
230
|
-
* the end, returning the first real model id
|
|
231
|
-
* (`<synthetic>`) and empty values are skipped. Returns undefined
|
|
232
|
-
* transcript is missing/empty (e.g. a fresh session's first
|
|
252
|
+
* the end, returning the first row with a real model id. Synthetic
|
|
253
|
+
* placeholders (`<synthetic>`) and empty values are skipped. Returns undefined
|
|
254
|
+
* when the transcript is missing/empty (e.g. a fresh session's first
|
|
255
|
+
* SessionStart).
|
|
233
256
|
*/
|
|
234
|
-
export function
|
|
257
|
+
export function scanTranscriptRuntime(
|
|
258
|
+
transcriptPath: string | undefined,
|
|
259
|
+
): TranscriptRuntimeScan | undefined {
|
|
235
260
|
const readablePath = resolveTranscriptPath(transcriptPath);
|
|
236
261
|
if (!readablePath) return undefined;
|
|
237
262
|
const text = tailText(readablePath);
|
|
@@ -242,12 +267,19 @@ export function scanTranscriptModel(transcriptPath: string | undefined): string
|
|
|
242
267
|
if (!line?.includes('"model"')) continue;
|
|
243
268
|
try {
|
|
244
269
|
const obj = JSON.parse(line) as {
|
|
245
|
-
|
|
270
|
+
effort?: unknown;
|
|
271
|
+
message?: { model?: unknown; usage?: { speed?: unknown } };
|
|
246
272
|
model?: unknown;
|
|
247
273
|
};
|
|
248
274
|
const model = obj.message?.model ?? obj.model;
|
|
249
275
|
if (typeof model === "string" && model.length > 0 && !model.startsWith("<")) {
|
|
250
|
-
|
|
276
|
+
const effort = obj.effort;
|
|
277
|
+
const speed = obj.message?.usage?.speed;
|
|
278
|
+
return {
|
|
279
|
+
model,
|
|
280
|
+
...(typeof effort === "string" && effort.length > 0 ? { effort } : {}),
|
|
281
|
+
...(typeof speed === "string" && speed.length > 0 ? { speed } : {}),
|
|
282
|
+
};
|
|
251
283
|
}
|
|
252
284
|
} catch {
|
|
253
285
|
// Partial/truncated first line of the tail window; skip it.
|