harnery 0.34.1 → 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.
Files changed (212) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +2 -0
  3. package/dist/commands/agents.d.ts +15 -4
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +89 -175
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +26 -2
  8. package/dist/commands/decision.d.ts.map +1 -1
  9. package/dist/commands/decision.js +2 -2
  10. package/dist/commands/events.d.ts.map +1 -1
  11. package/dist/commands/events.js +18 -1
  12. package/dist/commands/presence.d.ts.map +1 -1
  13. package/dist/commands/presence.js +2 -2
  14. package/dist/commands/semantic.d.ts +4 -0
  15. package/dist/commands/semantic.d.ts.map +1 -0
  16. package/dist/commands/semantic.js +147 -0
  17. package/dist/commands/web.d.ts.map +1 -1
  18. package/dist/commands/web.js +17 -0
  19. package/dist/core/agents/canonical-emit.d.ts +3 -0
  20. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  21. package/dist/core/agents/canonical-emit.js +12 -0
  22. package/dist/core/agents/claim-path.d.ts +11 -0
  23. package/dist/core/agents/claim-path.d.ts.map +1 -0
  24. package/dist/core/agents/claim-path.js +22 -0
  25. package/dist/core/agents/coord-client.d.ts +29 -3
  26. package/dist/core/agents/coord-client.d.ts.map +1 -1
  27. package/dist/core/agents/coord-client.js +88 -9
  28. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  29. package/dist/core/agents/live-authority-v3.js +1 -3
  30. package/dist/core/agents/live-lifecycle-v3.js +3 -3
  31. package/dist/core/agents/live-observation-v3.d.ts.map +1 -1
  32. package/dist/core/agents/live-observation-v3.js +2 -2
  33. package/dist/core/agents/render/prompt-context.js +12 -10
  34. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  35. package/dist/core/agents/rules/stop-hook.js +23 -11
  36. package/dist/core/agents/session-name-display.d.ts +1 -0
  37. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  38. package/dist/core/agents/session-name-display.js +8 -1
  39. package/dist/core/agents/state/heartbeat-reader.d.ts +46 -0
  40. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -0
  41. package/dist/core/agents/state/heartbeat-reader.js +25 -0
  42. package/dist/core/agents/state/heartbeat-writer.d.ts +3 -48
  43. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  44. package/dist/core/agents/state/heartbeat-writer.js +16 -34
  45. package/dist/core/agents/state/live-coordination-view.d.ts +1 -1
  46. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  47. package/dist/core/agents/state/live-coordination-view.js +25 -1
  48. package/dist/core/agents/state/names.d.ts +9 -1
  49. package/dist/core/agents/state/names.d.ts.map +1 -1
  50. package/dist/core/agents/state/names.js +190 -182
  51. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  52. package/dist/core/agents/state/stale-sweep.js +23 -5
  53. package/dist/core/artifacts/constants.d.ts +5 -0
  54. package/dist/core/artifacts/constants.d.ts.map +1 -0
  55. package/dist/core/artifacts/constants.js +4 -0
  56. package/dist/core/artifacts/index.d.ts +3 -3
  57. package/dist/core/artifacts/index.d.ts.map +1 -1
  58. package/dist/core/artifacts/index.js +50 -7
  59. package/dist/core/events/v3/base-contract.d.ts +795 -0
  60. package/dist/core/events/v3/base-contract.d.ts.map +1 -1
  61. package/dist/core/events/v3/base-contract.js +33 -0
  62. package/dist/core/events/v3/capabilities.d.ts +11 -0
  63. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  64. package/dist/core/events/v3/capabilities.js +23 -0
  65. package/dist/core/events/v3/generated.d.ts +1 -1
  66. package/dist/core/events/v3/generated.js +1 -1
  67. package/dist/core/events/v3/index.d.ts +4 -3
  68. package/dist/core/events/v3/index.d.ts.map +1 -1
  69. package/dist/core/events/v3/index.js +4 -3
  70. package/dist/core/events/v3/latency.d.ts +31 -1
  71. package/dist/core/events/v3/latency.d.ts.map +1 -1
  72. package/dist/core/events/v3/latency.js +127 -3
  73. package/dist/core/events/v3/live-routing.d.ts +30 -0
  74. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  75. package/dist/core/events/v3/live-routing.js +109 -1
  76. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  77. package/dist/core/events/v3/producers/command-recorder.js +10 -1
  78. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  79. package/dist/core/events/v3/producers/coordination-recorder.js +14 -3
  80. package/dist/core/events/v3/producers/hook-base.d.ts +17 -1
  81. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  82. package/dist/core/events/v3/producers/hook-base.js +82 -1
  83. package/dist/core/events/v3/producers/intake.d.ts +6 -0
  84. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  85. package/dist/core/events/v3/producers/intake.js +11 -0
  86. package/dist/core/events/v3/producers/recorder.d.ts +62 -0
  87. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  88. package/dist/core/events/v3/producers/recorder.js +654 -18
  89. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +51 -0
  90. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -0
  91. package/dist/core/events/v3/runtime-telemetry-capabilities.js +54 -0
  92. package/dist/core/events/v3/turn-telemetry.d.ts +9 -0
  93. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
  94. package/dist/core/hooks/adapter/parse.d.ts +3 -0
  95. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  96. package/dist/core/hooks/adapter/parse.js +14 -0
  97. package/dist/core/hooks/adapter/runtime-telemetry.d.ts +119 -0
  98. package/dist/core/hooks/adapter/runtime-telemetry.d.ts.map +1 -0
  99. package/dist/core/hooks/adapter/runtime-telemetry.js +568 -0
  100. package/dist/core/hooks/cli.js +111 -36
  101. package/dist/core/hooks/remediation-cap.d.ts +34 -0
  102. package/dist/core/hooks/remediation-cap.d.ts.map +1 -0
  103. package/dist/core/hooks/remediation-cap.js +71 -0
  104. package/dist/core/hooks/resolve/transcript.d.ts +25 -9
  105. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  106. package/dist/core/hooks/resolve/transcript.js +22 -10
  107. package/dist/core/semantic/adapters.d.ts +34 -0
  108. package/dist/core/semantic/adapters.d.ts.map +1 -0
  109. package/dist/core/semantic/adapters.js +222 -0
  110. package/dist/core/semantic/contract.d.ts +588 -0
  111. package/dist/core/semantic/contract.d.ts.map +1 -0
  112. package/dist/core/semantic/contract.js +230 -0
  113. package/dist/core/semantic/evidence.d.ts +11 -0
  114. package/dist/core/semantic/evidence.d.ts.map +1 -0
  115. package/dist/core/semantic/evidence.js +262 -0
  116. package/dist/core/semantic/index.d.ts +10 -0
  117. package/dist/core/semantic/index.d.ts.map +1 -0
  118. package/dist/core/semantic/index.js +9 -0
  119. package/dist/core/semantic/once.d.ts +33 -0
  120. package/dist/core/semantic/once.d.ts.map +1 -0
  121. package/dist/core/semantic/once.js +293 -0
  122. package/dist/core/semantic/prompt.d.ts +12 -0
  123. package/dist/core/semantic/prompt.d.ts.map +1 -0
  124. package/dist/core/semantic/prompt.js +70 -0
  125. package/dist/core/semantic/scheduler.d.ts +16 -0
  126. package/dist/core/semantic/scheduler.d.ts.map +1 -0
  127. package/dist/core/semantic/scheduler.js +67 -0
  128. package/dist/core/semantic/service-status.d.ts +31 -0
  129. package/dist/core/semantic/service-status.d.ts.map +1 -0
  130. package/dist/core/semantic/service-status.js +84 -0
  131. package/dist/core/semantic/service.d.ts +51 -0
  132. package/dist/core/semantic/service.d.ts.map +1 -0
  133. package/dist/core/semantic/service.js +470 -0
  134. package/dist/core/semantic/storage.d.ts +71 -0
  135. package/dist/core/semantic/storage.d.ts.map +1 -0
  136. package/dist/core/semantic/storage.js +179 -0
  137. package/dist/core/semantic/validate.d.ts +16 -0
  138. package/dist/core/semantic/validate.d.ts.map +1 -0
  139. package/dist/core/semantic/validate.js +174 -0
  140. package/dist/lib/browser/client.d.ts +11 -0
  141. package/dist/lib/browser/client.d.ts.map +1 -1
  142. package/dist/lib/browser/client.js +13 -0
  143. package/dist/lib/browser/index.d.ts +1 -0
  144. package/dist/lib/browser/index.d.ts.map +1 -1
  145. package/dist/lib/browser/standalone-html.d.ts +25 -0
  146. package/dist/lib/browser/standalone-html.d.ts.map +1 -0
  147. package/dist/lib/browser/standalone-html.js +284 -0
  148. package/dist/lib/instructions/apply.d.ts.map +1 -1
  149. package/dist/lib/instructions/apply.js +1 -0
  150. package/dist/lib/instructions/templates.d.ts +2 -0
  151. package/dist/lib/instructions/templates.d.ts.map +1 -1
  152. package/dist/lib/instructions/templates.js +144 -4
  153. package/package.json +6 -1
  154. package/schemas/event-v3.schema.json +2758 -478
  155. package/src/commander.ts +2 -0
  156. package/src/commands/agents.ts +102 -167
  157. package/src/commands/browse.ts +25 -2
  158. package/src/commands/decision.ts +8 -2
  159. package/src/commands/events.ts +21 -1
  160. package/src/commands/presence.ts +7 -2
  161. package/src/commands/semantic.ts +179 -0
  162. package/src/commands/web.ts +15 -0
  163. package/src/core/agents/canonical-emit.ts +15 -0
  164. package/src/core/agents/claim-path.ts +23 -0
  165. package/src/core/agents/coord-client.ts +92 -8
  166. package/src/core/agents/live-authority-v3.ts +1 -4
  167. package/src/core/agents/live-lifecycle-v3.ts +8 -3
  168. package/src/core/agents/live-observation-v3.ts +7 -2
  169. package/src/core/agents/render/prompt-context.ts +13 -10
  170. package/src/core/agents/rules/stop-hook.ts +24 -11
  171. package/src/core/agents/session-name-display.ts +9 -1
  172. package/src/core/agents/state/heartbeat-reader.ts +69 -0
  173. package/src/core/agents/state/heartbeat-writer.ts +20 -84
  174. package/src/core/agents/state/live-coordination-view.ts +26 -1
  175. package/src/core/agents/state/names.ts +190 -182
  176. package/src/core/agents/state/stale-sweep.ts +25 -5
  177. package/src/core/artifacts/constants.ts +4 -0
  178. package/src/core/artifacts/index.ts +49 -7
  179. package/src/core/events/v3/base-contract.ts +39 -0
  180. package/src/core/events/v3/capabilities.ts +34 -0
  181. package/src/core/events/v3/generated.ts +1 -1
  182. package/src/core/events/v3/index.ts +21 -0
  183. package/src/core/events/v3/latency.ts +213 -2
  184. package/src/core/events/v3/live-routing.ts +120 -1
  185. package/src/core/events/v3/producers/command-recorder.ts +10 -1
  186. package/src/core/events/v3/producers/coordination-recorder.ts +17 -5
  187. package/src/core/events/v3/producers/hook-base.ts +115 -1
  188. package/src/core/events/v3/producers/intake.ts +18 -0
  189. package/src/core/events/v3/producers/recorder.ts +856 -21
  190. package/src/core/events/v3/runtime-telemetry-capabilities.ts +137 -0
  191. package/src/core/events/v3/turn-telemetry.ts +10 -0
  192. package/src/core/hooks/adapter/parse.ts +16 -0
  193. package/src/core/hooks/adapter/runtime-telemetry.ts +805 -0
  194. package/src/core/hooks/cli.ts +123 -39
  195. package/src/core/hooks/remediation-cap.ts +77 -0
  196. package/src/core/hooks/resolve/transcript.ts +37 -11
  197. package/src/core/semantic/adapters.ts +293 -0
  198. package/src/core/semantic/contract.ts +298 -0
  199. package/src/core/semantic/evidence.ts +336 -0
  200. package/src/core/semantic/index.ts +127 -0
  201. package/src/core/semantic/once.ts +410 -0
  202. package/src/core/semantic/prompt.ts +84 -0
  203. package/src/core/semantic/scheduler.ts +107 -0
  204. package/src/core/semantic/service-status.ts +118 -0
  205. package/src/core/semantic/service.ts +580 -0
  206. package/src/core/semantic/storage.ts +286 -0
  207. package/src/core/semantic/validate.ts +212 -0
  208. package/src/lib/browser/client.ts +21 -0
  209. package/src/lib/browser/index.ts +4 -0
  210. package/src/lib/browser/standalone-html.ts +312 -0
  211. package/src/lib/instructions/apply.ts +1 -0
  212. package/src/lib/instructions/templates.ts +149 -4
