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.
- package/dist/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +7 -2
- package/dist/commands/admission.d.ts +21 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +565 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +61 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +48 -2
- package/dist/commands/browse-ai.d.ts +2 -2
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +6 -4
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +349 -21
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/qa-record.d.ts +144 -0
- package/dist/commands/qa-record.d.ts.map +1 -0
- package/dist/commands/qa-record.js +0 -0
- package/dist/commands/qa-run.d.ts +7 -3
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +268 -13
- package/dist/commands/qa-status.d.ts +71 -0
- package/dist/commands/qa-status.d.ts.map +1 -0
- package/dist/commands/qa-status.js +490 -0
- package/dist/commands/qa-verify.d.ts +40 -0
- package/dist/commands/qa-verify.d.ts.map +1 -0
- package/dist/commands/qa-verify.js +180 -0
- package/dist/commands/review-pack.d.ts +4 -0
- package/dist/commands/review-pack.d.ts.map +1 -0
- package/dist/commands/review-pack.js +1001 -0
- package/dist/core/agents/qa-signal.d.ts +111 -0
- package/dist/core/agents/qa-signal.d.ts.map +1 -0
- package/dist/core/agents/qa-signal.js +231 -0
- package/dist/core/agents/session-name-display.d.ts +20 -5
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +67 -7
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +17 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/artifacts/constants.d.ts +1 -1
- package/dist/core/artifacts/constants.js +1 -1
- package/dist/core/artifacts/index.d.ts +57 -6
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +265 -10
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -0
- package/dist/core/diagnostics/bundle.d.ts +16 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +101 -7
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +10 -0
- package/dist/core/events/v3/coordination-view.d.ts +3 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-view.js +69 -8
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +9 -2
- package/dist/core/events/v3/producers/recorder.d.ts +23 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +264 -18
- package/dist/core/hooks/cli.js +120 -27
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +10 -3
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +4 -1
- package/dist/core/qa-artifacts.d.ts +20 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -0
- package/dist/core/qa-artifacts.js +110 -0
- package/dist/core/resources/contract.d.ts +6 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/sampler.d.ts +7 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +106 -5
- package/dist/lib/admission.d.ts +71 -0
- package/dist/lib/admission.d.ts.map +1 -0
- package/dist/lib/admission.js +264 -0
- package/dist/lib/agent-browser/client.d.ts +1 -1
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +1 -5
- package/dist/lib/browser/capture-fidelity.d.ts +39 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
- package/dist/lib/browser/capture-fidelity.js +84 -0
- package/dist/lib/browser/client.d.ts +41 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +167 -9
- package/dist/lib/browser/critique.d.ts +38 -1
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +34 -6
- package/dist/lib/browser/index.d.ts +4 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/page-review-judge.d.ts +64 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
- package/dist/lib/browser/page-review-judge.js +270 -0
- package/dist/lib/browser/page-review-pack.d.ts +613 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
- package/dist/lib/browser/page-review-pack.js +1751 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +136 -1
- package/dist/lib/browser/qa-run.d.ts +100 -10
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +768 -169
- package/dist/lib/browser/request-diagnostics.d.ts +13 -0
- package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
- package/dist/lib/browser/request-diagnostics.js +18 -0
- package/dist/lib/browser/tiling.d.ts +19 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +28 -0
- package/dist/lib/cookies/client.d.ts +9 -0
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +197 -44
- package/dist/lib/cookies/extra.d.ts +18 -0
- package/dist/lib/cookies/extra.d.ts.map +1 -0
- package/dist/lib/cookies/extra.js +14 -0
- package/dist/lib/cookies/index.d.ts +2 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +2 -1
- package/dist/lib/durable-job.d.ts +124 -0
- package/dist/lib/durable-job.d.ts.map +1 -0
- package/dist/lib/durable-job.js +296 -0
- package/dist/lib/http/client.d.ts +7 -1
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +2 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +5 -2
- package/package.json +8 -2
- package/src/commander.ts +50 -2
- package/src/commands/admission.ts +699 -0
- package/src/commands/agents.ts +87 -1
- package/src/commands/artifacts.ts +97 -21
- package/src/commands/browse-ai.ts +10 -5
- package/src/commands/browse.ts +481 -20
- package/src/commands/fetch.ts +1 -0
- package/src/commands/qa-record.ts +682 -0
- package/src/commands/qa-run.ts +335 -16
- package/src/commands/qa-status.ts +608 -0
- package/src/commands/qa-verify.ts +238 -0
- package/src/commands/review-pack.ts +1281 -0
- package/src/core/agents/qa-signal.ts +261 -0
- package/src/core/agents/session-name-display.ts +78 -7
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +23 -0
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +5 -0
- package/src/core/artifacts/constants.ts +1 -1
- package/src/core/artifacts/index.ts +370 -21
- package/src/core/config.ts +23 -0
- package/src/core/diagnostics/bundle.ts +119 -11
- package/src/core/events/v3/bootstrap.ts +10 -0
- package/src/core/events/v3/coordination-view.ts +100 -11
- package/src/core/events/v3/producers/intake.ts +9 -2
- package/src/core/events/v3/producers/recorder.ts +312 -18
- package/src/core/hooks/cli.ts +140 -32
- package/src/core/hooks/resolve/transcript.ts +10 -3
- package/src/core/hooks/session-name-presence.ts +6 -1
- package/src/core/qa-artifacts.ts +126 -0
- package/src/core/resources/contract.ts +7 -0
- package/src/core/resources/sampler.ts +138 -6
- package/src/lib/admission.ts +347 -0
- package/src/lib/agent-browser/client.ts +2 -10
- package/src/lib/browser/capture-fidelity.ts +98 -0
- package/src/lib/browser/client.ts +206 -10
- package/src/lib/browser/critique.ts +62 -7
- package/src/lib/browser/index.ts +36 -0
- package/src/lib/browser/page-review-judge.ts +360 -0
- package/src/lib/browser/page-review-pack.ts +2384 -0
- package/src/lib/browser/qa-run-contracts.ts +366 -3
- package/src/lib/browser/qa-run.ts +868 -190
- package/src/lib/browser/request-diagnostics.ts +27 -0
- package/src/lib/browser/tiling.ts +32 -0
- package/src/lib/cookies/client.ts +228 -42
- package/src/lib/cookies/extra.ts +28 -0
- package/src/lib/cookies/index.ts +2 -0
- package/src/lib/durable-job.ts +407 -0
- package/src/lib/http/client.ts +13 -1
- package/src/lib/instructions/templates.ts +5 -2
|
@@ -329,7 +329,7 @@ function processHookSignalLocked(control, input, sessionHash, path) {
|
|
|
329
329
|
const durability = writeEventV3(input.coordRoot, pendingEvent, input.writerOptions);
|
|
330
330
|
applyCommittedEvent(state, pendingEvent);
|
|
331
331
|
state.pending = undefined;
|
|
332
|
-
publishProducerState(path, state);
|
|
332
|
+
publishProducerState(input.coordRoot, path, state);
|
|
333
333
|
if (pendingSource && incomingSource === pendingSource) {
|
|
334
334
|
recovered = { state: "recorded", event: pendingEvent, durability, recovered: true };
|
|
335
335
|
}
|
|
@@ -373,7 +373,16 @@ function processHookSignalLocked(control, input, sessionHash, path) {
|
|
|
373
373
|
state = newProducerState(input, sessionId, epochId, boundaryEventId, genesisId);
|
|
374
374
|
const cursorPromptBootstrap = isCursorPromptBootstrap(input);
|
|
375
375
|
const onboarding = buildMidFlightSessionStart(input, state, rootId, cursorPromptBootstrap);
|
|
376
|
-
|
|
376
|
+
// Onboarding commits eagerly even when the triggering hook defers its
|
|
377
|
+
// drain. The coordination view reads committed history only, so a
|
|
378
|
+
// spooled session.started leaves the session invisible to every
|
|
379
|
+
// authority command (set-task, status --end-turn, heal) until some other
|
|
380
|
+
// writer happens to drain the spool.
|
|
381
|
+
const onboardingInput = withEagerDrain(input);
|
|
382
|
+
commitEventLocked(onboardingInput, state, path, onboarding);
|
|
383
|
+
if (midFlightSignalImpliesOpenTurn(input.signal)) {
|
|
384
|
+
commitMidFlightTurnStart(onboardingInput, state, path, rootId);
|
|
385
|
+
}
|
|
377
386
|
if (!cursorPromptBootstrap) {
|
|
378
387
|
writeProducerDiagnosticV3(input.coordRoot, "mid_flight_onboarding", {
|
|
379
388
|
adapter: input.adapter,
|
|
@@ -460,7 +469,7 @@ function processHookSignalLocked(control, input, sessionHash, path) {
|
|
|
460
469
|
span_id: state.current_turn_span?.span_id,
|
|
461
470
|
payload: input.payload,
|
|
462
471
|
});
|
|
463
|
-
publishProducerState(path, state);
|
|
472
|
+
publishProducerState(input.coordRoot, path, state);
|
|
464
473
|
return { state: "ignored" };
|
|
465
474
|
}
|
|
466
475
|
// Every event derived before the native signal has now been committed.
|
|
@@ -483,7 +492,7 @@ function processHookSignalLocked(control, input, sessionHash, path) {
|
|
|
483
492
|
? "no_open_turn_span"
|
|
484
493
|
: "ritual_observation_missing",
|
|
485
494
|
});
|
|
486
|
-
publishProducerState(path, state);
|
|
495
|
+
publishProducerState(input.coordRoot, path, state);
|
|
487
496
|
return { state: "ignored" };
|
|
488
497
|
}
|
|
489
498
|
const observation = {
|
|
@@ -494,7 +503,7 @@ function processHookSignalLocked(control, input, sessionHash, path) {
|
|
|
494
503
|
status_box_present_strict: input.turn_ritual.status_box_present_strict,
|
|
495
504
|
};
|
|
496
505
|
state.cursor_response_ritual = observation;
|
|
497
|
-
publishProducerState(path, state);
|
|
506
|
+
publishProducerState(input.coordRoot, path, state);
|
|
498
507
|
writeProducerDiagnosticV3(input.coordRoot, "cursor_response_ritual_observed", {
|
|
499
508
|
instance_id: state.instance_id,
|
|
500
509
|
generation_id: state.generation_id,
|
|
@@ -1259,16 +1268,107 @@ function commitRecoveredToolTurnStart(input, state, path, rootId, fingerprintCon
|
|
|
1259
1268
|
assertEventV3(event);
|
|
1260
1269
|
commitEventLocked(input, state, path, event);
|
|
1261
1270
|
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Signals an adapter can only deliver while a turn is executing. A session
|
|
1273
|
+
* onboarded mid-flight by one of these is provably inside a turn, so the
|
|
1274
|
+
* generation opens with one instead of waiting for the next native prompt.
|
|
1275
|
+
* The native prompt path opens its own turn, session boundaries carry no
|
|
1276
|
+
* turn, and compaction can run between turns.
|
|
1277
|
+
*/
|
|
1278
|
+
function midFlightSignalImpliesOpenTurn(signal) {
|
|
1279
|
+
switch (signal) {
|
|
1280
|
+
case "session-start":
|
|
1281
|
+
case "session-end":
|
|
1282
|
+
case "user-prompt-submit":
|
|
1283
|
+
case "pre-compact":
|
|
1284
|
+
case "post-compact":
|
|
1285
|
+
return false;
|
|
1286
|
+
default:
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/** Commit through the append path even when the hook asked to defer its drain. */
|
|
1291
|
+
function withEagerDrain(input) {
|
|
1292
|
+
if (!input.writerOptions?.deferDrain)
|
|
1293
|
+
return input;
|
|
1294
|
+
return { ...input, writerOptions: { ...input.writerOptions, deferDrain: false } };
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Open a derived turn for a session onboarded mid-flight (ADR 0078) by a
|
|
1298
|
+
* signal that only an executing turn can deliver. Without it the fresh
|
|
1299
|
+
* generation has no current turn: command telemetry refuses to join
|
|
1300
|
+
* (`turn_not_started`), tool spans nest under the session span, and after an
|
|
1301
|
+
* epoch rotation (ADR 0137) the live session cannot satisfy its end-of-turn
|
|
1302
|
+
* gate until a human submits the next prompt. The payload's native turn id
|
|
1303
|
+
* is kept when the adapter supplies one, so the turn already running keeps
|
|
1304
|
+
* its identity across the boundary and later signals join it directly.
|
|
1305
|
+
*/
|
|
1306
|
+
function commitMidFlightTurnStart(input, state, path, rootId, recovery) {
|
|
1307
|
+
const fingerprintContext = fingerprintContextV3(input.coordRoot, rootId, state.generation_id, state.privacy_epoch_id);
|
|
1308
|
+
const clock = signalClock(input);
|
|
1309
|
+
const span = openSpanStateV3({
|
|
1310
|
+
span_id: spanIdV3(),
|
|
1311
|
+
parent_span_id: state.session_span.span_id,
|
|
1312
|
+
boot_id: state.boot_id,
|
|
1313
|
+
clock,
|
|
1314
|
+
});
|
|
1315
|
+
state.current_turn_span = span;
|
|
1316
|
+
const nativeTurnId = input.payload.turn_id;
|
|
1317
|
+
state.current_native_turn_id = nativeTurnId;
|
|
1318
|
+
const event = normalizeHookEventV3("user-prompt-submit", recoveryBoundaryPayload(input.payload), {
|
|
1319
|
+
coordRoot: input.coordRoot,
|
|
1320
|
+
adapter: input.adapter,
|
|
1321
|
+
adapterVersion: input.adapterVersion,
|
|
1322
|
+
harnessVersion: input.harnessVersion,
|
|
1323
|
+
root_id: rootId,
|
|
1324
|
+
run_id: input.run_id,
|
|
1325
|
+
workflow_id: input.workflow_id,
|
|
1326
|
+
workflow_agent_id: input.workflow_agent_id,
|
|
1327
|
+
instance_id: state.instance_id,
|
|
1328
|
+
generation_id: state.generation_id,
|
|
1329
|
+
attestation_id: state.attestation_id,
|
|
1330
|
+
producer_id: input.producer_id,
|
|
1331
|
+
boot_id: state.boot_id,
|
|
1332
|
+
sequence: state.next_sequence,
|
|
1333
|
+
build_id: input.build_id,
|
|
1334
|
+
platform: input.platform,
|
|
1335
|
+
bridge: input.bridge,
|
|
1336
|
+
capability_profile: state.capability_profile,
|
|
1337
|
+
fingerprintContext,
|
|
1338
|
+
turn_native_id: nativeTurnId ?? `mid-flight:${state.generation_id}:${state.turn_ordinal + 1}`,
|
|
1339
|
+
span_id: span.span_id,
|
|
1340
|
+
caused_by: state.last_event_id ? [state.last_event_id] : [],
|
|
1341
|
+
observed_at: clock.observed_at,
|
|
1342
|
+
monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
|
|
1343
|
+
clock_id: state.clock_id,
|
|
1344
|
+
});
|
|
1345
|
+
if (event?.event_type !== "turn.started") {
|
|
1346
|
+
throw new Error("mid-flight turn start could not be normalized");
|
|
1347
|
+
}
|
|
1348
|
+
event.provenance = {
|
|
1349
|
+
...event.provenance,
|
|
1350
|
+
source_event: recovery?.source_event ?? `${input.adapter}.recovery`,
|
|
1351
|
+
attestation: "derived",
|
|
1352
|
+
confidence: recovery?.confidence ?? (nativeTurnId ? "medium" : "low"),
|
|
1353
|
+
attribution: {
|
|
1354
|
+
method: recovery?.attribution_method ?? (nativeTurnId ? "native_payload" : "session_env"),
|
|
1355
|
+
state: "verified",
|
|
1356
|
+
subject_instance_id: state.instance_id,
|
|
1357
|
+
},
|
|
1358
|
+
};
|
|
1359
|
+
assertEventV3(event);
|
|
1360
|
+
commitEventLocked(input, state, path, event);
|
|
1361
|
+
}
|
|
1262
1362
|
/** The single pending-publish/write/apply/publish cycle every locked event commit uses. */
|
|
1263
1363
|
function commitEventLocked(input, state, path, event, sourceId) {
|
|
1264
1364
|
markObservedClockRegressionV3(event, state.last_observed_at);
|
|
1265
1365
|
assertEventV3(event);
|
|
1266
1366
|
state.pending = { ...(sourceId ? { source_id: sourceId } : {}), event };
|
|
1267
|
-
publishProducerState(path, state);
|
|
1367
|
+
publishProducerState(input.coordRoot, path, state);
|
|
1268
1368
|
const durability = writeEventV3(input.coordRoot, event, input.writerOptions);
|
|
1269
1369
|
applyCommittedEvent(state, event);
|
|
1270
1370
|
state.pending = undefined;
|
|
1271
|
-
publishProducerState(path, state);
|
|
1371
|
+
publishProducerState(input.coordRoot, path, state);
|
|
1272
1372
|
return durability;
|
|
1273
1373
|
}
|
|
1274
1374
|
function commitTurnContextObservation(input, state, path, rootId, fingerprintContext, target, measurement, contextProvenance, observedAt) {
|
|
@@ -1287,7 +1387,7 @@ function commitContextObservation(input, state, path, rootId, fingerprintContext
|
|
|
1287
1387
|
if (measurement.state === "observed" &&
|
|
1288
1388
|
contextProvenance?.source_witness &&
|
|
1289
1389
|
state.last_context_source_witness === contextProvenance.source_witness) {
|
|
1290
|
-
publishProducerState(path, state);
|
|
1390
|
+
publishProducerState(input.coordRoot, path, state);
|
|
1291
1391
|
return;
|
|
1292
1392
|
}
|
|
1293
1393
|
if (measurement.state === "observed" && contextProvenance?.source_witness) {
|
|
@@ -1387,12 +1487,12 @@ function maybeCommitActiveRuntimeContextObservation(input, state, path, rootId,
|
|
|
1387
1487
|
return;
|
|
1388
1488
|
}
|
|
1389
1489
|
if (input.adapter === "cursor" && state.cursor_mode === "cloud") {
|
|
1390
|
-
publishProducerState(path, state);
|
|
1490
|
+
publishProducerState(input.coordRoot, path, state);
|
|
1391
1491
|
return;
|
|
1392
1492
|
}
|
|
1393
1493
|
const transcriptPath = runtimeTranscriptPath(input, state);
|
|
1394
1494
|
if (!transcriptPath && input.adapter !== "cursor") {
|
|
1395
|
-
publishProducerState(path, state);
|
|
1495
|
+
publishProducerState(input.coordRoot, path, state);
|
|
1396
1496
|
return;
|
|
1397
1497
|
}
|
|
1398
1498
|
const runtime = readRuntimeContextTelemetry({
|
|
@@ -1409,7 +1509,7 @@ function maybeCommitActiveRuntimeContextObservation(input, state, path, rootId,
|
|
|
1409
1509
|
if (runtime.bytes_read > 0)
|
|
1410
1510
|
recordRuntimeTelemetryTiming(state, runtime.io_duration_ms);
|
|
1411
1511
|
if (runtime.state !== "observed") {
|
|
1412
|
-
publishProducerState(path, state);
|
|
1512
|
+
publishProducerState(input.coordRoot, path, state);
|
|
1413
1513
|
return;
|
|
1414
1514
|
}
|
|
1415
1515
|
commitActiveContextObservation(input, state, path, rootId, fingerprintContext, sourceEvent, runtimeContextObservation(runtime), {
|
|
@@ -1578,7 +1678,7 @@ function reconcilePendingRuntimeContexts(input, state, path, rootId, fingerprint
|
|
|
1578
1678
|
if (state.pending_runtime_contexts?.length === 0) {
|
|
1579
1679
|
state.pending_runtime_contexts = undefined;
|
|
1580
1680
|
}
|
|
1581
|
-
publishProducerState(path, state);
|
|
1681
|
+
publishProducerState(input.coordRoot, path, state);
|
|
1582
1682
|
}
|
|
1583
1683
|
function reconcilePendingRuntimeContextsBeforeApprovedEnd(input, state, path, rootId, fingerprintContext) {
|
|
1584
1684
|
if (state.adapter !== "codex" || !state.pending_runtime_contexts?.length)
|
|
@@ -2234,7 +2334,7 @@ export function recordApprovedSessionEndV3(input) {
|
|
|
2234
2334
|
const durability = writeEventV3(input.coordRoot, pending, input.writerOptions);
|
|
2235
2335
|
applyCommittedEvent(state, pending);
|
|
2236
2336
|
state.pending = undefined;
|
|
2237
|
-
publishProducerState(record.path, state);
|
|
2337
|
+
publishProducerState(input.coordRoot, record.path, state);
|
|
2238
2338
|
recovered = true;
|
|
2239
2339
|
if (pending.event_type === "session.ended") {
|
|
2240
2340
|
return { state: "recorded", event: pending, durability, recovered };
|
|
@@ -2317,11 +2417,11 @@ export function recordApprovedSessionEndV3(input) {
|
|
|
2317
2417
|
markObservedClockRegressionV3(event, state.last_observed_at);
|
|
2318
2418
|
assertEventV3(event);
|
|
2319
2419
|
state.pending = { event };
|
|
2320
|
-
publishProducerState(record.path, state);
|
|
2420
|
+
publishProducerState(input.coordRoot, record.path, state);
|
|
2321
2421
|
const durability = writeEventV3(input.coordRoot, event, input.writerOptions);
|
|
2322
2422
|
applyCommittedEvent(state, event);
|
|
2323
2423
|
state.pending = undefined;
|
|
2324
|
-
publishProducerState(record.path, state);
|
|
2424
|
+
publishProducerState(input.coordRoot, record.path, state);
|
|
2325
2425
|
return { state: "recorded", event, durability, recovered };
|
|
2326
2426
|
}
|
|
2327
2427
|
finally {
|
|
@@ -2383,7 +2483,7 @@ export function salvageOpenSpansV3(input) {
|
|
|
2383
2483
|
writeEventV3(input.coordRoot, pendingEvent, input.writerOptions);
|
|
2384
2484
|
applyCommittedEvent(state, pendingEvent);
|
|
2385
2485
|
state.pending = undefined;
|
|
2386
|
-
publishProducerState(record.path, state);
|
|
2486
|
+
publishProducerState(input.coordRoot, record.path, state);
|
|
2387
2487
|
}
|
|
2388
2488
|
const allowed = new Set(input.allowed_span_ids);
|
|
2389
2489
|
const fingerprintContext = fingerprintContextV3(input.coordRoot, rootId, state.generation_id, state.privacy_epoch_id);
|
|
@@ -2445,6 +2545,126 @@ export function listHookProducerStateRecordsV3(coordRoot, options = {}) {
|
|
|
2445
2545
|
return records.sort((left, right) => left.state.generation_id.localeCompare(right.state.generation_id) ||
|
|
2446
2546
|
left.path.localeCompare(right.path));
|
|
2447
2547
|
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Re-anchor every live archived hook producer into a newly activated epoch.
|
|
2550
|
+
*
|
|
2551
|
+
* The archive rename is the rotation's atomic snapshot: it includes every
|
|
2552
|
+
* state published before the boundary without copying any old boot sequence
|
|
2553
|
+
* or causal link into the successor. Each live session gets a fresh
|
|
2554
|
+
* generation and attestation. Only a turn that was open at the boundary is
|
|
2555
|
+
* reopened, which makes command telemetry joinable before another adapter
|
|
2556
|
+
* hook fires while preserving the between-turn state for idle sessions.
|
|
2557
|
+
*/
|
|
2558
|
+
export function reanchorArchivedHookProducersV3(input) {
|
|
2559
|
+
const control = readEventV3ControlState(input.coordRoot);
|
|
2560
|
+
if (control.state !== input.mode) {
|
|
2561
|
+
throw new Error(`event_v3_rotation_reanchor_gate_closed:${control.state}`);
|
|
2562
|
+
}
|
|
2563
|
+
const rootId = control.genesis.event.scope.root_id;
|
|
2564
|
+
const epochId = control.genesis.profile.privacy_key_epoch;
|
|
2565
|
+
const genesisId = control.genesis.event.payload.genesis_id;
|
|
2566
|
+
const boundaryEventId = control.state === "candidate"
|
|
2567
|
+
? control.genesis.event.event_id
|
|
2568
|
+
: control.activation.event.event_id;
|
|
2569
|
+
const observedAt = input.observed_at ?? new Date().toISOString();
|
|
2570
|
+
let sessions = 0;
|
|
2571
|
+
let turns = 0;
|
|
2572
|
+
for (const archived of listArchivedHookProducerStatesV3(input.archivedEpoch)) {
|
|
2573
|
+
const previous = archived.state;
|
|
2574
|
+
if (previous.terminal || !previous.started_event_id)
|
|
2575
|
+
continue;
|
|
2576
|
+
const path = producerStatePath(input.coordRoot, previous.adapter, archived.sessionHash);
|
|
2577
|
+
const lease = acquireStateLeaseWithRetry(input.coordRoot, path, 40);
|
|
2578
|
+
if (!lease) {
|
|
2579
|
+
throw new Error(`event_v3_rotation_reanchor_busy:${previous.instance_id}`);
|
|
2580
|
+
}
|
|
2581
|
+
try {
|
|
2582
|
+
if (existsSync(path)) {
|
|
2583
|
+
const current = readProducerState(path);
|
|
2584
|
+
if (current.epoch_genesis_id === genesisId)
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
const sourceEvent = `${previous.adapter}.epoch-rotation-reanchor`;
|
|
2588
|
+
const rotationInput = {
|
|
2589
|
+
coordRoot: input.coordRoot,
|
|
2590
|
+
mode: input.mode,
|
|
2591
|
+
signal: "session-start",
|
|
2592
|
+
payload: {
|
|
2593
|
+
raw: {},
|
|
2594
|
+
...(previous.current_native_turn_id ? { turn_id: previous.current_native_turn_id } : {}),
|
|
2595
|
+
...(previous.adapter === "cursor" && previous.cursor_mode
|
|
2596
|
+
? { cursor_mode: previous.cursor_mode }
|
|
2597
|
+
: {}),
|
|
2598
|
+
},
|
|
2599
|
+
adapter: previous.adapter,
|
|
2600
|
+
instance_id: previous.instance_id,
|
|
2601
|
+
producer_id: "prd_agent-hook",
|
|
2602
|
+
build_id: input.build_id,
|
|
2603
|
+
platform: input.platform,
|
|
2604
|
+
observed_at: observedAt,
|
|
2605
|
+
writerOptions: { expectedGenesisId: genesisId },
|
|
2606
|
+
};
|
|
2607
|
+
const state = newProducerState(rotationInput, previous.session_id, epochId, boundaryEventId, genesisId);
|
|
2608
|
+
const sessionStart = buildMidFlightSessionStart(rotationInput, state, rootId);
|
|
2609
|
+
if (sessionStart.event_type !== "session.started") {
|
|
2610
|
+
throw new Error("rotation re-anchor session start could not be normalized");
|
|
2611
|
+
}
|
|
2612
|
+
sessionStart.provenance = {
|
|
2613
|
+
...sessionStart.provenance,
|
|
2614
|
+
source_event: sourceEvent,
|
|
2615
|
+
attestation: "derived",
|
|
2616
|
+
confidence: "high",
|
|
2617
|
+
attribution: {
|
|
2618
|
+
method: "session_env",
|
|
2619
|
+
state: "verified",
|
|
2620
|
+
subject_instance_id: state.instance_id,
|
|
2621
|
+
},
|
|
2622
|
+
};
|
|
2623
|
+
sessionStart.payload.resume = {
|
|
2624
|
+
state: "unknown",
|
|
2625
|
+
reason: "epoch_rotation_reanchor",
|
|
2626
|
+
};
|
|
2627
|
+
assertEventV3(sessionStart);
|
|
2628
|
+
commitEventLocked(rotationInput, state, path, sessionStart);
|
|
2629
|
+
sessions += 1;
|
|
2630
|
+
if (previous.current_turn_id && previous.current_turn_span) {
|
|
2631
|
+
commitMidFlightTurnStart(rotationInput, state, path, rootId, {
|
|
2632
|
+
source_event: sourceEvent,
|
|
2633
|
+
confidence: "high",
|
|
2634
|
+
attribution_method: "session_env",
|
|
2635
|
+
});
|
|
2636
|
+
turns += 1;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
finally {
|
|
2640
|
+
lease.release();
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return { sessions, turns };
|
|
2644
|
+
}
|
|
2645
|
+
function listArchivedHookProducerStatesV3(archivedEpoch) {
|
|
2646
|
+
const producerRoot = join(resolve(archivedEpoch), "private-producers");
|
|
2647
|
+
if (!existsSync(producerRoot))
|
|
2648
|
+
return [];
|
|
2649
|
+
const records = [];
|
|
2650
|
+
for (const adapter of ["claude-code", "codex", "cursor"]) {
|
|
2651
|
+
const directory = join(producerRoot, adapter);
|
|
2652
|
+
if (!existsSync(directory))
|
|
2653
|
+
continue;
|
|
2654
|
+
const metadata = lstatSync(directory);
|
|
2655
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
2656
|
+
throw new Error("V3 archived producer state directory is unsafe");
|
|
2657
|
+
}
|
|
2658
|
+
for (const name of readdirSync(directory).filter((entry) => /^hid_[a-f0-9]{64}\.json$/.test(entry))) {
|
|
2659
|
+
records.push({
|
|
2660
|
+
sessionHash: name.slice(0, -".json".length),
|
|
2661
|
+
state: readProducerState(join(directory, name)),
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
return records.sort((left, right) => left.state.generation_id.localeCompare(right.state.generation_id) ||
|
|
2666
|
+
left.sessionHash.localeCompare(right.sessionHash));
|
|
2667
|
+
}
|
|
2448
2668
|
function hookSignalCapability(signal) {
|
|
2449
2669
|
switch (signal) {
|
|
2450
2670
|
case "session-start":
|
|
@@ -2895,7 +3115,11 @@ function acquireStateLease(coordRoot, statePath) {
|
|
|
2895
3115
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
2896
3116
|
chmodSync(producerRoot, 0o700);
|
|
2897
3117
|
chmodSync(directory, 0o700);
|
|
2898
|
-
|
|
3118
|
+
const acquiredControl = readEventV3ControlState(coordRoot);
|
|
3119
|
+
const acquiredGenesis = acquiredControl.state === "candidate" || acquiredControl.state === "active"
|
|
3120
|
+
? acquiredControl.genesis.event.payload.genesis_id
|
|
3121
|
+
: undefined;
|
|
3122
|
+
const lease = acquireNoClobberLease({
|
|
2899
3123
|
path: `${statePath}.lease`,
|
|
2900
3124
|
scope: "event-v3-hook-producer",
|
|
2901
3125
|
authoritySha256: createHash("sha256")
|
|
@@ -2906,8 +3130,30 @@ function acquireStateLease(coordRoot, statePath) {
|
|
|
2906
3130
|
staleAfterMs: 5_000,
|
|
2907
3131
|
validateStaleOwner: (owner) => owner.host === hostname() && !pidIsAlive(owner.pid),
|
|
2908
3132
|
});
|
|
3133
|
+
return {
|
|
3134
|
+
...lease,
|
|
3135
|
+
release() {
|
|
3136
|
+
try {
|
|
3137
|
+
lease.release();
|
|
3138
|
+
}
|
|
3139
|
+
catch (error) {
|
|
3140
|
+
const current = readEventV3ControlState(coordRoot);
|
|
3141
|
+
const epochReplaced = acquiredGenesis !== undefined &&
|
|
3142
|
+
(current.state === "candidate" || current.state === "active") &&
|
|
3143
|
+
current.genesis.event.payload.genesis_id !== acquiredGenesis;
|
|
3144
|
+
if (error.code === "ENOENT" && epochReplaced)
|
|
3145
|
+
return;
|
|
3146
|
+
throw error;
|
|
3147
|
+
}
|
|
3148
|
+
},
|
|
3149
|
+
};
|
|
2909
3150
|
}
|
|
2910
|
-
function publishProducerState(path, state) {
|
|
3151
|
+
function publishProducerState(coordRoot, path, state) {
|
|
3152
|
+
const control = readEventV3ControlState(coordRoot);
|
|
3153
|
+
if ((control.state === "candidate" || control.state === "active") &&
|
|
3154
|
+
state.epoch_genesis_id !== control.genesis.event.payload.genesis_id) {
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
2911
3157
|
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
2912
3158
|
let fd;
|
|
2913
3159
|
try {
|
package/dist/core/hooks/cli.js
CHANGED
|
@@ -25,8 +25,8 @@ import { resolveCoordRoot } from "../agents/coord-client.js";
|
|
|
25
25
|
import { classifyWriteClaimFinalization, formatWriteClaimFinalizationDenial, } from "../agents/finalization.js";
|
|
26
26
|
import { recordLiveClaimChangeV3, restoreLiveCoordinationStateAfterEpochV3, } from "../agents/live-authority-v3.js";
|
|
27
27
|
import { evaluateStopHook, STOP_REMEDIATION_MARKER } from "../agents/rules/stop-hook.js";
|
|
28
|
-
import { assistantTextStartsWithSessionNameBlock, isSessionNameRemediationCommand, sessionNameDisplayInstruction, sessionNameDisplayPending, sessionNameDisplayRecoveryInstruction, toolResponseMintedSessionName, } from "../agents/session-name-display.js";
|
|
29
|
-
import { readHeartbeat, stampSessionNameSeen } from "../agents/state/heartbeat-writer.js";
|
|
28
|
+
import { assistantTextStartsWithSessionNameBlock, isSessionNameRemediationCommand, matchSessionNameDisplay, sessionNameDisplayAcceptedNames, sessionNameDisplayInstruction, sessionNameDisplayPending, sessionNameDisplayRecoveryInstruction, toolResponseMintedSessionName, } from "../agents/session-name-display.js";
|
|
29
|
+
import { readHeartbeat, setAssignedNameCache, stampSessionNameRequested, stampSessionNameSeen, } from "../agents/state/heartbeat-writer.js";
|
|
30
30
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
|
|
31
31
|
import { ensureLiveCoordinationHeartbeat } from "../agents/state/live-coordination-writer.js";
|
|
32
32
|
import { assignName } from "../agents/state/names.js";
|
|
@@ -159,6 +159,29 @@ function assignNameInProcess(coordRoot, instanceId, kind, forkedFrom) {
|
|
|
159
159
|
return null;
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
+
/** Mid-flight sessions can emit tools before SessionStart assigns a pool name.
|
|
163
|
+
* Stamp the disposable cache so Codec does not render the 8-character id. */
|
|
164
|
+
function ensureSessionDisplayName(coordRoot, instanceId, sessionId, adapter) {
|
|
165
|
+
if (coordEnv("WORKFLOW_CHILD") === "1")
|
|
166
|
+
return;
|
|
167
|
+
const row = readLiveCoordinationRow(coordRoot, instanceId) ?? readHeartbeat(coordRoot, instanceId);
|
|
168
|
+
if (row?.kind === "subagent" || row?.kind === "transient" || row?.workflow_run_id)
|
|
169
|
+
return;
|
|
170
|
+
if (row?.name?.trim())
|
|
171
|
+
return;
|
|
172
|
+
const assigned = assignNameInProcess(coordRoot, instanceId, "session");
|
|
173
|
+
if (!assigned?.name)
|
|
174
|
+
return;
|
|
175
|
+
if (!readHeartbeat(coordRoot, instanceId)) {
|
|
176
|
+
try {
|
|
177
|
+
ensureLiveCoordinationHeartbeat(coordRoot, instanceId, sessionId, adapter);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
/* cache materialization is best-effort; history still holds the name */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
setAssignedNameCache(coordRoot, instanceId, assigned.name, "session");
|
|
184
|
+
}
|
|
162
185
|
/**
|
|
163
186
|
* Direct (in-process) pidmap write, avoiding the spawn overhead of going via
|
|
164
187
|
* the agent-coord CLI for every session.started signal. Pid-map rows
|
|
@@ -270,11 +293,11 @@ function buildEventData(eventType, ctx) {
|
|
|
270
293
|
clean_exit: p?.clean_exit ?? true,
|
|
271
294
|
};
|
|
272
295
|
case "turn.started": {
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
// Mid-flight onboarding (any adapter) can observe a turn before
|
|
297
|
+
// SessionStart. Mint durable display identity here so a later
|
|
298
|
+
// SessionStart remains idempotent and Codec is not stuck on the
|
|
299
|
+
// 8-character native id.
|
|
300
|
+
if (!readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name) {
|
|
278
301
|
assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session");
|
|
279
302
|
}
|
|
280
303
|
const prompt = p?.prompt ?? "";
|
|
@@ -559,14 +582,34 @@ async function main() {
|
|
|
559
582
|
appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
|
|
560
583
|
return 0;
|
|
561
584
|
}
|
|
562
|
-
const
|
|
585
|
+
const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
|
|
586
|
+
const name = sessionNameDisplayPending(row);
|
|
563
587
|
const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
588
|
+
const sighting = matchSessionNameDisplay(row, text);
|
|
589
|
+
if (sighting) {
|
|
590
|
+
stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
|
|
591
|
+
appendDebug(coordRoot, {
|
|
592
|
+
...debugBase,
|
|
593
|
+
effect: "session-name-display-stamped",
|
|
594
|
+
session_name_pending: sighting.pending,
|
|
595
|
+
...(sighting.displayed === sighting.pending
|
|
596
|
+
? {}
|
|
597
|
+
: { session_name_displayed: sighting.displayed, session_name_drift: true }),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
else if (!name) {
|
|
601
|
+
// Nothing was owed. Distinct from a miss: conflating the two is what made
|
|
602
|
+
// a latched Cursor session undiagnosable from this log.
|
|
603
|
+
appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
|
|
567
604
|
}
|
|
568
605
|
else {
|
|
569
|
-
appendDebug(coordRoot, {
|
|
606
|
+
appendDebug(coordRoot, {
|
|
607
|
+
...debugBase,
|
|
608
|
+
skipped: "session-name-block-absent",
|
|
609
|
+
session_name_pending: name,
|
|
610
|
+
reply_bytes: text.length,
|
|
611
|
+
reply_leads_with_fence: /^\s*`{3,}/.test(text),
|
|
612
|
+
});
|
|
570
613
|
}
|
|
571
614
|
if (ledgerRoute.state === "blocked") {
|
|
572
615
|
appendDebug(coordRoot, {
|
|
@@ -638,6 +681,14 @@ async function main() {
|
|
|
638
681
|
hookHealthState.owner_id = owner.instance_id;
|
|
639
682
|
const sessionId = payload?.session_id ?? payload?.conversation_id ?? owner.instance_id;
|
|
640
683
|
const priorCoordination = readHeartbeat(coordRoot, owner.instance_id);
|
|
684
|
+
if (norm.event_type !== "session.ended" &&
|
|
685
|
+
coordEnv("WORKFLOW_CHILD") !== "1" &&
|
|
686
|
+
priorCoordination?.kind !== "subagent" &&
|
|
687
|
+
priorCoordination?.kind !== "transient" &&
|
|
688
|
+
!priorCoordination?.workflow_run_id &&
|
|
689
|
+
!priorCoordination?.name?.trim()) {
|
|
690
|
+
assignNameInProcess(coordRoot, owner.instance_id, "session");
|
|
691
|
+
}
|
|
641
692
|
const data = buildEventData(norm.event_type, {
|
|
642
693
|
coordRoot,
|
|
643
694
|
payload,
|
|
@@ -723,6 +774,14 @@ async function main() {
|
|
|
723
774
|
logError(coordRoot, error, { phase: "epoch-coordination-restore" });
|
|
724
775
|
}
|
|
725
776
|
}
|
|
777
|
+
if (norm.event_type !== "session.ended") {
|
|
778
|
+
try {
|
|
779
|
+
ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter);
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
logError(coordRoot, error, { phase: "session-display-name" });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
726
785
|
const v3EventId = v3Result && "event" in v3Result
|
|
727
786
|
? v3Result.event.event_id
|
|
728
787
|
: v3Result && "event_id" in v3Result
|
|
@@ -1177,6 +1236,11 @@ async function main() {
|
|
|
1177
1236
|
try {
|
|
1178
1237
|
const name = sessionNameDisplayPending(readLiveCoordinationRow(coordRoot, owner.instance_id));
|
|
1179
1238
|
if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
|
|
1239
|
+
// Record the title before asking for it. The suggestion can still
|
|
1240
|
+
// change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
|
|
1241
|
+
// rebuilt cache), and the agent must not be stranded for displaying
|
|
1242
|
+
// exactly what it was handed.
|
|
1243
|
+
stampSessionNameRequested(coordRoot, owner.instance_id, name);
|
|
1180
1244
|
const { emitContext } = await import("./adapter/output.js");
|
|
1181
1245
|
emitContext(adapter, "PostToolUse", sessionNameDisplayInstruction(name));
|
|
1182
1246
|
}
|
|
@@ -1241,21 +1305,7 @@ async function enforcePendingSessionNameDisplay(coordRoot, instanceId, adapter,
|
|
|
1241
1305
|
const command = extractBashCommand(payload?.tool_name, payload?.tool_input);
|
|
1242
1306
|
if (isSessionNameRemediationCommand(command, resolveBinName(coordRoot)))
|
|
1243
1307
|
return true;
|
|
1244
|
-
const inspection = adapter
|
|
1245
|
-
? {
|
|
1246
|
-
state: assistantTextStartsWithSessionNameBlock(payload.agent_message, name)
|
|
1247
|
-
? "present"
|
|
1248
|
-
: "unavailable",
|
|
1249
|
-
}
|
|
1250
|
-
: inspectSessionNameDisplayImmediately(
|
|
1251
|
-
// Codex hook payloads omit transcript_path on every event, which
|
|
1252
|
-
// left this inspection permanently unavailable and the latch never
|
|
1253
|
-
// stamped. Discover the rollout by session id; the scan only runs
|
|
1254
|
-
// while a name is pending, so the latch closes after one success.
|
|
1255
|
-
payload?.transcript_path ??
|
|
1256
|
-
(adapter === "codex"
|
|
1257
|
-
? discoverCodexSessionTranscript(payload?.session_id ?? instanceId)
|
|
1258
|
-
: undefined), name, assistantTextStartsWithSessionNameBlock);
|
|
1308
|
+
const inspection = inspectDisplayEvidence(instanceId, adapter, payload, coordination);
|
|
1259
1309
|
if (inspection.state === "present") {
|
|
1260
1310
|
stampSessionNameSeen(coordRoot, instanceId, name);
|
|
1261
1311
|
return true;
|
|
@@ -1268,10 +1318,53 @@ async function enforcePendingSessionNameDisplay(coordRoot, instanceId, adapter,
|
|
|
1268
1318
|
// readable transcript can still verify it.
|
|
1269
1319
|
return true;
|
|
1270
1320
|
}
|
|
1321
|
+
// Record the refusal. Without this the gate was the one enforcement path
|
|
1322
|
+
// that left no trace, so a session reporting "the gate keeps denying me"
|
|
1323
|
+
// could not be confirmed or refuted from the hook log.
|
|
1324
|
+
appendDebug(coordRoot, {
|
|
1325
|
+
ts: new Date().toISOString(),
|
|
1326
|
+
event_name: "pre-tool-use",
|
|
1327
|
+
adapter,
|
|
1328
|
+
effect: "session-name-display-denied",
|
|
1329
|
+
session_name_pending: name,
|
|
1330
|
+
tool_name: payload?.tool_name,
|
|
1331
|
+
});
|
|
1271
1332
|
const { emitDeny } = await import("./adapter/output.js");
|
|
1272
1333
|
emitDeny(adapter, sessionNameDisplayRecoveryInstruction(name, resolveBinName(coordRoot)));
|
|
1273
1334
|
return false;
|
|
1274
1335
|
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Evidence for the pending display, over every title whose display counts.
|
|
1338
|
+
*
|
|
1339
|
+
* Cursor supplies the current narration directly; Claude Code and Codex are
|
|
1340
|
+
* resolved from their JSONL transcripts. Absent is the only state that denies,
|
|
1341
|
+
* so a title is refused only when a readable surface positively shows some
|
|
1342
|
+
* other opening text for every accepted title.
|
|
1343
|
+
*/
|
|
1344
|
+
function inspectDisplayEvidence(instanceId, adapter, payload, coordination) {
|
|
1345
|
+
if (adapter === "cursor" && payload?.agent_message !== undefined) {
|
|
1346
|
+
return matchSessionNameDisplay(coordination, payload.agent_message)
|
|
1347
|
+
? { state: "present" }
|
|
1348
|
+
: { state: "unavailable", reason: "transcript_not_ready" };
|
|
1349
|
+
}
|
|
1350
|
+
// Codex hook payloads omit transcript_path on every event, which left this
|
|
1351
|
+
// inspection permanently unavailable and the latch never stamped. Discover
|
|
1352
|
+
// the rollout by session id; the scan only runs while a name is pending, so
|
|
1353
|
+
// the latch closes after one success.
|
|
1354
|
+
const transcriptPath = payload?.transcript_path ??
|
|
1355
|
+
(adapter === "codex"
|
|
1356
|
+
? discoverCodexSessionTranscript(payload?.session_id ?? instanceId)
|
|
1357
|
+
: undefined);
|
|
1358
|
+
let verdict = { state: "absent" };
|
|
1359
|
+
for (const candidate of sessionNameDisplayAcceptedNames(coordination)) {
|
|
1360
|
+
const inspection = inspectSessionNameDisplayImmediately(transcriptPath, candidate, assistantTextStartsWithSessionNameBlock);
|
|
1361
|
+
if (inspection.state === "present")
|
|
1362
|
+
return inspection;
|
|
1363
|
+
if (inspection.state === "unavailable")
|
|
1364
|
+
verdict = inspection;
|
|
1365
|
+
}
|
|
1366
|
+
return verdict;
|
|
1367
|
+
}
|
|
1275
1368
|
function generationBoundHeartbeat(heartbeat, generationId) {
|
|
1276
1369
|
return generationId && heartbeat?.v3_generation_id === generationId ? heartbeat : null;
|
|
1277
1370
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/transcript.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,2BAA2B,wBAAc,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAQhF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,MAAM,EAAE,MAAM,GACb,OAAO,CAiCT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,oBAAoB,EAAE,MAAM,GAAG,SAAS,GACvC,OAAO,CAKT;AAED,kFAAkF;AAClF,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAgB9F;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CACjD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GAC/D,OAAO,CAIT;AAED,MAAM,MAAM,4BAA4B,GACpC;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,GACpB;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,GACnB;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,oBAAoB,GAAG,sBAAsB,CAAA;CAAE,CAAC;AAEpF;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAClD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GAC/D,4BAA4B,
|
|
1
|
+
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/transcript.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,2BAA2B,wBAAc,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAQhF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,MAAM,EAAE,MAAM,GACb,OAAO,CAiCT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,oBAAoB,EAAE,MAAM,GAAG,SAAS,GACvC,OAAO,CAKT;AAED,kFAAkF;AAClF,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAgB9F;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CACjD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GAC/D,OAAO,CAIT;AAED,MAAM,MAAM,4BAA4B,GACpC;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,GACpB;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,GACnB;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,oBAAoB,GAAG,sBAAsB,CAAA;CAAE,CAAC;AAEpF;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAClD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GAC/D,4BAA4B,CAiC9B;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAUrF;AAwDD;4CAC4C;AAC5C,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd;+EAC2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,qBAAqB,GAAG,SAAS,CA8BnC;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAkDpB"}
|
|
@@ -154,9 +154,16 @@ export function inspectSessionNameDisplayImmediately(transcriptPath, name, start
|
|
|
154
154
|
awaitingReply = false;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
// Some adapter transcripts persist assistant messages and tool requests but
|
|
158
|
+
// omit tool-result rows entirely. A readable file with no mint result cannot
|
|
159
|
+
// establish the ordered verification boundary, so it is unavailable evidence
|
|
160
|
+
// rather than proof that the agent displayed the title incorrectly. Only a
|
|
161
|
+
// transcript that contains the mint followed by a non-matching assistant
|
|
162
|
+
// reply can safely deny the next tool.
|
|
163
|
+
if (!sawMintResult || awaitingReply) {
|
|
164
|
+
return { state: "unavailable", reason: "transcript_not_ready" };
|
|
165
|
+
}
|
|
166
|
+
return { state: "absent" };
|
|
160
167
|
}
|
|
161
168
|
/**
|
|
162
169
|
* Resolve a transcript path at the adapter boundary. Windows-native Codex can
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-name-presence.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/session-name-presence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;
|
|
1
|
+
{"version":3,"file":"session-name-presence.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/session-name-presence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,MAAM,WAAW,mBAAmB;IAClC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,iBAAiB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAC3C,mBAAmB,CAkBrB"}
|