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
package/src/core/hooks/cli.ts
CHANGED
|
@@ -38,13 +38,20 @@ import { evaluateStopHook, STOP_REMEDIATION_MARKER } from "../agents/rules/stop-
|
|
|
38
38
|
import {
|
|
39
39
|
assistantTextStartsWithSessionNameBlock,
|
|
40
40
|
isSessionNameRemediationCommand,
|
|
41
|
+
matchSessionNameDisplay,
|
|
42
|
+
sessionNameDisplayAcceptedNames,
|
|
41
43
|
sessionNameDisplayInstruction,
|
|
42
44
|
sessionNameDisplayPending,
|
|
43
45
|
sessionNameDisplayRecoveryInstruction,
|
|
44
46
|
toolResponseMintedSessionName,
|
|
45
47
|
} from "../agents/session-name-display.ts";
|
|
46
48
|
import type { Heartbeat } from "../agents/state/heartbeat-reader.ts";
|
|
47
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
readHeartbeat,
|
|
51
|
+
setAssignedNameCache,
|
|
52
|
+
stampSessionNameRequested,
|
|
53
|
+
stampSessionNameSeen,
|
|
54
|
+
} from "../agents/state/heartbeat-writer.ts";
|
|
48
55
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
|
|
49
56
|
import { ensureLiveCoordinationHeartbeat } from "../agents/state/live-coordination-writer.ts";
|
|
50
57
|
import { assignName } from "../agents/state/names.ts";
|
|
@@ -113,6 +120,7 @@ import {
|
|
|
113
120
|
import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/anchor.ts";
|
|
114
121
|
import { extractIntentComment, resolveIntent } from "./resolve/intent.ts";
|
|
115
122
|
import { resolveOwner } from "./resolve/owner.ts";
|
|
123
|
+
import type { SessionNameDisplayInspection } from "./resolve/transcript.ts";
|
|
116
124
|
import {
|
|
117
125
|
inspectSessionNameDisplayImmediately,
|
|
118
126
|
scanAssistantStatusBoxPresent,
|
|
@@ -245,6 +253,31 @@ function assignNameInProcess(
|
|
|
245
253
|
}
|
|
246
254
|
}
|
|
247
255
|
|
|
256
|
+
/** Mid-flight sessions can emit tools before SessionStart assigns a pool name.
|
|
257
|
+
* Stamp the disposable cache so Codec does not render the 8-character id. */
|
|
258
|
+
function ensureSessionDisplayName(
|
|
259
|
+
coordRoot: string,
|
|
260
|
+
instanceId: string,
|
|
261
|
+
sessionId: string,
|
|
262
|
+
adapter: Adapter,
|
|
263
|
+
): void {
|
|
264
|
+
if (coordEnv("WORKFLOW_CHILD") === "1") return;
|
|
265
|
+
const row =
|
|
266
|
+
readLiveCoordinationRow(coordRoot, instanceId) ?? readHeartbeat(coordRoot, instanceId);
|
|
267
|
+
if (row?.kind === "subagent" || row?.kind === "transient" || row?.workflow_run_id) return;
|
|
268
|
+
if (row?.name?.trim()) return;
|
|
269
|
+
const assigned = assignNameInProcess(coordRoot, instanceId, "session");
|
|
270
|
+
if (!assigned?.name) return;
|
|
271
|
+
if (!readHeartbeat(coordRoot, instanceId)) {
|
|
272
|
+
try {
|
|
273
|
+
ensureLiveCoordinationHeartbeat(coordRoot, instanceId, sessionId, adapter);
|
|
274
|
+
} catch {
|
|
275
|
+
/* cache materialization is best-effort; history still holds the name */
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
setAssignedNameCache(coordRoot, instanceId, assigned.name, "session");
|
|
279
|
+
}
|
|
280
|
+
|
|
248
281
|
/**
|
|
249
282
|
* Direct (in-process) pidmap write, avoiding the spawn overhead of going via
|
|
250
283
|
* the agent-coord CLI for every session.started signal. Pid-map rows
|
|
@@ -376,13 +409,11 @@ function buildEventData(
|
|
|
376
409
|
};
|
|
377
410
|
|
|
378
411
|
case "turn.started": {
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
!readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name
|
|
385
|
-
) {
|
|
412
|
+
// Mid-flight onboarding (any adapter) can observe a turn before
|
|
413
|
+
// SessionStart. Mint durable display identity here so a later
|
|
414
|
+
// SessionStart remains idempotent and Codec is not stuck on the
|
|
415
|
+
// 8-character native id.
|
|
416
|
+
if (!readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name) {
|
|
386
417
|
assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session");
|
|
387
418
|
}
|
|
388
419
|
const prompt = p?.prompt ?? "";
|
|
@@ -703,13 +734,32 @@ async function main(): Promise<number> {
|
|
|
703
734
|
appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
|
|
704
735
|
return 0;
|
|
705
736
|
}
|
|
706
|
-
const
|
|
737
|
+
const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
|
|
738
|
+
const name = sessionNameDisplayPending(row);
|
|
707
739
|
const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
740
|
+
const sighting = matchSessionNameDisplay(row, text);
|
|
741
|
+
if (sighting) {
|
|
742
|
+
stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
|
|
743
|
+
appendDebug(coordRoot, {
|
|
744
|
+
...debugBase,
|
|
745
|
+
effect: "session-name-display-stamped",
|
|
746
|
+
session_name_pending: sighting.pending,
|
|
747
|
+
...(sighting.displayed === sighting.pending
|
|
748
|
+
? {}
|
|
749
|
+
: { session_name_displayed: sighting.displayed, session_name_drift: true }),
|
|
750
|
+
});
|
|
751
|
+
} else if (!name) {
|
|
752
|
+
// Nothing was owed. Distinct from a miss: conflating the two is what made
|
|
753
|
+
// a latched Cursor session undiagnosable from this log.
|
|
754
|
+
appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
|
|
711
755
|
} else {
|
|
712
|
-
appendDebug(coordRoot, {
|
|
756
|
+
appendDebug(coordRoot, {
|
|
757
|
+
...debugBase,
|
|
758
|
+
skipped: "session-name-block-absent",
|
|
759
|
+
session_name_pending: name,
|
|
760
|
+
reply_bytes: text.length,
|
|
761
|
+
reply_leads_with_fence: /^\s*`{3,}/.test(text),
|
|
762
|
+
});
|
|
713
763
|
}
|
|
714
764
|
if (ledgerRoute.state === "blocked") {
|
|
715
765
|
appendDebug(coordRoot, {
|
|
@@ -784,6 +834,16 @@ async function main(): Promise<number> {
|
|
|
784
834
|
|
|
785
835
|
const sessionId = payload?.session_id ?? payload?.conversation_id ?? owner.instance_id;
|
|
786
836
|
const priorCoordination = readHeartbeat(coordRoot, owner.instance_id);
|
|
837
|
+
if (
|
|
838
|
+
norm.event_type !== "session.ended" &&
|
|
839
|
+
coordEnv("WORKFLOW_CHILD") !== "1" &&
|
|
840
|
+
priorCoordination?.kind !== "subagent" &&
|
|
841
|
+
priorCoordination?.kind !== "transient" &&
|
|
842
|
+
!priorCoordination?.workflow_run_id &&
|
|
843
|
+
!priorCoordination?.name?.trim()
|
|
844
|
+
) {
|
|
845
|
+
assignNameInProcess(coordRoot, owner.instance_id, "session");
|
|
846
|
+
}
|
|
787
847
|
|
|
788
848
|
const data = buildEventData(norm.event_type, {
|
|
789
849
|
coordRoot,
|
|
@@ -879,6 +939,13 @@ async function main(): Promise<number> {
|
|
|
879
939
|
logError(coordRoot, error, { phase: "epoch-coordination-restore" });
|
|
880
940
|
}
|
|
881
941
|
}
|
|
942
|
+
if (norm.event_type !== "session.ended") {
|
|
943
|
+
try {
|
|
944
|
+
ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter);
|
|
945
|
+
} catch (error) {
|
|
946
|
+
logError(coordRoot, error, { phase: "session-display-name" });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
882
949
|
const v3EventId =
|
|
883
950
|
v3Result && "event" in v3Result
|
|
884
951
|
? v3Result.event.event_id
|
|
@@ -1364,6 +1431,11 @@ async function main(): Promise<number> {
|
|
|
1364
1431
|
try {
|
|
1365
1432
|
const name = sessionNameDisplayPending(readLiveCoordinationRow(coordRoot, owner.instance_id));
|
|
1366
1433
|
if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
|
|
1434
|
+
// Record the title before asking for it. The suggestion can still
|
|
1435
|
+
// change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
|
|
1436
|
+
// rebuilt cache), and the agent must not be stranded for displaying
|
|
1437
|
+
// exactly what it was handed.
|
|
1438
|
+
stampSessionNameRequested(coordRoot, owner.instance_id, name);
|
|
1367
1439
|
const { emitContext } = await import("./adapter/output.ts");
|
|
1368
1440
|
emitContext(adapter, "PostToolUse", sessionNameDisplayInstruction(name));
|
|
1369
1441
|
}
|
|
@@ -1440,25 +1512,7 @@ async function enforcePendingSessionNameDisplay(
|
|
|
1440
1512
|
const command = extractBashCommand(payload?.tool_name, payload?.tool_input);
|
|
1441
1513
|
if (isSessionNameRemediationCommand(command, resolveBinName(coordRoot))) return true;
|
|
1442
1514
|
|
|
1443
|
-
const inspection =
|
|
1444
|
-
adapter === "cursor" && payload?.agent_message !== undefined
|
|
1445
|
-
? {
|
|
1446
|
-
state: assistantTextStartsWithSessionNameBlock(payload.agent_message, name)
|
|
1447
|
-
? ("present" as const)
|
|
1448
|
-
: ("unavailable" as const),
|
|
1449
|
-
}
|
|
1450
|
-
: inspectSessionNameDisplayImmediately(
|
|
1451
|
-
// Codex hook payloads omit transcript_path on every event, which
|
|
1452
|
-
// left this inspection permanently unavailable and the latch never
|
|
1453
|
-
// stamped. Discover the rollout by session id; the scan only runs
|
|
1454
|
-
// while a name is pending, so the latch closes after one success.
|
|
1455
|
-
payload?.transcript_path ??
|
|
1456
|
-
(adapter === "codex"
|
|
1457
|
-
? discoverCodexSessionTranscript(payload?.session_id ?? instanceId)
|
|
1458
|
-
: undefined),
|
|
1459
|
-
name,
|
|
1460
|
-
assistantTextStartsWithSessionNameBlock,
|
|
1461
|
-
);
|
|
1515
|
+
const inspection = inspectDisplayEvidence(instanceId, adapter, payload, coordination);
|
|
1462
1516
|
if (inspection.state === "present") {
|
|
1463
1517
|
stampSessionNameSeen(coordRoot, instanceId, name);
|
|
1464
1518
|
return true;
|
|
@@ -1473,11 +1527,65 @@ async function enforcePendingSessionNameDisplay(
|
|
|
1473
1527
|
return true;
|
|
1474
1528
|
}
|
|
1475
1529
|
|
|
1530
|
+
// Record the refusal. Without this the gate was the one enforcement path
|
|
1531
|
+
// that left no trace, so a session reporting "the gate keeps denying me"
|
|
1532
|
+
// could not be confirmed or refuted from the hook log.
|
|
1533
|
+
appendDebug(coordRoot, {
|
|
1534
|
+
ts: new Date().toISOString(),
|
|
1535
|
+
event_name: "pre-tool-use",
|
|
1536
|
+
adapter,
|
|
1537
|
+
effect: "session-name-display-denied",
|
|
1538
|
+
session_name_pending: name,
|
|
1539
|
+
tool_name: payload?.tool_name,
|
|
1540
|
+
});
|
|
1476
1541
|
const { emitDeny } = await import("./adapter/output.ts");
|
|
1477
1542
|
emitDeny(adapter, sessionNameDisplayRecoveryInstruction(name, resolveBinName(coordRoot)));
|
|
1478
1543
|
return false;
|
|
1479
1544
|
}
|
|
1480
1545
|
|
|
1546
|
+
/**
|
|
1547
|
+
* Evidence for the pending display, over every title whose display counts.
|
|
1548
|
+
*
|
|
1549
|
+
* Cursor supplies the current narration directly; Claude Code and Codex are
|
|
1550
|
+
* resolved from their JSONL transcripts. Absent is the only state that denies,
|
|
1551
|
+
* so a title is refused only when a readable surface positively shows some
|
|
1552
|
+
* other opening text for every accepted title.
|
|
1553
|
+
*/
|
|
1554
|
+
function inspectDisplayEvidence(
|
|
1555
|
+
instanceId: string,
|
|
1556
|
+
adapter: Adapter,
|
|
1557
|
+
payload: ParsedPayload | null,
|
|
1558
|
+
coordination: Heartbeat | null,
|
|
1559
|
+
): SessionNameDisplayInspection {
|
|
1560
|
+
if (adapter === "cursor" && payload?.agent_message !== undefined) {
|
|
1561
|
+
return matchSessionNameDisplay(coordination, payload.agent_message)
|
|
1562
|
+
? { state: "present" }
|
|
1563
|
+
: { state: "unavailable", reason: "transcript_not_ready" };
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// Codex hook payloads omit transcript_path on every event, which left this
|
|
1567
|
+
// inspection permanently unavailable and the latch never stamped. Discover
|
|
1568
|
+
// the rollout by session id; the scan only runs while a name is pending, so
|
|
1569
|
+
// the latch closes after one success.
|
|
1570
|
+
const transcriptPath =
|
|
1571
|
+
payload?.transcript_path ??
|
|
1572
|
+
(adapter === "codex"
|
|
1573
|
+
? discoverCodexSessionTranscript(payload?.session_id ?? instanceId)
|
|
1574
|
+
: undefined);
|
|
1575
|
+
|
|
1576
|
+
let verdict: SessionNameDisplayInspection = { state: "absent" };
|
|
1577
|
+
for (const candidate of sessionNameDisplayAcceptedNames(coordination)) {
|
|
1578
|
+
const inspection = inspectSessionNameDisplayImmediately(
|
|
1579
|
+
transcriptPath,
|
|
1580
|
+
candidate,
|
|
1581
|
+
assistantTextStartsWithSessionNameBlock,
|
|
1582
|
+
);
|
|
1583
|
+
if (inspection.state === "present") return inspection;
|
|
1584
|
+
if (inspection.state === "unavailable") verdict = inspection;
|
|
1585
|
+
}
|
|
1586
|
+
return verdict;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1481
1589
|
function generationBoundHeartbeat(
|
|
1482
1590
|
heartbeat: Heartbeat | null,
|
|
1483
1591
|
generationId: `gen_${string}` | undefined,
|
|
@@ -167,9 +167,16 @@ export function inspectSessionNameDisplayImmediately(
|
|
|
167
167
|
awaitingReply = false;
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
// Some adapter transcripts persist assistant messages and tool requests but
|
|
171
|
+
// omit tool-result rows entirely. A readable file with no mint result cannot
|
|
172
|
+
// establish the ordered verification boundary, so it is unavailable evidence
|
|
173
|
+
// rather than proof that the agent displayed the title incorrectly. Only a
|
|
174
|
+
// transcript that contains the mint followed by a non-matching assistant
|
|
175
|
+
// reply can safely deny the next tool.
|
|
176
|
+
if (!sawMintResult || awaitingReply) {
|
|
177
|
+
return { state: "unavailable", reason: "transcript_not_ready" };
|
|
178
|
+
}
|
|
179
|
+
return { state: "absent" };
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
/**
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* Never throws: coordination telemetry must not take down a hook.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { sessionNameDisplayAcceptedNames } from "../agents/session-name-display.ts";
|
|
25
26
|
import { stampSessionNameSeen } from "../agents/state/heartbeat-writer.ts";
|
|
26
27
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
|
|
27
28
|
|
|
@@ -42,7 +43,11 @@ export function sessionNamePresence(
|
|
|
42
43
|
if (row?.session_name_seen_for === name) {
|
|
43
44
|
return { session_name_present: true, session_name_present_for: name };
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
// A display of the title Harnery last asked for counts here too, so Stop
|
|
47
|
+
// and the PreToolUse gate cannot disagree about the same reply.
|
|
48
|
+
const present = sessionNameDisplayAcceptedNames(row).some((candidate) =>
|
|
49
|
+
scanAssistantText(candidate),
|
|
50
|
+
);
|
|
46
51
|
if (present) stampSessionNameSeen(coordRoot, instanceId, name);
|
|
47
52
|
return { session_name_present: present, session_name_present_for: name };
|
|
48
53
|
} catch {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed output workspaces for page-QA commands.
|
|
3
|
+
*
|
|
4
|
+
* An implicit qa-run/qa-record output is working evidence, not project source.
|
|
5
|
+
* Keep it in the bounded artifact store. Callers that pass --out-dir retain
|
|
6
|
+
* full control of that explicit destination.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
import type { HarneryProgramContext } from "../commander.ts";
|
|
12
|
+
import { monorepoRoot } from "./agents/index.ts";
|
|
13
|
+
import {
|
|
14
|
+
ARTIFACT_MANIFEST,
|
|
15
|
+
type ArtifactManifestV2,
|
|
16
|
+
artifactsRoot,
|
|
17
|
+
configuredArtifactRetentionDays,
|
|
18
|
+
createArtifact,
|
|
19
|
+
parseArtifactManifest,
|
|
20
|
+
} from "./artifacts/index.ts";
|
|
21
|
+
|
|
22
|
+
export type QaArtifactKind = "qa-run" | "qa-record" | "review-pack";
|
|
23
|
+
|
|
24
|
+
export function resolveQaRepoRoot(context?: HarneryProgramContext): string {
|
|
25
|
+
return resolve(context?.repoRoot ?? monorepoRoot() ?? process.cwd());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createManagedQaOutParent(
|
|
29
|
+
repoRoot: string,
|
|
30
|
+
kind: QaArtifactKind,
|
|
31
|
+
opts: { retentionMinutes?: number } = {},
|
|
32
|
+
): string {
|
|
33
|
+
return createArtifact(repoRoot, {
|
|
34
|
+
slug: kind,
|
|
35
|
+
purpose:
|
|
36
|
+
kind === "qa-run"
|
|
37
|
+
? "Page QA run evidence"
|
|
38
|
+
: kind === "review-pack"
|
|
39
|
+
? "Page review pack"
|
|
40
|
+
: "Hand-recorded page QA evidence",
|
|
41
|
+
retentionDays: configuredArtifactRetentionDays(repoRoot),
|
|
42
|
+
// A standalone review-pack workspace IS the pack, so the artifact store's
|
|
43
|
+
// own expiry follows the pack's (90 minutes by default, ruled 2026-09-02).
|
|
44
|
+
...(opts.retentionMinutes !== undefined ? { retentionMinutes: opts.retentionMinutes } : {}),
|
|
45
|
+
}).path;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find the newest run in a valid managed QA workspace. This keeps no-path
|
|
50
|
+
* `qa-status` useful even though every implicit invocation gets an isolated
|
|
51
|
+
* artifact workspace instead of sharing one root-level `.qa-run` directory.
|
|
52
|
+
*/
|
|
53
|
+
export function latestManagedQaRun(repoRoot: string): string | null {
|
|
54
|
+
const root = artifactsRoot(repoRoot);
|
|
55
|
+
if (!existsSync(root)) return null;
|
|
56
|
+
|
|
57
|
+
let workspaceNames: string[];
|
|
58
|
+
try {
|
|
59
|
+
workspaceNames = readdirSync(root).sort();
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let latest: { path: string; startedAtMs: number } | null = null;
|
|
65
|
+
for (const workspaceName of workspaceNames) {
|
|
66
|
+
const workspace = join(root, workspaceName);
|
|
67
|
+
const manifest = readQaManifest(join(workspace, ARTIFACT_MANIFEST));
|
|
68
|
+
if (!manifest) continue;
|
|
69
|
+
|
|
70
|
+
let runNames: string[];
|
|
71
|
+
try {
|
|
72
|
+
runNames = readdirSync(workspace)
|
|
73
|
+
.filter((name) => name.startsWith("run-"))
|
|
74
|
+
.sort();
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const runName of runNames) {
|
|
79
|
+
const runDir = join(workspace, runName);
|
|
80
|
+
const startedAtMs = readStartedAtMs(runDir);
|
|
81
|
+
if (startedAtMs === null) continue;
|
|
82
|
+
if (
|
|
83
|
+
latest === null ||
|
|
84
|
+
startedAtMs > latest.startedAtMs ||
|
|
85
|
+
(startedAtMs === latest.startedAtMs && runDir > latest.path)
|
|
86
|
+
) {
|
|
87
|
+
latest = { path: runDir, startedAtMs };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return latest?.path ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readQaManifest(path: string): ArtifactManifestV2 | null {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = parseArtifactManifest(JSON.parse(readFileSync(path, "utf8")));
|
|
97
|
+
if (!parsed.ok) return null;
|
|
98
|
+
if (parsed.manifest.slug !== "qa-run" && parsed.manifest.slug !== "qa-record") return null;
|
|
99
|
+
return parsed.manifest;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readStartedAtMs(runDir: string): number | null {
|
|
106
|
+
for (const file of ["run-status.json", "page-qa-result.json"]) {
|
|
107
|
+
try {
|
|
108
|
+
const value = JSON.parse(readFileSync(join(runDir, file), "utf8")) as Record<string, unknown>;
|
|
109
|
+
const run = value.run;
|
|
110
|
+
const startedAt =
|
|
111
|
+
typeof value.started_at === "string"
|
|
112
|
+
? value.started_at
|
|
113
|
+
: run &&
|
|
114
|
+
typeof run === "object" &&
|
|
115
|
+
typeof (run as Record<string, unknown>).started_at === "string"
|
|
116
|
+
? ((run as Record<string, unknown>).started_at as string)
|
|
117
|
+
: null;
|
|
118
|
+
if (!startedAt) continue;
|
|
119
|
+
const parsed = Date.parse(startedAt);
|
|
120
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
121
|
+
} catch {
|
|
122
|
+
// Try the other authoritative run document.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
@@ -16,6 +16,7 @@ export interface ResourceProcessSample {
|
|
|
16
16
|
owner_kind: "agent" | "service" | "unattributed";
|
|
17
17
|
owner_id: string | null;
|
|
18
18
|
owner_root_pid: number | null;
|
|
19
|
+
owner_source?: "pid-map" | "session-environment" | "service";
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface ResourceProcessGroup {
|
|
@@ -90,6 +91,12 @@ export interface ResourceSamplerState {
|
|
|
90
91
|
cpu_total_ticks: number;
|
|
91
92
|
cpu_idle_ticks: number;
|
|
92
93
|
process_ticks: Map<string, number>;
|
|
94
|
+
process_owners: Map<string, ResourceProcessOwnerProof>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ResourceProcessOwnerProof {
|
|
98
|
+
session_id: string;
|
|
99
|
+
instance_id: string;
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
export interface ResourceSampleResult {
|
|
@@ -3,11 +3,13 @@ import { existsSync, readdirSync, readFileSync, readlinkSync } from "node:fs";
|
|
|
3
3
|
import { cpus, loadavg, platform, release } from "node:os";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
6
|
+
import { readLiveCoordinationRows } from "../agents/state/live-coordination-view.ts";
|
|
6
7
|
import { parsePidmapRow } from "../agents/state/pidmap.ts";
|
|
7
8
|
import { checkPidToken } from "../agents/state/proc-start.ts";
|
|
8
9
|
import {
|
|
9
10
|
RESOURCE_SNAPSHOT_SCHEMA_VERSION,
|
|
10
11
|
type ResourceProcessGroup,
|
|
12
|
+
type ResourceProcessOwnerProof,
|
|
11
13
|
type ResourceProcessSample,
|
|
12
14
|
type ResourceSampleResult,
|
|
13
15
|
type ResourceSamplerState,
|
|
@@ -44,11 +46,19 @@ interface ResourceSamplerOptions {
|
|
|
44
46
|
clockTicks?: number;
|
|
45
47
|
pageSize?: number;
|
|
46
48
|
services?: readonly { pid: number; id: string }[];
|
|
49
|
+
sessionOwners?: readonly SessionOwner[];
|
|
47
50
|
unattributedCpuFloor?: number;
|
|
48
51
|
unattributedRssFloor?: number;
|
|
49
52
|
maxProcesses?: number;
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
interface SessionOwner {
|
|
56
|
+
nativeSessionId: string;
|
|
57
|
+
instanceId: string;
|
|
58
|
+
platform: string;
|
|
59
|
+
kind: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
let cachedClockTicks: number | undefined;
|
|
53
63
|
let cachedPageSize: number | undefined;
|
|
54
64
|
|
|
@@ -86,7 +96,24 @@ export function sampleResources(
|
|
|
86
96
|
const cpu = parseLinuxCpu(readFileSync(join(procRoot, "stat"), "utf8"));
|
|
87
97
|
const memory = parseMeminfo(readFileSync(join(procRoot, "meminfo"), "utf8"));
|
|
88
98
|
const readings = readLinuxProcesses(procRoot, clockTicks, pageSize, uptimeSeconds);
|
|
89
|
-
const
|
|
99
|
+
const liveSessionOwners = uniqueLiveCodexSessionOwners(
|
|
100
|
+
options.sessionOwners ??
|
|
101
|
+
readLiveCoordinationRows(coordRoot).map((row) => ({
|
|
102
|
+
nativeSessionId: row.native_session_id ?? "",
|
|
103
|
+
instanceId: row.instance_id,
|
|
104
|
+
platform: row.platform ?? "",
|
|
105
|
+
kind: row.kind ?? "",
|
|
106
|
+
})),
|
|
107
|
+
);
|
|
108
|
+
const ownership = readOwnershipAnchors(
|
|
109
|
+
coordRoot,
|
|
110
|
+
procRoot,
|
|
111
|
+
readings,
|
|
112
|
+
options.services ?? [],
|
|
113
|
+
liveSessionOwners,
|
|
114
|
+
previous?.process_owners,
|
|
115
|
+
);
|
|
116
|
+
const anchors = ownership.anchors;
|
|
90
117
|
const byPid = new Map(readings.map((reading) => [reading.pid, reading]));
|
|
91
118
|
const processTicks = new Map<string, number>();
|
|
92
119
|
const intervalMs = previous ? Math.max(0, nowMs - previous.sampled_at_ms) : null;
|
|
@@ -116,6 +143,7 @@ export function sampleResources(
|
|
|
116
143
|
owner_kind: ownership?.kind ?? "unattributed",
|
|
117
144
|
owner_id: ownership?.id ?? null,
|
|
118
145
|
owner_root_pid: ownership?.rootPid ?? null,
|
|
146
|
+
...(ownership ? { owner_source: ownership.source } : {}),
|
|
119
147
|
};
|
|
120
148
|
});
|
|
121
149
|
const visible = processes
|
|
@@ -182,6 +210,7 @@ export function sampleResources(
|
|
|
182
210
|
cpu_total_ticks: cpu.total,
|
|
183
211
|
cpu_idle_ticks: cpu.idle,
|
|
184
212
|
process_ticks: processTicks,
|
|
213
|
+
process_owners: ownership.processOwners,
|
|
185
214
|
},
|
|
186
215
|
};
|
|
187
216
|
}
|
|
@@ -288,13 +317,20 @@ function readLinuxProcesses(
|
|
|
288
317
|
interface OwnershipAnchor {
|
|
289
318
|
kind: "agent" | "service";
|
|
290
319
|
id: string;
|
|
320
|
+
source: "pid-map" | "session-environment" | "service";
|
|
291
321
|
}
|
|
292
322
|
|
|
293
323
|
function readOwnershipAnchors(
|
|
294
324
|
coordRoot: string,
|
|
325
|
+
procRoot: string,
|
|
295
326
|
readings: readonly LinuxProcessReading[],
|
|
296
327
|
services: readonly { pid: number; id: string }[],
|
|
297
|
-
|
|
328
|
+
liveSessionOwners: ReadonlyMap<string, string>,
|
|
329
|
+
previousOwners?: ReadonlyMap<string, ResourceProcessOwnerProof>,
|
|
330
|
+
): {
|
|
331
|
+
anchors: Map<number, OwnershipAnchor>;
|
|
332
|
+
processOwners: Map<string, ResourceProcessOwnerProof>;
|
|
333
|
+
} {
|
|
298
334
|
const byPid = new Set(readings.map((row) => row.pid));
|
|
299
335
|
const directory = join(coordRoot, ".harnery", "pid-map");
|
|
300
336
|
const anchors = new Map<number, OwnershipAnchor>();
|
|
@@ -306,25 +342,121 @@ function readOwnershipAnchors(
|
|
|
306
342
|
try {
|
|
307
343
|
const row = parsePidmapRow(readFileSync(join(directory, entry), "utf8"));
|
|
308
344
|
if (!row.instanceId || checkPidToken(pid, row.startToken) === "mismatch") continue;
|
|
309
|
-
anchors.set(pid, { kind: "agent", id: row.instanceId });
|
|
345
|
+
anchors.set(pid, { kind: "agent", id: row.instanceId, source: "pid-map" });
|
|
310
346
|
} catch {
|
|
311
347
|
// An unreadable or racing row cannot prove ownership.
|
|
312
348
|
}
|
|
313
349
|
}
|
|
314
350
|
}
|
|
351
|
+
|
|
352
|
+
const processOwners = readBridgeProcessOwners(
|
|
353
|
+
procRoot,
|
|
354
|
+
readings,
|
|
355
|
+
liveSessionOwners,
|
|
356
|
+
previousOwners,
|
|
357
|
+
);
|
|
358
|
+
const byReadingPid = new Map(readings.map((row) => [row.pid, row]));
|
|
359
|
+
for (const reading of readings) {
|
|
360
|
+
const identity = `${reading.pid}:${reading.startTicks}`;
|
|
361
|
+
const owner = processOwners.get(identity);
|
|
362
|
+
if (!owner || anchors.has(reading.pid)) continue;
|
|
363
|
+
const parent = byReadingPid.get(reading.ppid);
|
|
364
|
+
const parentOwner = parent
|
|
365
|
+
? processOwners.get(`${parent.pid}:${parent.startTicks}`)
|
|
366
|
+
: undefined;
|
|
367
|
+
if (parentOwner?.instance_id === owner.instance_id) continue;
|
|
368
|
+
anchors.set(reading.pid, {
|
|
369
|
+
kind: "agent",
|
|
370
|
+
id: owner.instance_id,
|
|
371
|
+
source: "session-environment",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
315
375
|
for (const service of services) {
|
|
316
376
|
if (byPid.has(service.pid) && service.id.trim()) {
|
|
317
|
-
anchors.set(service.pid, { kind: "service", id: service.id.trim() });
|
|
377
|
+
anchors.set(service.pid, { kind: "service", id: service.id.trim(), source: "service" });
|
|
318
378
|
}
|
|
319
379
|
}
|
|
320
|
-
return anchors;
|
|
380
|
+
return { anchors, processOwners };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function uniqueLiveCodexSessionOwners(rows: readonly SessionOwner[]): Map<string, string> {
|
|
384
|
+
const candidates = new Map<string, Set<string>>();
|
|
385
|
+
for (const row of rows) {
|
|
386
|
+
const sessionId = row.nativeSessionId.trim();
|
|
387
|
+
const instanceId = row.instanceId.trim();
|
|
388
|
+
if (row.platform !== "codex" || row.kind !== "session" || !sessionId || !instanceId) continue;
|
|
389
|
+
const owners = candidates.get(sessionId) ?? new Set<string>();
|
|
390
|
+
owners.add(instanceId);
|
|
391
|
+
candidates.set(sessionId, owners);
|
|
392
|
+
}
|
|
393
|
+
const result = new Map<string, string>();
|
|
394
|
+
for (const [sessionId, owners] of candidates) {
|
|
395
|
+
if (owners.size === 1) result.set(sessionId, [...owners][0]!);
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function readBridgeProcessOwners(
|
|
401
|
+
procRoot: string,
|
|
402
|
+
readings: readonly LinuxProcessReading[],
|
|
403
|
+
liveSessionOwners: ReadonlyMap<string, string>,
|
|
404
|
+
previousOwners?: ReadonlyMap<string, ResourceProcessOwnerProof>,
|
|
405
|
+
): Map<string, ResourceProcessOwnerProof> {
|
|
406
|
+
const result = new Map<string, ResourceProcessOwnerProof>();
|
|
407
|
+
if (liveSessionOwners.size === 0) return result;
|
|
408
|
+
for (const reading of readings) {
|
|
409
|
+
const identity = `${reading.pid}:${reading.startTicks}`;
|
|
410
|
+
const previous = previousOwners?.get(identity);
|
|
411
|
+
if (previous && liveSessionOwners.get(previous.session_id) === previous.instance_id) {
|
|
412
|
+
result.set(identity, previous);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const sessionId = readCodexWslBridgeSessionId(procRoot, reading.pid);
|
|
416
|
+
if (!sessionId) continue;
|
|
417
|
+
const instanceId = liveSessionOwners.get(sessionId);
|
|
418
|
+
if (instanceId) result.set(identity, { session_id: sessionId, instance_id: instanceId });
|
|
419
|
+
}
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function readCodexWslBridgeSessionId(procRoot: string, pid: number): string | undefined {
|
|
424
|
+
try {
|
|
425
|
+
const entries = readFileSync(join(procRoot, String(pid), "environ"), "utf8").split("\0");
|
|
426
|
+
let bridge: string | undefined;
|
|
427
|
+
let platform: string | undefined;
|
|
428
|
+
let coordinationSessionId: string | undefined;
|
|
429
|
+
let codexThreadId: string | undefined;
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
if (entry.startsWith("HARNERY_AGENT_COORD_BRIDGE=")) {
|
|
432
|
+
bridge = entry.slice("HARNERY_AGENT_COORD_BRIDGE=".length);
|
|
433
|
+
} else if (entry.startsWith("HARNERY_AGENT_COORD_PLATFORM=")) {
|
|
434
|
+
platform = entry.slice("HARNERY_AGENT_COORD_PLATFORM=".length);
|
|
435
|
+
} else if (entry.startsWith("HARNERY_AGENT_COORD_SESSION_ID=")) {
|
|
436
|
+
coordinationSessionId = entry.slice("HARNERY_AGENT_COORD_SESSION_ID=".length);
|
|
437
|
+
} else if (entry.startsWith("CODEX_THREAD_ID=")) {
|
|
438
|
+
codexThreadId = entry.slice("CODEX_THREAD_ID=".length);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (
|
|
442
|
+
bridge === "codex-wsl" &&
|
|
443
|
+
platform === "codex" &&
|
|
444
|
+
coordinationSessionId &&
|
|
445
|
+
coordinationSessionId === codexThreadId
|
|
446
|
+
) {
|
|
447
|
+
return coordinationSessionId;
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
// An unreadable or racing environment cannot prove ownership.
|
|
451
|
+
}
|
|
452
|
+
return undefined;
|
|
321
453
|
}
|
|
322
454
|
|
|
323
455
|
function resolveOwnership(
|
|
324
456
|
pid: number,
|
|
325
457
|
byPid: ReadonlyMap<number, LinuxProcessReading>,
|
|
326
458
|
anchors: ReadonlyMap<number, OwnershipAnchor>,
|
|
327
|
-
):
|
|
459
|
+
): (OwnershipAnchor & { rootPid: number }) | undefined {
|
|
328
460
|
const seen = new Set<number>();
|
|
329
461
|
let cursor = pid;
|
|
330
462
|
for (let hops = 0; hops < 64 && cursor > 0 && !seen.has(cursor); hops++) {
|