@@ -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 { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
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
- scanTranscriptModel,
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 ?? scanTranscriptModel(p?.transcript_path),
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 ?? scanTranscriptModel(p?.transcript_path),
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
- // Shadow measurement for the rule-2/3 detector: the loose scan above
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). Telemetry-only: the Stop verdict still reads
403
- // status_box_present. Once the divergence rate between the two fields
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
- p?.transcript_path,
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 assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "subagent");
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: ctx.instanceId,
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 { session_id?: string; conversation_id?: string };
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
- playSound(repoRoot, s.sound, sid, s.maxPlays);
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
- ...(coordEnv("EXPERIMENT_DEFER_V3_DRAIN") === "1" ? { defer_drain: true } : {}),
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: sync-project to create the subagent heartbeat,
893
- // log the lifecycle event, and emit a context message announcing the
894
- // subagent (claude-code + cursor; codex doesn't fan out subagents today).
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=${owner.instance_id.slice(0, 8)} platform=${adapterPlatform(adapter)}`,
965
+ `SUBAGENT_START agent_type=${(data.agent_type as string) ?? "unknown"} agent_id=${childInstanceId.slice(0, 8)} platform=${adapterPlatform(adapter)}`,
909
966
  "--instance",
910
- owner.instance_id,
967
+ childInstanceId,
911
968
  ],
912
969
  { encoding: "utf8", timeout: 2000, env: childEnv(coordRoot) },
913
970
  );
914
971
  }
915
- emitSubagentStartContext(coordRoot, owner.instance_id, sessionId, data, adapter);
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
- cleanupSessionEnd(coordRoot, owner.instance_id, (data.reason as string) ?? "unknown");
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=${owner.instance_id.slice(0, 8)} platform=${adapterPlatform(adapter)}`,
989
+ `SUBAGENT_STOP agent_id=${childInstanceId.slice(0, 8)} platform=${adapterPlatform(adapter)}`,
932
990
  "--instance",
