harnery 0.37.0 → 0.38.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 (184) hide show
  1. package/dist/commander.d.ts +9 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +7 -2
  4. package/dist/commands/admission.d.ts +21 -0
  5. package/dist/commands/admission.d.ts.map +1 -0
  6. package/dist/commands/admission.js +565 -0
  7. package/dist/commands/agents.d.ts +7 -0
  8. package/dist/commands/agents.d.ts.map +1 -1
  9. package/dist/commands/agents.js +61 -1
  10. package/dist/commands/artifacts.d.ts.map +1 -1
  11. package/dist/commands/artifacts.js +48 -2
  12. package/dist/commands/browse-ai.d.ts +2 -2
  13. package/dist/commands/browse-ai.d.ts.map +1 -1
  14. package/dist/commands/browse-ai.js +6 -4
  15. package/dist/commands/browse.d.ts.map +1 -1
  16. package/dist/commands/browse.js +349 -21
  17. package/dist/commands/fetch.js +1 -0
  18. package/dist/commands/qa-record.d.ts +144 -0
  19. package/dist/commands/qa-record.d.ts.map +1 -0
  20. package/dist/commands/qa-record.js +0 -0
  21. package/dist/commands/qa-run.d.ts +7 -3
  22. package/dist/commands/qa-run.d.ts.map +1 -1
  23. package/dist/commands/qa-run.js +268 -13
  24. package/dist/commands/qa-status.d.ts +71 -0
  25. package/dist/commands/qa-status.d.ts.map +1 -0
  26. package/dist/commands/qa-status.js +490 -0
  27. package/dist/commands/qa-verify.d.ts +40 -0
  28. package/dist/commands/qa-verify.d.ts.map +1 -0
  29. package/dist/commands/qa-verify.js +180 -0
  30. package/dist/commands/review-pack.d.ts +4 -0
  31. package/dist/commands/review-pack.d.ts.map +1 -0
  32. package/dist/commands/review-pack.js +1001 -0
  33. package/dist/core/agents/qa-signal.d.ts +111 -0
  34. package/dist/core/agents/qa-signal.d.ts.map +1 -0
  35. package/dist/core/agents/qa-signal.js +231 -0
  36. package/dist/core/agents/session-name-display.d.ts +20 -5
  37. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  38. package/dist/core/agents/session-name-display.js +67 -7
  39. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  40. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  41. package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
  42. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  43. package/dist/core/agents/state/heartbeat-writer.js +17 -0
  44. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  45. package/dist/core/agents/state/live-coordination-view.js +1 -0
  46. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  47. package/dist/core/artifacts/constants.d.ts +1 -1
  48. package/dist/core/artifacts/constants.js +1 -1
  49. package/dist/core/artifacts/index.d.ts +57 -6
  50. package/dist/core/artifacts/index.d.ts.map +1 -1
  51. package/dist/core/artifacts/index.js +265 -10
  52. package/dist/core/config.d.ts +8 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +16 -0
  55. package/dist/core/diagnostics/bundle.d.ts +16 -0
  56. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  57. package/dist/core/diagnostics/bundle.js +101 -7
  58. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  59. package/dist/core/events/v3/bootstrap.js +10 -0
  60. package/dist/core/events/v3/coordination-view.d.ts +3 -0
  61. package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
  62. package/dist/core/events/v3/coordination-view.js +69 -8
  63. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  64. package/dist/core/events/v3/producers/intake.js +9 -2
  65. package/dist/core/events/v3/producers/recorder.d.ts +23 -0
  66. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  67. package/dist/core/events/v3/producers/recorder.js +264 -18
  68. package/dist/core/hooks/cli.js +120 -27
  69. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  70. package/dist/core/hooks/resolve/transcript.js +10 -3
  71. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  72. package/dist/core/hooks/session-name-presence.js +4 -1
  73. package/dist/core/qa-artifacts.d.ts +20 -0
  74. package/dist/core/qa-artifacts.d.ts.map +1 -0
  75. package/dist/core/qa-artifacts.js +110 -0
  76. package/dist/core/resources/contract.d.ts +6 -0
  77. package/dist/core/resources/contract.d.ts.map +1 -1
  78. package/dist/core/resources/sampler.d.ts +7 -0
  79. package/dist/core/resources/sampler.d.ts.map +1 -1
  80. package/dist/core/resources/sampler.js +106 -5
  81. package/dist/lib/admission.d.ts +71 -0
  82. package/dist/lib/admission.d.ts.map +1 -0
  83. package/dist/lib/admission.js +264 -0
  84. package/dist/lib/agent-browser/client.d.ts +1 -1
  85. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  86. package/dist/lib/agent-browser/client.js +1 -5
  87. package/dist/lib/browser/capture-fidelity.d.ts +39 -0
  88. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
  89. package/dist/lib/browser/capture-fidelity.js +84 -0
  90. package/dist/lib/browser/client.d.ts +41 -1
  91. package/dist/lib/browser/client.d.ts.map +1 -1
  92. package/dist/lib/browser/client.js +167 -9
  93. package/dist/lib/browser/critique.d.ts +38 -1
  94. package/dist/lib/browser/critique.d.ts.map +1 -1
  95. package/dist/lib/browser/critique.js +34 -6
  96. package/dist/lib/browser/index.d.ts +4 -2
  97. package/dist/lib/browser/index.d.ts.map +1 -1
  98. package/dist/lib/browser/index.js +2 -0
  99. package/dist/lib/browser/page-review-judge.d.ts +64 -0
  100. package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
  101. package/dist/lib/browser/page-review-judge.js +270 -0
  102. package/dist/lib/browser/page-review-pack.d.ts +613 -0
  103. package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
  104. package/dist/lib/browser/page-review-pack.js +1751 -0
  105. package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
  106. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  107. package/dist/lib/browser/qa-run-contracts.js +136 -1
  108. package/dist/lib/browser/qa-run.d.ts +100 -10
  109. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  110. package/dist/lib/browser/qa-run.js +768 -169
  111. package/dist/lib/browser/request-diagnostics.d.ts +13 -0
  112. package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
  113. package/dist/lib/browser/request-diagnostics.js +18 -0
  114. package/dist/lib/browser/tiling.d.ts +19 -0
  115. package/dist/lib/browser/tiling.d.ts.map +1 -1
  116. package/dist/lib/browser/tiling.js +28 -0
  117. package/dist/lib/cookies/client.d.ts +9 -0
  118. package/dist/lib/cookies/client.d.ts.map +1 -1
  119. package/dist/lib/cookies/client.js +197 -44
  120. package/dist/lib/cookies/extra.d.ts +18 -0
  121. package/dist/lib/cookies/extra.d.ts.map +1 -0
  122. package/dist/lib/cookies/extra.js +14 -0
  123. package/dist/lib/cookies/index.d.ts +2 -1
  124. package/dist/lib/cookies/index.d.ts.map +1 -1
  125. package/dist/lib/cookies/index.js +2 -1
  126. package/dist/lib/durable-job.d.ts +124 -0
  127. package/dist/lib/durable-job.d.ts.map +1 -0
  128. package/dist/lib/durable-job.js +296 -0
  129. package/dist/lib/http/client.d.ts +7 -1
  130. package/dist/lib/http/client.d.ts.map +1 -1
  131. package/dist/lib/http/client.js +2 -0
  132. package/dist/lib/instructions/templates.d.ts.map +1 -1
  133. package/dist/lib/instructions/templates.js +5 -2
  134. package/package.json +8 -2
  135. package/src/commander.ts +50 -2
  136. package/src/commands/admission.ts +699 -0
  137. package/src/commands/agents.ts +87 -1
  138. package/src/commands/artifacts.ts +97 -21
  139. package/src/commands/browse-ai.ts +10 -5
  140. package/src/commands/browse.ts +481 -20
  141. package/src/commands/fetch.ts +1 -0
  142. package/src/commands/qa-record.ts +682 -0
  143. package/src/commands/qa-run.ts +335 -16
  144. package/src/commands/qa-status.ts +608 -0
  145. package/src/commands/qa-verify.ts +238 -0
  146. package/src/commands/review-pack.ts +1281 -0
  147. package/src/core/agents/qa-signal.ts +261 -0
  148. package/src/core/agents/session-name-display.ts +78 -7
  149. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  150. package/src/core/agents/state/heartbeat-writer.ts +23 -0
  151. package/src/core/agents/state/live-coordination-view.ts +1 -0
  152. package/src/core/agents/state/live-coordination-writer.ts +5 -0
  153. package/src/core/artifacts/constants.ts +1 -1
  154. package/src/core/artifacts/index.ts +370 -21
  155. package/src/core/config.ts +23 -0
  156. package/src/core/diagnostics/bundle.ts +119 -11
  157. package/src/core/events/v3/bootstrap.ts +10 -0
  158. package/src/core/events/v3/coordination-view.ts +100 -11
  159. package/src/core/events/v3/producers/intake.ts +9 -2
  160. package/src/core/events/v3/producers/recorder.ts +312 -18
  161. package/src/core/hooks/cli.ts +140 -32
  162. package/src/core/hooks/resolve/transcript.ts +10 -3
  163. package/src/core/hooks/session-name-presence.ts +6 -1
  164. package/src/core/qa-artifacts.ts +126 -0
  165. package/src/core/resources/contract.ts +7 -0
  166. package/src/core/resources/sampler.ts +138 -6
  167. package/src/lib/admission.ts +347 -0
  168. package/src/lib/agent-browser/client.ts +2 -10
  169. package/src/lib/browser/capture-fidelity.ts +98 -0
  170. package/src/lib/browser/client.ts +206 -10
  171. package/src/lib/browser/critique.ts +62 -7
  172. package/src/lib/browser/index.ts +36 -0
  173. package/src/lib/browser/page-review-judge.ts +360 -0
  174. package/src/lib/browser/page-review-pack.ts +2384 -0
  175. package/src/lib/browser/qa-run-contracts.ts +366 -3
  176. package/src/lib/browser/qa-run.ts +868 -190
  177. package/src/lib/browser/request-diagnostics.ts +27 -0
  178. package/src/lib/browser/tiling.ts +32 -0
  179. package/src/lib/cookies/client.ts +228 -42
  180. package/src/lib/cookies/extra.ts +28 -0
  181. package/src/lib/cookies/index.ts +2 -0
  182. package/src/lib/durable-job.ts +407 -0
  183. package/src/lib/http/client.ts +13 -1
  184. package/src/lib/instructions/templates.ts +5 -2