933
- owner.instance_id,
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
- // Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
1017
- // as a turn block; Cursor ignores exit codes and re-prompts only via a
1018
- // `followup_message` it auto-submits. Codex returned observe-only above.
1019
- // emitStopBlock writes the right shape and returns the exit code to use.
1020
- const { emitStopBlock } = await import("./adapter/output.ts");
1021
- return emitStopBlock(adapter, verdict, coordRoot);
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
@@ -1160,14 +1238,21 @@ async function enforcePendingSessionNameDisplay(
1160
1238
  if (isSessionNameRemediationCommand(command, resolveBinName(coordRoot))) return true;
1161
1239
 
1162
1240
  const inspection =
1163
- payload?.agent_message !== undefined
1241
+ adapter === "cursor" && payload?.agent_message !== undefined
1164
1242
  ? {
1165
1243
  state: assistantTextStartsWithSessionNameBlock(payload.agent_message, name)
1166
1244
  ? ("present" as const)
1167
1245
  : ("absent" as const),
1168
1246
  }
1169
1247
  : inspectSessionNameDisplayImmediately(
1170
- payload?.transcript_path,
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),
1171
1256
  name,
1172
1257
  assistantTextStartsWithSessionNameBlock,
1173
1258
  );
@@ -1177,17 +1262,16 @@ async function enforcePendingSessionNameDisplay(
1177
1262
  }
1178
1263
 
1179
1264
  if (inspection.state === "unavailable") {
1180
- const { emitContext } = await import("./adapter/output.ts");
1181
- emitContext(
1182
- adapter,
1183
- "PreToolUse",
1184
- "Harnery could not verify the pending session-name display because the adapter transcript is unavailable or not flushed yet. This tool is allowed so the session cannot deadlock; display the requested block before later work. Verification remains pending.",
1185
- );
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.
1186
1270
  return true;
1187
1271
  }
1188
1272
 
1189
1273
  const { emitDeny } = await import("./adapter/output.ts");
1190
- emitDeny(adapter, sessionNameDisplayInstruction(name));
1274
+ emitDeny(adapter, sessionNameDisplayRecoveryInstruction(name, resolveBinName(coordRoot)));
1191
1275
  return false;
1192
1276
  }
1193
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
+ }
@@ -225,19 +225,38 @@ function textFromContent(content: unknown): string {
225
225
  return parts.join("\n");
226
226
  }
227
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
+
228
239
  /**
229
- * Resolve the agent's model from a CC-style JSONL transcript by reading the
230
- * most-recent assistant message's `message.model`. Claude Code's SessionStart
231
- * payload omits `model` (Codex + Cursor supply it directly), so this is the
232
- * fallback that lets `session.started` / `turn.completed` populate the cache's
233
- * model field once the transcript has at least one assistant turn.
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.
234
250
  *
235
251
  * Tail-reads the same 256KB window as the status-box scan and walks lines from
236
- * the end, returning the first real model id found. Synthetic placeholders
237
- * (`<synthetic>`) and empty values are skipped. Returns undefined when the
238
- * transcript is missing/empty (e.g. a fresh session's first SessionStart).
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).
239
256
  */
240
- export function scanTranscriptModel(transcriptPath: string | undefined): string | undefined {
257
+ export function scanTranscriptRuntime(
258
+ transcriptPath: string | undefined,
259
+ ): TranscriptRuntimeScan | undefined {
241
260
  const readablePath = resolveTranscriptPath(transcriptPath);
242
261
  if (!readablePath) return undefined;
243
262
  const text = tailText(readablePath);
@@ -248,12 +267,19 @@ export function scanTranscriptModel(transcriptPath: string | undefined): string
248
267
  if (!line?.includes('"model"')) continue;
249
268
  try {
250
269
  const obj = JSON.parse(line) as {
251
- message?: { model?: unknown };
270
+ effort?: unknown;
271
+ message?: { model?: unknown; usage?: { speed?: unknown } };
252
272
  model?: unknown;
253
273
  };
254
274
  const model = obj.message?.model ?? obj.model;
255
275
  if (typeof model === "string" && model.length > 0 && !model.startsWith("<")) {
256
- return model;
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
+ };
257
283
  }
258
284
  } catch {
259
285
  // Partial/truncated first line of the tail window; skip it.