@@ -695,7 +695,7 @@ function processHookSignalLocked(
695
695
  const durability = writeEventV3(input.coordRoot, pendingEvent, input.writerOptions);
696
696
  applyCommittedEvent(state, pendingEvent);
697
697
  state.pending = undefined;
698
- publishProducerState(path, state);
698
+ publishProducerState(input.coordRoot, path, state);
699
699
  if (pendingSource && incomingSource === pendingSource) {
700
700
  recovered = { state: "recorded", event: pendingEvent, durability, recovered: true };
701
701
  }
@@ -754,7 +754,16 @@ function processHookSignalLocked(
754
754
  );
755
755
  const cursorPromptBootstrap = isCursorPromptBootstrap(input);
756
756
  const onboarding = buildMidFlightSessionStart(input, state, rootId, cursorPromptBootstrap);
757
- commitEventLocked(input, state, path, onboarding);
757
+ // Onboarding commits eagerly even when the triggering hook defers its
758
+ // drain. The coordination view reads committed history only, so a
759
+ // spooled session.started leaves the session invisible to every
760
+ // authority command (set-task, status --end-turn, heal) until some other
761
+ // writer happens to drain the spool.
762
+ const onboardingInput = withEagerDrain(input);
763
+ commitEventLocked(onboardingInput, state, path, onboarding);
764
+ if (midFlightSignalImpliesOpenTurn(input.signal)) {
765
+ commitMidFlightTurnStart(onboardingInput, state, path, rootId);
766
+ }
758
767
  if (!cursorPromptBootstrap) {
759
768
  writeProducerDiagnosticV3(input.coordRoot, "mid_flight_onboarding", {
760
769
  adapter: input.adapter,
@@ -881,7 +890,7 @@ function processHookSignalLocked(
881
890
  span_id: state.current_turn_span?.span_id,
882
891
  payload: input.payload,
883
892
  });
884
- publishProducerState(path, state);
893
+ publishProducerState(input.coordRoot, path, state);
885
894
  return { state: "ignored" };
886
895
  }
887
896
 
@@ -907,7 +916,7 @@ function processHookSignalLocked(
907
916
  ? "no_open_turn_span"
908
917
  : "ritual_observation_missing",
909
918
  });
910
- publishProducerState(path, state);
919
+ publishProducerState(input.coordRoot, path, state);
911
920
  return { state: "ignored" };
912
921
  }
913
922
  const observation: CursorResponseRitualV3 = {
@@ -918,7 +927,7 @@ function processHookSignalLocked(
918
927
  status_box_present_strict: input.turn_ritual.status_box_present_strict,
919
928
  };
920
929
  state.cursor_response_ritual = observation;
921
- publishProducerState(path, state);
930
+ publishProducerState(input.coordRoot, path, state);
922
931
  writeProducerDiagnosticV3(input.coordRoot, "cursor_response_ritual_observed", {
923
932
  instance_id: state.instance_id,
924
933
  generation_id: state.generation_id,
@@ -1805,6 +1814,114 @@ function commitRecoveredToolTurnStart(
1805
1814
  commitEventLocked(input, state, path, event);
1806
1815
  }
1807
1816
 
1817
+ /**
1818
+ * Signals an adapter can only deliver while a turn is executing. A session
1819
+ * onboarded mid-flight by one of these is provably inside a turn, so the
1820
+ * generation opens with one instead of waiting for the next native prompt.
1821
+ * The native prompt path opens its own turn, session boundaries carry no
1822
+ * turn, and compaction can run between turns.
1823
+ */
1824
+ function midFlightSignalImpliesOpenTurn(signal: HookSignalV3): boolean {
1825
+ switch (signal) {
1826
+ case "session-start":
1827
+ case "session-end":
1828
+ case "user-prompt-submit":
1829
+ case "pre-compact":
1830
+ case "post-compact":
1831
+ return false;
1832
+ default:
1833
+ return true;
1834
+ }
1835
+ }
1836
+
1837
+ /** Commit through the append path even when the hook asked to defer its drain. */
1838
+ function withEagerDrain(input: RecordHookSignalV3Input): RecordHookSignalV3Input {
1839
+ if (!input.writerOptions?.deferDrain) return input;
1840
+ return { ...input, writerOptions: { ...input.writerOptions, deferDrain: false } };
1841
+ }
1842
+
1843
+ /**
1844
+ * Open a derived turn for a session onboarded mid-flight (ADR 0078) by a
1845
+ * signal that only an executing turn can deliver. Without it the fresh
1846
+ * generation has no current turn: command telemetry refuses to join
1847
+ * (`turn_not_started`), tool spans nest under the session span, and after an
1848
+ * epoch rotation (ADR 0137) the live session cannot satisfy its end-of-turn
1849
+ * gate until a human submits the next prompt. The payload's native turn id
1850
+ * is kept when the adapter supplies one, so the turn already running keeps
1851
+ * its identity across the boundary and later signals join it directly.
1852
+ */
1853
+ function commitMidFlightTurnStart(
1854
+ input: RecordHookSignalV3Input,
1855
+ state: HookProducerStateV3,
1856
+ path: string,
1857
+ rootId: `root_${string}`,
1858
+ recovery?: {
1859
+ source_event: string;
1860
+ confidence: "high" | "medium" | "low";
1861
+ attribution_method: "native_payload" | "session_env";
1862
+ },
1863
+ ): void {
1864
+ const fingerprintContext = fingerprintContextV3(
1865
+ input.coordRoot,
1866
+ rootId,
1867
+ state.generation_id,
1868
+ state.privacy_epoch_id,
1869
+ );
1870
+ const clock = signalClock(input);
1871
+ const span = openSpanStateV3({
1872
+ span_id: spanIdV3(),
1873
+ parent_span_id: state.session_span.span_id,
1874
+ boot_id: state.boot_id,
1875
+ clock,
1876
+ });
1877
+ state.current_turn_span = span;
1878
+ const nativeTurnId = input.payload.turn_id;
1879
+ state.current_native_turn_id = nativeTurnId;
1880
+ const event = normalizeHookEventV3("user-prompt-submit", recoveryBoundaryPayload(input.payload), {
1881
+ coordRoot: input.coordRoot,
1882
+ adapter: input.adapter,
1883
+ adapterVersion: input.adapterVersion,
1884
+ harnessVersion: input.harnessVersion,
1885
+ root_id: rootId,
1886
+ run_id: input.run_id,
1887
+ workflow_id: input.workflow_id,
1888
+ workflow_agent_id: input.workflow_agent_id,
1889
+ instance_id: state.instance_id,
1890
+ generation_id: state.generation_id,
1891
+ attestation_id: state.attestation_id,
1892
+ producer_id: input.producer_id,
1893
+ boot_id: state.boot_id,
1894
+ sequence: state.next_sequence,
1895
+ build_id: input.build_id,
1896
+ platform: input.platform,
1897
+ bridge: input.bridge,
1898
+ capability_profile: state.capability_profile,
1899
+ fingerprintContext,
1900
+ turn_native_id: nativeTurnId ?? `mid-flight:${state.generation_id}:${state.turn_ordinal + 1}`,
1901
+ span_id: span.span_id,
1902
+ caused_by: state.last_event_id ? [state.last_event_id] : [],
1903
+ observed_at: clock.observed_at,
1904
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1905
+ clock_id: state.clock_id,
1906
+ });
1907
+ if (event?.event_type !== "turn.started") {
1908
+ throw new Error("mid-flight turn start could not be normalized");
1909
+ }
1910
+ event.provenance = {
1911
+ ...event.provenance,
1912
+ source_event: recovery?.source_event ?? `${input.adapter}.recovery`,
1913
+ attestation: "derived",
1914
+ confidence: recovery?.confidence ?? (nativeTurnId ? "medium" : "low"),
1915
+ attribution: {
1916
+ method: recovery?.attribution_method ?? (nativeTurnId ? "native_payload" : "session_env"),
1917
+ state: "verified",
1918
+ subject_instance_id: state.instance_id,
1919
+ },
1920
+ };
1921
+ assertEventV3(event);
1922
+ commitEventLocked(input, state, path, event);
1923
+ }
1924
+
1808
1925
  /** The single pending-publish/write/apply/publish cycle every locked event commit uses. */
1809
1926
  function commitEventLocked(
1810
1927
  input: RecordHookSignalV3Input,
@@ -1816,11 +1933,11 @@ function commitEventLocked(
1816
1933
  markObservedClockRegressionV3(event, state.last_observed_at);
1817
1934
  assertEventV3(event);
1818
1935
  state.pending = { ...(sourceId ? { source_id: sourceId } : {}), event };
1819
- publishProducerState(path, state);
1936
+ publishProducerState(input.coordRoot, path, state);
1820
1937
  const durability = writeEventV3(input.coordRoot, event, input.writerOptions);
1821
1938
  applyCommittedEvent(state, event);
1822
1939
  state.pending = undefined;
1823
- publishProducerState(path, state);
1940
+ publishProducerState(input.coordRoot, path, state);
1824
1941
  return durability;
1825
1942
  }
1826
1943
 
@@ -1877,7 +1994,7 @@ function commitContextObservation(
1877
1994
  contextProvenance?.source_witness &&
1878
1995
  state.last_context_source_witness === contextProvenance.source_witness
1879
1996
  ) {
1880
- publishProducerState(path, state);
1997
+ publishProducerState(input.coordRoot, path, state);
1881
1998
  return;
1882
1999
  }
1883
2000
  if (measurement.state === "observed" && contextProvenance?.source_witness) {
@@ -2008,13 +2125,13 @@ function maybeCommitActiveRuntimeContextObservation(
2008
2125
  return;
2009
2126
  }
2010
2127
  if (input.adapter === "cursor" && state.cursor_mode === "cloud") {
2011
- publishProducerState(path, state);
2128
+ publishProducerState(input.coordRoot, path, state);
2012
2129
  return;
2013
2130
  }
2014
2131
 
2015
2132
  const transcriptPath = runtimeTranscriptPath(input, state);
2016
2133
  if (!transcriptPath && input.adapter !== "cursor") {
2017
- publishProducerState(path, state);
2134
+ publishProducerState(input.coordRoot, path, state);
2018
2135
  return;
2019
2136
  }
2020
2137
  const runtime = readRuntimeContextTelemetry(
@@ -2033,7 +2150,7 @@ function maybeCommitActiveRuntimeContextObservation(
2033
2150
  );
2034
2151
  if (runtime.bytes_read > 0) recordRuntimeTelemetryTiming(state, runtime.io_duration_ms);
2035
2152
  if (runtime.state !== "observed") {
2036
- publishProducerState(path, state);
2153
+ publishProducerState(input.coordRoot, path, state);
2037
2154
  return;
2038
2155
  }
2039
2156
 
@@ -2300,7 +2417,7 @@ function reconcilePendingRuntimeContexts(
2300
2417
  if (state.pending_runtime_contexts?.length === 0) {
2301
2418
  state.pending_runtime_contexts = undefined;
2302
2419
  }
2303
- publishProducerState(path, state);
2420
+ publishProducerState(input.coordRoot, path, state);
2304
2421
  }
2305
2422
 
2306
2423
  function reconcilePendingRuntimeContextsBeforeApprovedEnd(
@@ -3078,7 +3195,7 @@ export function recordApprovedSessionEndV3(
3078
3195
  const durability = writeEventV3(input.coordRoot, pending, input.writerOptions);
3079
3196
  applyCommittedEvent(state, pending);
3080
3197
  state.pending = undefined;
3081
- publishProducerState(record.path, state);
3198
+ publishProducerState(input.coordRoot, record.path, state);
3082
3199
  recovered = true;
3083
3200
  if (pending.event_type === "session.ended") {
3084
3201
  return { state: "recorded", event: pending, durability, recovered };
@@ -3175,11 +3292,11 @@ export function recordApprovedSessionEndV3(
3175
3292
  assertEventV3(event);
3176
3293
 
3177
3294
  state.pending = { event };
3178
- publishProducerState(record.path, state);
3295
+ publishProducerState(input.coordRoot, record.path, state);
3179
3296
  const durability = writeEventV3(input.coordRoot, event, input.writerOptions);
3180
3297
  applyCommittedEvent(state, event);
3181
3298
  state.pending = undefined;
3182
- publishProducerState(record.path, state);
3299
+ publishProducerState(input.coordRoot, record.path, state);
3183
3300
  return { state: "recorded", event, durability, recovered };
3184
3301
  } finally {
3185
3302
  lease.release();
@@ -3263,7 +3380,7 @@ export function salvageOpenSpansV3(input: SalvageOpenSpansV3Input): SalvageOpenS
3263
3380
  writeEventV3(input.coordRoot, pendingEvent, input.writerOptions);
3264
3381
  applyCommittedEvent(state, pendingEvent);
3265
3382
  state.pending = undefined;
3266
- publishProducerState(record.path, state);
3383
+ publishProducerState(input.coordRoot, record.path, state);
3267
3384
  }
3268
3385
  const allowed = new Set(input.allowed_span_ids);
3269
3386
  const fingerprintContext = fingerprintContextV3(
@@ -3345,6 +3462,155 @@ export function listHookProducerStateRecordsV3(
3345
3462
  );
3346
3463
  }
3347
3464
 
3465
+ export interface ReanchorArchivedHookProducersV3Input {
3466
+ coordRoot: string;
3467
+ archivedEpoch: string;
3468
+ mode: EventV3WriteMode;
3469
+ build_id: `build_${string}`;
3470
+ platform: "linux" | "windows" | "macos" | "unknown";
3471
+ observed_at?: string;
3472
+ }
3473
+
3474
+ export interface ReanchorArchivedHookProducersV3Result {
3475
+ sessions: number;
3476
+ turns: number;
3477
+ }
3478
+
3479
+ /**
3480
+ * Re-anchor every live archived hook producer into a newly activated epoch.
3481
+ *
3482
+ * The archive rename is the rotation's atomic snapshot: it includes every
3483
+ * state published before the boundary without copying any old boot sequence
3484
+ * or causal link into the successor. Each live session gets a fresh
3485
+ * generation and attestation. Only a turn that was open at the boundary is
3486
+ * reopened, which makes command telemetry joinable before another adapter
3487
+ * hook fires while preserving the between-turn state for idle sessions.
3488
+ */
3489
+ export function reanchorArchivedHookProducersV3(
3490
+ input: ReanchorArchivedHookProducersV3Input,
3491
+ ): ReanchorArchivedHookProducersV3Result {
3492
+ const control = readEventV3ControlState(input.coordRoot);
3493
+ if (control.state !== input.mode) {
3494
+ throw new Error(`event_v3_rotation_reanchor_gate_closed:${control.state}`);
3495
+ }
3496
+ const rootId = control.genesis.event.scope.root_id as `root_${string}`;
3497
+ const epochId = control.genesis.profile.privacy_key_epoch;
3498
+ const genesisId = control.genesis.event.payload.genesis_id as `gex_${string}`;
3499
+ const boundaryEventId =
3500
+ control.state === "candidate"
3501
+ ? control.genesis.event.event_id
3502
+ : control.activation.event.event_id;
3503
+ const observedAt = input.observed_at ?? new Date().toISOString();
3504
+ let sessions = 0;
3505
+ let turns = 0;
3506
+
3507
+ for (const archived of listArchivedHookProducerStatesV3(input.archivedEpoch)) {
3508
+ const previous = archived.state;
3509
+ if (previous.terminal || !previous.started_event_id) continue;
3510
+ const path = producerStatePath(input.coordRoot, previous.adapter, archived.sessionHash);
3511
+ const lease = acquireStateLeaseWithRetry(input.coordRoot, path, 40);
3512
+ if (!lease) {
3513
+ throw new Error(`event_v3_rotation_reanchor_busy:${previous.instance_id}`);
3514
+ }
3515
+ try {
3516
+ if (existsSync(path)) {
3517
+ const current = readProducerState(path);
3518
+ if (current.epoch_genesis_id === genesisId) continue;
3519
+ }
3520
+ const sourceEvent = `${previous.adapter}.epoch-rotation-reanchor`;
3521
+ const rotationInput: RecordHookSignalV3Input = {
3522
+ coordRoot: input.coordRoot,
3523
+ mode: input.mode,
3524
+ signal: "session-start",
3525
+ payload: {
3526
+ raw: {},
3527
+ ...(previous.current_native_turn_id ? { turn_id: previous.current_native_turn_id } : {}),
3528
+ ...(previous.adapter === "cursor" && previous.cursor_mode
3529
+ ? { cursor_mode: previous.cursor_mode }
3530
+ : {}),
3531
+ },
3532
+ adapter: previous.adapter,
3533
+ instance_id: previous.instance_id,
3534
+ producer_id: "prd_agent-hook",
3535
+ build_id: input.build_id,
3536
+ platform: input.platform,
3537
+ observed_at: observedAt,
3538
+ writerOptions: { expectedGenesisId: genesisId },
3539
+ };
3540
+ const state = newProducerState(
3541
+ rotationInput,
3542
+ previous.session_id,
3543
+ epochId,
3544
+ boundaryEventId as `evt_${string}`,
3545
+ genesisId,
3546
+ );
3547
+ const sessionStart = buildMidFlightSessionStart(rotationInput, state, rootId);
3548
+ if (sessionStart.event_type !== "session.started") {
3549
+ throw new Error("rotation re-anchor session start could not be normalized");
3550
+ }
3551
+ sessionStart.provenance = {
3552
+ ...sessionStart.provenance,
3553
+ source_event: sourceEvent,
3554
+ attestation: "derived",
3555
+ confidence: "high",
3556
+ attribution: {
3557
+ method: "session_env",
3558
+ state: "verified",
3559
+ subject_instance_id: state.instance_id,
3560
+ },
3561
+ };
3562
+ sessionStart.payload.resume = {
3563
+ state: "unknown",
3564
+ reason: "epoch_rotation_reanchor",
3565
+ };
3566
+ assertEventV3(sessionStart);
3567
+ commitEventLocked(rotationInput, state, path, sessionStart);
3568
+ sessions += 1;
3569
+
3570
+ if (previous.current_turn_id && previous.current_turn_span) {
3571
+ commitMidFlightTurnStart(rotationInput, state, path, rootId, {
3572
+ source_event: sourceEvent,
3573
+ confidence: "high",
3574
+ attribution_method: "session_env",
3575
+ });
3576
+ turns += 1;
3577
+ }
3578
+ } finally {
3579
+ lease.release();
3580
+ }
3581
+ }
3582
+ return { sessions, turns };
3583
+ }
3584
+
3585
+ function listArchivedHookProducerStatesV3(
3586
+ archivedEpoch: string,
3587
+ ): Array<{ sessionHash: `hid_${string}`; state: HookProducerStateV3 }> {
3588
+ const producerRoot = join(resolve(archivedEpoch), "private-producers");
3589
+ if (!existsSync(producerRoot)) return [];
3590
+ const records: Array<{ sessionHash: `hid_${string}`; state: HookProducerStateV3 }> = [];
3591
+ for (const adapter of ["claude-code", "codex", "cursor"] as const) {
3592
+ const directory = join(producerRoot, adapter);
3593
+ if (!existsSync(directory)) continue;
3594
+ const metadata = lstatSync(directory);
3595
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
3596
+ throw new Error("V3 archived producer state directory is unsafe");
3597
+ }
3598
+ for (const name of readdirSync(directory).filter((entry) =>
3599
+ /^hid_[a-f0-9]{64}\.json$/.test(entry),
3600
+ )) {
3601
+ records.push({
3602
+ sessionHash: name.slice(0, -".json".length) as `hid_${string}`,
3603
+ state: readProducerState(join(directory, name)),
3604
+ });
3605
+ }
3606
+ }
3607
+ return records.sort(
3608
+ (left, right) =>
3609
+ left.state.generation_id.localeCompare(right.state.generation_id) ||
3610
+ left.sessionHash.localeCompare(right.sessionHash),
3611
+ );
3612
+ }
3613
+
3348
3614
  function hookSignalCapability(signal: HookSignalV3): AdapterSignalV3 {
3349
3615
  switch (signal) {
3350
3616
  case "session-start":
@@ -3923,7 +4189,12 @@ function acquireStateLease(coordRoot: string, statePath: string) {
3923
4189
  mkdirSync(directory, { recursive: true, mode: 0o700 });
3924
4190
  chmodSync(producerRoot, 0o700);
3925
4191
  chmodSync(directory, 0o700);
3926
- return acquireNoClobberLease({
4192
+ const acquiredControl = readEventV3ControlState(coordRoot);
4193
+ const acquiredGenesis =
4194
+ acquiredControl.state === "candidate" || acquiredControl.state === "active"
4195
+ ? acquiredControl.genesis.event.payload.genesis_id
4196
+ : undefined;
4197
+ const lease = acquireNoClobberLease({
3927
4198
  path: `${statePath}.lease`,
3928
4199
  scope: "event-v3-hook-producer",
3929
4200
  authoritySha256: createHash("sha256")
@@ -3934,9 +4205,32 @@ function acquireStateLease(coordRoot: string, statePath: string) {
3934
4205
  staleAfterMs: 5_000,
3935
4206
  validateStaleOwner: (owner) => owner.host === hostname() && !pidIsAlive(owner.pid),
3936
4207
  });
4208
+ return {
4209
+ ...lease,
4210
+ release() {
4211
+ try {
4212
+ lease.release();
4213
+ } catch (error) {
4214
+ const current = readEventV3ControlState(coordRoot);
4215
+ const epochReplaced =
4216
+ acquiredGenesis !== undefined &&
4217
+ (current.state === "candidate" || current.state === "active") &&
4218
+ current.genesis.event.payload.genesis_id !== acquiredGenesis;
4219
+ if ((error as NodeJS.ErrnoException).code === "ENOENT" && epochReplaced) return;
4220
+ throw error;
4221
+ }
4222
+ },
4223
+ };
3937
4224
  }
3938
4225
 
3939
- function publishProducerState(path: string, state: HookProducerStateV3): void {
4226
+ function publishProducerState(coordRoot: string, path: string, state: HookProducerStateV3): void {
4227
+ const control = readEventV3ControlState(coordRoot);
4228
+ if (
4229
+ (control.state === "candidate" || control.state === "active") &&
4230
+ state.epoch_genesis_id !== control.genesis.event.payload.genesis_id
4231
+ ) {
4232
+ return;
4233
+ }
3940
4234
  const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
3941
4235
  let fd: number | undefined;
3942
4236
  try {