harnery 0.45.2 → 0.46.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 +13 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commands/admission.d.ts.map +1 -1
- package/dist/commands/admission.js +214 -27
- package/dist/commands/agents.d.ts +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +102 -25
- package/dist/commands/browse.d.ts +3 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +172 -8
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +8 -2
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +40 -0
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +4 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +33 -6
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -4
- package/dist/core/agents/render/session-context.d.ts +0 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +1 -6
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -4
- package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +6 -2
- package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
- package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference-contract.js +1 -0
- package/dist/core/agents/state/parent-reference.d.ts +18 -0
- package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference.js +119 -0
- package/dist/core/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- package/dist/core/diagnostics/path-identity.d.ts +1 -1
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
- package/dist/core/diagnostics/path-identity.js +51 -6
- package/dist/core/events/v3/canonical.js +43 -14
- package/dist/core/events/v3/index.d.ts +1 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -1
- package/dist/core/events/v3/producers/recorder.d.ts +34 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +117 -13
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +53 -13
- package/dist/core/events/v3/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
- package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
- package/dist/core/hooks/adapter/native-parent.js +91 -0
- package/dist/core/hooks/cli.js +79 -4
- package/dist/core/hooks/resolve/owner.d.ts +0 -11
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +0 -23
- package/dist/core/hooks/shell-admission.d.ts +84 -0
- package/dist/core/hooks/shell-admission.d.ts.map +1 -0
- package/dist/core/hooks/shell-admission.js +622 -0
- package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
- package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
- package/dist/core/hooks/shell-commit-tree.js +104 -0
- package/dist/core/inbox/contract.d.ts +12 -0
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/contract.js +16 -0
- package/dist/core/inbox/peer-messages.d.ts +41 -16
- package/dist/core/inbox/peer-messages.d.ts.map +1 -1
- package/dist/core/inbox/peer-messages.js +372 -15
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +83 -3
- package/dist/core/inbox/targets.d.ts +17 -3
- package/dist/core/inbox/targets.d.ts.map +1 -1
- package/dist/core/inbox/targets.js +77 -2
- package/dist/core/presence/git.d.ts +18 -12
- package/dist/core/presence/git.d.ts.map +1 -1
- package/dist/core/presence/git.js +32 -38
- package/dist/core/presence/index.d.ts +2 -2
- package/dist/core/presence/index.d.ts.map +1 -1
- package/dist/core/presence/index.js +8 -6
- package/dist/core/presence/mail.d.ts +133 -0
- package/dist/core/presence/mail.d.ts.map +1 -0
- package/dist/core/presence/mail.js +464 -0
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/storage/modes.d.ts +4 -0
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +4 -0
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +5 -4
- package/dist/lib/admission-history.d.ts +96 -0
- package/dist/lib/admission-history.d.ts.map +1 -0
- package/dist/lib/admission-history.js +196 -0
- package/dist/lib/admission.d.ts +56 -7
- package/dist/lib/admission.d.ts.map +1 -1
- package/dist/lib/admission.js +161 -19
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +100 -12
- package/dist/lib/browser/content-checks.d.ts +3 -1
- package/dist/lib/browser/content-checks.d.ts.map +1 -1
- package/dist/lib/browser/content-checks.js +10 -2
- package/dist/lib/browser/critique.d.ts +32 -0
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +72 -3
- package/dist/lib/browser/geometry.d.ts +2 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +61 -15
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -1
- package/dist/lib/browser/page-review-contracts.d.ts +6 -1
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
- package/dist/lib/browser/page-review-contracts.js +6 -1
- package/dist/lib/browser/page-review-judge.d.ts +3 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +41 -11
- package/dist/lib/browser/page-review-pack.d.ts +3 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +5 -2
- package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +11 -4
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +73 -16
- package/dist/lib/browser/runts.d.ts +4 -2
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +84 -33
- package/dist/lib/browser/windows-chrome.d.ts +14 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +42 -0
- package/dist/lib/browser/windows-login-sync.d.ts +110 -0
- package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
- package/dist/lib/browser/windows-login-sync.js +171 -0
- package/dist/lib/cookies/client.d.ts +30 -5
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +55 -10
- package/dist/lib/cookies/index.d.ts +1 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +1 -1
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
- package/dist/lib/docs-metadata-sync.js +41 -2
- package/dist/lib/durable-job.d.ts +8 -0
- package/dist/lib/durable-job.d.ts.map +1 -1
- package/dist/lib/durable-job.js +10 -0
- package/dist/lib/http/client.d.ts +9 -8
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +44 -2
- package/dist/lib/instructions/templates.js +4 -4
- package/package.json +2 -2
- package/src/commander.ts +13 -0
- package/src/commands/admission.ts +272 -29
- package/src/commands/agents.ts +132 -27
- package/src/commands/browse.ts +208 -6
- package/src/commands/fetch.ts +1 -0
- package/src/commands/ledger-v3.ts +8 -2
- package/src/commands/presence.ts +49 -0
- package/src/commands/qa-run.ts +4 -3
- package/src/commands/review-pack.ts +41 -5
- package/src/core/agents/index.ts +6 -0
- package/src/core/agents/live-authority-v3.ts +11 -4
- package/src/core/agents/render/session-context.ts +1 -7
- package/src/core/agents/session-events.ts +18 -3
- package/src/core/agents/state/heartbeat-reader.ts +3 -0
- package/src/core/agents/state/live-coordination-view.ts +13 -6
- package/src/core/agents/state/parent-reference-contract.ts +14 -0
- package/src/core/agents/state/parent-reference.ts +179 -0
- package/src/core/config.ts +85 -0
- package/src/core/diagnostics/path-identity.ts +52 -6
- package/src/core/events/v3/canonical.ts +46 -13
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/producers/recorder.ts +151 -13
- package/src/core/events/v3/reader.ts +55 -11
- package/src/core/events/v3/validate.ts +42 -1
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +83 -4
- package/src/core/hooks/resolve/owner.ts +0 -23
- package/src/core/hooks/shell-admission.ts +634 -0
- package/src/core/hooks/shell-commit-tree.ts +102 -0
- package/src/core/inbox/contract.ts +18 -0
- package/src/core/inbox/peer-messages.ts +501 -15
- package/src/core/inbox/service.ts +108 -3
- package/src/core/inbox/targets.ts +94 -3
- package/src/core/presence/git.ts +45 -37
- package/src/core/presence/index.ts +16 -8
- package/src/core/presence/mail.ts +620 -0
- package/src/core/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- package/src/lib/admission-history.ts +267 -0
- package/src/lib/admission.ts +217 -19
- package/src/lib/browser/client.ts +147 -35
- package/src/lib/browser/content-checks.ts +14 -3
- package/src/lib/browser/critique.ts +113 -3
- package/src/lib/browser/geometry.ts +63 -14
- package/src/lib/browser/index.ts +5 -0
- package/src/lib/browser/page-review-contracts.ts +6 -1
- package/src/lib/browser/page-review-judge.ts +58 -13
- package/src/lib/browser/page-review-pack.ts +13 -2
- package/src/lib/browser/qa-run-contracts.ts +30 -4
- package/src/lib/browser/qa-run.ts +78 -17
- package/src/lib/browser/runts.ts +85 -37
- package/src/lib/browser/windows-chrome.ts +50 -0
- package/src/lib/browser/windows-login-sync.ts +275 -0
- package/src/lib/cookies/client.ts +62 -10
- package/src/lib/cookies/index.ts +3 -0
- package/src/lib/docs-metadata-sync.ts +44 -2
- package/src/lib/durable-job.ts +15 -0
- package/src/lib/http/client.ts +63 -1
- package/src/lib/instructions/templates.ts +4 -4
|
@@ -125,7 +125,15 @@ function sessionHashForSignal(input, context) {
|
|
|
125
125
|
// single live producer authority instead of hashing the instance into a
|
|
126
126
|
// second state path. Ambiguous or absent matches keep the fail-closed fallback.
|
|
127
127
|
if (adapterBehavior(input.adapter).sessionIdentityOnStartOnly) {
|
|
128
|
-
const
|
|
128
|
+
const scan = scanHookProducerStateRecordsV3(input.coordRoot);
|
|
129
|
+
// This instance's own state is unreadable: fail closed rather than hash
|
|
130
|
+
// the instance into a second state path beside it.
|
|
131
|
+
if (scan.invalid.some((file) => file.adapter === input.adapter && file.instance_id === input.instance_id)) {
|
|
132
|
+
throw new Error("V3 producer state is invalid");
|
|
133
|
+
}
|
|
134
|
+
for (const file of scan.invalid)
|
|
135
|
+
recordInvalidProducerStateDiagnostic(input.coordRoot, file);
|
|
136
|
+
const matches = scan.records.filter(({ state }) => state.adapter === input.adapter && state.instance_id === input.instance_id);
|
|
129
137
|
if (matches.length === 1) {
|
|
130
138
|
const name = basename(matches[0].path);
|
|
131
139
|
if (/^hid_[a-f0-9]{64}\.json$/.test(name)) {
|
|
@@ -2559,14 +2567,23 @@ export function salvageOpenSpansV3(input) {
|
|
|
2559
2567
|
lease.release();
|
|
2560
2568
|
}
|
|
2561
2569
|
}
|
|
2562
|
-
|
|
2570
|
+
/**
|
|
2571
|
+
* Scan every producer-state file of the active epoch. A file the validator
|
|
2572
|
+
* refuses is reported in `invalid` instead of failing the scan, so one
|
|
2573
|
+
* session's bad state cannot take coordination away from every other session
|
|
2574
|
+
* in the checkout. An unsafe adapter directory still throws.
|
|
2575
|
+
*/
|
|
2576
|
+
export function scanHookProducerStateRecordsV3(coordRoot, options = {}) {
|
|
2563
2577
|
const control = readEventV3ControlState(coordRoot);
|
|
2564
|
-
if (control.state !== "candidate" && control.state !== "active")
|
|
2565
|
-
return [];
|
|
2578
|
+
if (control.state !== "candidate" && control.state !== "active") {
|
|
2579
|
+
return { records: [], invalid: [] };
|
|
2580
|
+
}
|
|
2581
|
+
const genesisId = control.genesis.event.payload.genesis_id;
|
|
2566
2582
|
const producerRoot = join(resolve(coordRoot), EVENT_V3_LEDGER_RELATIVE_ROOT, "private-producers");
|
|
2567
2583
|
if (!existsSync(producerRoot))
|
|
2568
|
-
return [];
|
|
2584
|
+
return { records: [], invalid: [] };
|
|
2569
2585
|
const records = [];
|
|
2586
|
+
const invalid = [];
|
|
2570
2587
|
for (const adapter of EVENT_ADAPTER_IDS_V3) {
|
|
2571
2588
|
const directory = join(producerRoot, adapter);
|
|
2572
2589
|
if (!existsSync(directory))
|
|
@@ -2577,18 +2594,79 @@ export function listHookProducerStateRecordsV3(coordRoot, options = {}) {
|
|
|
2577
2594
|
}
|
|
2578
2595
|
for (const name of readdirSync(directory).filter((entry) => /^hid_[a-f0-9]{64}\.json$/.test(entry))) {
|
|
2579
2596
|
const path = join(directory, name);
|
|
2580
|
-
|
|
2597
|
+
let state;
|
|
2598
|
+
try {
|
|
2599
|
+
state = readProducerState(path);
|
|
2600
|
+
}
|
|
2601
|
+
catch (error) {
|
|
2602
|
+
const raw = rawProducerStateIdentity(path);
|
|
2603
|
+
// A refused file from a replaced epoch would have been skipped anyway.
|
|
2604
|
+
if (raw.epoch_genesis_id !== undefined && raw.epoch_genesis_id !== genesisId)
|
|
2605
|
+
continue;
|
|
2606
|
+
invalid.push({
|
|
2607
|
+
path,
|
|
2608
|
+
adapter,
|
|
2609
|
+
session_hash: name.slice(0, -".json".length),
|
|
2610
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2611
|
+
...(raw.instance_id ? { instance_id: raw.instance_id } : {}),
|
|
2612
|
+
});
|
|
2613
|
+
continue;
|
|
2614
|
+
}
|
|
2581
2615
|
// Skip authorities left behind by a replaced epoch: their sequences and
|
|
2582
2616
|
// causal links belong to the archived ledger, not this one.
|
|
2583
|
-
if (state.epoch_genesis_id !==
|
|
2617
|
+
if (state.epoch_genesis_id !== genesisId)
|
|
2584
2618
|
continue;
|
|
2585
2619
|
if (!options.includeTerminal && state.terminal)
|
|
2586
2620
|
continue;
|
|
2587
2621
|
records.push({ path, modified_at_ms: lstatSync(path).mtimeMs, state });
|
|
2588
2622
|
}
|
|
2589
2623
|
}
|
|
2590
|
-
|
|
2624
|
+
records.sort((left, right) => left.state.generation_id.localeCompare(right.state.generation_id) ||
|
|
2591
2625
|
left.path.localeCompare(right.path));
|
|
2626
|
+
invalid.sort((left, right) => left.path.localeCompare(right.path));
|
|
2627
|
+
return { records, invalid };
|
|
2628
|
+
}
|
|
2629
|
+
/**
|
|
2630
|
+
* List the valid producer states of the active epoch. An invalid file is
|
|
2631
|
+
* skipped and recorded as a `producer_state_invalid_skipped` diagnostic that
|
|
2632
|
+
* names its adapter and session hash; readers of that one session's state
|
|
2633
|
+
* still fail closed through `readHookProducerStateV3`.
|
|
2634
|
+
*/
|
|
2635
|
+
export function listHookProducerStateRecordsV3(coordRoot, options = {}) {
|
|
2636
|
+
const scan = scanHookProducerStateRecordsV3(coordRoot, options);
|
|
2637
|
+
for (const file of scan.invalid)
|
|
2638
|
+
recordInvalidProducerStateDiagnostic(coordRoot, file);
|
|
2639
|
+
return scan.records;
|
|
2640
|
+
}
|
|
2641
|
+
function recordInvalidProducerStateDiagnostic(coordRoot, file) {
|
|
2642
|
+
writeProducerDiagnosticV3(coordRoot, "producer_state_invalid_skipped", {
|
|
2643
|
+
adapter: file.adapter,
|
|
2644
|
+
session_hash: file.session_hash,
|
|
2645
|
+
reason: file.reason,
|
|
2646
|
+
...(file.instance_id ? { instance_id: file.instance_id } : {}),
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
function rawProducerStateIdentity(path) {
|
|
2650
|
+
try {
|
|
2651
|
+
const metadata = lstatSync(path);
|
|
2652
|
+
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
2653
|
+
return {};
|
|
2654
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2655
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2656
|
+
return {};
|
|
2657
|
+
return {
|
|
2658
|
+
...(typeof parsed.instance_id === "string" &&
|
|
2659
|
+
/^inst_[a-zA-Z0-9._-]{1,128}$/.test(parsed.instance_id)
|
|
2660
|
+
? { instance_id: parsed.instance_id }
|
|
2661
|
+
: {}),
|
|
2662
|
+
...(typeof parsed.epoch_genesis_id === "string"
|
|
2663
|
+
? { epoch_genesis_id: parsed.epoch_genesis_id }
|
|
2664
|
+
: {}),
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
catch {
|
|
2668
|
+
return {};
|
|
2669
|
+
}
|
|
2592
2670
|
}
|
|
2593
2671
|
/**
|
|
2594
2672
|
* Re-anchor every live archived hook producer into a newly activated epoch.
|
|
@@ -3194,17 +3272,30 @@ function acquireStateLease(coordRoot, statePath) {
|
|
|
3194
3272
|
},
|
|
3195
3273
|
};
|
|
3196
3274
|
}
|
|
3275
|
+
/** Test seam: publish a producer state through the real write-side validator. */
|
|
3276
|
+
export function publishHookProducerStateForTestV3(coordRoot, path, state) {
|
|
3277
|
+
publishProducerState(coordRoot, path, state);
|
|
3278
|
+
}
|
|
3197
3279
|
function publishProducerState(coordRoot, path, state) {
|
|
3198
3280
|
const control = readEventV3ControlState(coordRoot);
|
|
3199
3281
|
if ((control.state === "candidate" || control.state === "active") &&
|
|
3200
3282
|
state.epoch_genesis_id !== control.genesis.event.payload.genesis_id) {
|
|
3201
3283
|
return;
|
|
3202
3284
|
}
|
|
3285
|
+
const serialized = JSON.stringify(state);
|
|
3286
|
+
// Validate the exact bytes about to land with the reader's own validator.
|
|
3287
|
+
// The previous file stays in place when this refuses.
|
|
3288
|
+
try {
|
|
3289
|
+
validateProducerStateDocument(JSON.parse(serialized));
|
|
3290
|
+
}
|
|
3291
|
+
catch {
|
|
3292
|
+
throw new Error("V3 producer state is invalid; refusing to publish");
|
|
3293
|
+
}
|
|
3203
3294
|
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
3204
3295
|
let fd;
|
|
3205
3296
|
try {
|
|
3206
3297
|
fd = openSync(temporary, "wx", stateFileMode());
|
|
3207
|
-
writeFileSync(fd, `${
|
|
3298
|
+
writeFileSync(fd, `${serialized}\n`, "utf8");
|
|
3208
3299
|
fsyncSync(fd);
|
|
3209
3300
|
closeSync(fd);
|
|
3210
3301
|
fd = undefined;
|
|
@@ -3233,6 +3324,15 @@ function readProducerState(path) {
|
|
|
3233
3324
|
catch {
|
|
3234
3325
|
throw new Error("V3 producer state is unreadable");
|
|
3235
3326
|
}
|
|
3327
|
+
return validateProducerStateDocument(parsed);
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* The single producer-state validator. Readers run it on every file they
|
|
3331
|
+
* load, and `publishProducerState` runs it on the serialized document before
|
|
3332
|
+
* the rename, so a state no reader would accept fails in the producing hook
|
|
3333
|
+
* instead of in every later reader of the checkout.
|
|
3334
|
+
*/
|
|
3335
|
+
function validateProducerStateDocument(parsed) {
|
|
3236
3336
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3237
3337
|
throw new Error("V3 producer state is invalid");
|
|
3238
3338
|
}
|
|
@@ -3476,8 +3576,10 @@ function validPendingRuntimeContext(value) {
|
|
|
3476
3576
|
Number.isFinite(Date.parse(pending.terminal_observed_at)) &&
|
|
3477
3577
|
typeof pending.turn_id === "string" &&
|
|
3478
3578
|
/^tid_[a-f0-9]{64}$/.test(pending.turn_id) &&
|
|
3479
|
-
|
|
3480
|
-
|
|
3579
|
+
// Same shapes the event contract accepts: workflow children carry
|
|
3580
|
+
// stableScopeId hashes (run_<64 hex>), not UUIDs.
|
|
3581
|
+
(pending.run_id === undefined || /^run_[a-zA-Z0-9._-]{1,128}$/.test(pending.run_id)) &&
|
|
3582
|
+
(pending.workflow_id === undefined || /^wf_[a-zA-Z0-9._-]{1,128}$/.test(pending.workflow_id)) &&
|
|
3481
3583
|
(pending.workflow_agent_id === undefined ||
|
|
3482
3584
|
/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(pending.workflow_agent_id)) &&
|
|
3483
3585
|
typeof pending.native_session_id === "string" &&
|
|
@@ -3493,9 +3595,11 @@ function validPendingRuntimeContext(value) {
|
|
|
3493
3595
|
(pending.runtime_version === undefined ||
|
|
3494
3596
|
(typeof pending.runtime_version === "string" &&
|
|
3495
3597
|
/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(pending.runtime_version))) &&
|
|
3598
|
+
// Externally driven reconciles (`deferRetryLimit`) keep counting past
|
|
3599
|
+
// RUNTIME_CONTEXT_RETRY_LIMIT until their own final attempt, and publish
|
|
3600
|
+
// those counts, so the reader must accept them.
|
|
3496
3601
|
Number.isSafeInteger(pending.attempts) &&
|
|
3497
|
-
pending.attempts >= 0
|
|
3498
|
-
pending.attempts < RUNTIME_CONTEXT_RETRY_LIMIT);
|
|
3602
|
+
pending.attempts >= 0);
|
|
3499
3603
|
}
|
|
3500
3604
|
function validActiveRuntimeContextProbe(value) {
|
|
3501
3605
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../../../src/core/events/v3/reader.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../../../src/core/events/v3/reader.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ import { closeSync, existsSync, lstatSync, openSync, readdirSync, readFileSync,
|
|
|
4
4
|
import { join, relative, resolve } from "node:path";
|
|
5
5
|
import { canonicalJsonV3, sha256V3 } from "./canonical.js";
|
|
6
6
|
import { readEventV3Catalog, readEventV3SegmentManifest, } from "./catalog.js";
|
|
7
|
+
import { readAdvanceableControlWitnessV3 } from "./control-witness.js";
|
|
7
8
|
import { EVENT_V3_SCHEMA_DIGEST } from "./generated.js";
|
|
8
9
|
import { eventV3RecoveryRecordsRoot, listEventV3RecoveryReceipts, } from "./recovery-record.js";
|
|
9
10
|
import { validateEventV3 } from "./validate.js";
|
|
@@ -204,7 +205,8 @@ export function readLedgerV3(coordRoot, options = {}) {
|
|
|
204
205
|
return resumed.result;
|
|
205
206
|
}
|
|
206
207
|
const discovered = discoverLedgerFramesV3(coordRoot);
|
|
207
|
-
const validated =
|
|
208
|
+
const validated = validateWitnessedFramesV3(coordRoot, storage, discovered, options) ??
|
|
209
|
+
validateLedgerFramesV3(discovered.frames, options);
|
|
208
210
|
const recovery = discoverRecoveryReceiptsV3(coordRoot);
|
|
209
211
|
const diagnostics = [
|
|
210
212
|
...discovered.diagnostics,
|
|
@@ -263,7 +265,35 @@ function discoverRecoveryReceiptsV3(coordRoot) {
|
|
|
263
265
|
export function readLedgerFramesV3(frames, options = {}) {
|
|
264
266
|
return validateLedgerFramesV3(frames, options).result;
|
|
265
267
|
}
|
|
266
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Cold-process fast path. Writers advance an authenticated validation
|
|
270
|
+
* checkpoint under the append lease after every drain, so when the witness
|
|
271
|
+
* names exactly the storage this read observed, each frame already passed the
|
|
272
|
+
* contract and canonical-byte checks that dominate a full read. Those two
|
|
273
|
+
* checks are skipped; every ledger-level rule still runs and rebuilds the
|
|
274
|
+
* validator state. The result is used only if that state reproduces the
|
|
275
|
+
* witnessed checkpoint byte for byte with no diagnostics and storage did not
|
|
276
|
+
* move during the read; anything else returns undefined and the caller runs
|
|
277
|
+
* the canonical full validation.
|
|
278
|
+
*/
|
|
279
|
+
function validateWitnessedFramesV3(coordRoot, storage, discovered, options) {
|
|
280
|
+
if (!discovered.active || discovered.diagnostics.length > 0)
|
|
281
|
+
return undefined;
|
|
282
|
+
const witness = readAdvanceableControlWitnessV3(coordRoot);
|
|
283
|
+
if (!witness || witness.payload.storage.fingerprint !== sha256V3(storage.fingerprint)) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
const trusted = validateLedgerFramesV3(discovered.frames, options, true);
|
|
287
|
+
if (trusted.state.diagnostics.length > 0)
|
|
288
|
+
return undefined;
|
|
289
|
+
const reproduced = checkpointFromValidationStateV3(trusted.state, discovered.active);
|
|
290
|
+
if (canonicalJsonV3(reproduced) !== canonicalJsonV3(witness.checkpoint))
|
|
291
|
+
return undefined;
|
|
292
|
+
if (ledgerStorageVersionV3(coordRoot).fingerprint !== storage.fingerprint)
|
|
293
|
+
return undefined;
|
|
294
|
+
return trusted;
|
|
295
|
+
}
|
|
296
|
+
function validateLedgerFramesV3(frames, options, witnessed = false) {
|
|
267
297
|
const state = {
|
|
268
298
|
diagnostics: [],
|
|
269
299
|
events: [],
|
|
@@ -276,10 +306,14 @@ function validateLedgerFramesV3(frames, options) {
|
|
|
276
306
|
clocks: new Map(),
|
|
277
307
|
event_count: 0,
|
|
278
308
|
};
|
|
279
|
-
validateLedgerFramesIntoStateV3(frames, state);
|
|
309
|
+
validateLedgerFramesIntoStateV3(frames, state, witnessed);
|
|
280
310
|
return { result: finishLedgerValidationV3(state, options, ledgerFramesBytesV3(frames)), state };
|
|
281
311
|
}
|
|
282
|
-
|
|
312
|
+
/**
|
|
313
|
+
* `witnessed` skips only the per-frame contract and canonical-byte checks, and
|
|
314
|
+
* only validateWitnessedFramesV3 may pass it, after authenticating the frames.
|
|
315
|
+
*/
|
|
316
|
+
function validateLedgerFramesIntoStateV3(frames, state, witnessed = false) {
|
|
283
317
|
state.first_position ??= frames[0]?.position;
|
|
284
318
|
const { diagnostics, events, advances, accepted, reader_build: readerBuild, seen_ids: seenIds, producer_sequences: producerSequences, attestations, clocks, } = state;
|
|
285
319
|
for (const frame of frames) {
|
|
@@ -308,17 +342,23 @@ function validateLedgerFramesIntoStateV3(frames, state) {
|
|
|
308
342
|
diagnostics.push(diagnostic("unsupported_major", frame));
|
|
309
343
|
continue;
|
|
310
344
|
}
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
continue;
|
|
345
|
+
let event;
|
|
346
|
+
if (witnessed) {
|
|
347
|
+
event = parsed;
|
|
315
348
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
349
|
+
else {
|
|
350
|
+
const validation = validateEventV3(parsed);
|
|
351
|
+
if (!validation.ok || !validation.event) {
|
|
352
|
+
diagnostics.push(diagnostic("invalid_contract", frame));
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
event = validation.event;
|
|
356
|
+
if (canonicalJsonV3(event) !== frame.raw) {
|
|
357
|
+
diagnostics.push(diagnostic("noncanonical_frame", frame, event.event_id));
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
321
360
|
}
|
|
361
|
+
const shape = event;
|
|
322
362
|
const priorFrame = seenIds.get(shape.event_id);
|
|
323
363
|
if (priorFrame !== undefined) {
|
|
324
364
|
if (priorFrame !== frame.raw) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/core/events/v3/validate.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAiB,MAAM,eAAe,CAAC;AAE5D,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/core/events/v3/validate.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAiB,MAAM,eAAe,CAAC;AAE5D,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAgFD,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CAkBvE;AAkSD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAKtE"}
|
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
import { Value } from "@sinclair/typebox/value";
|
|
2
2
|
import { canonicalJsonV3 } from "./canonical.js";
|
|
3
3
|
import { EventV3Schema } from "./contract.js";
|
|
4
|
+
/**
|
|
5
|
+
* Every union member requires its own literal `event_type`, so only the member
|
|
6
|
+
* named by a value's `event_type` can accept it. Checking that one member gives
|
|
7
|
+
* the union's verdict without trying the other variants, which more than halves
|
|
8
|
+
* a full-ledger read. Returns null, falling back to the whole union, if a member
|
|
9
|
+
* ever stops carrying a required unique literal.
|
|
10
|
+
*/
|
|
11
|
+
let eventVariantsByType;
|
|
12
|
+
function eventVariantsByTypeV3() {
|
|
13
|
+
if (eventVariantsByType !== undefined)
|
|
14
|
+
return eventVariantsByType;
|
|
15
|
+
const variants = new Map();
|
|
16
|
+
for (const variant of EventV3Schema.anyOf) {
|
|
17
|
+
const literal = variant.properties.event_type?.const;
|
|
18
|
+
if (typeof literal !== "string" ||
|
|
19
|
+
!variant.required?.includes("event_type") ||
|
|
20
|
+
variants.has(literal)) {
|
|
21
|
+
eventVariantsByType = null;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
variants.set(literal, variant);
|
|
25
|
+
}
|
|
26
|
+
eventVariantsByType = variants;
|
|
27
|
+
return variants;
|
|
28
|
+
}
|
|
29
|
+
function checkEventSchemaV3(value) {
|
|
30
|
+
const variants = eventVariantsByTypeV3();
|
|
31
|
+
if (!variants)
|
|
32
|
+
return Value.Check(EventV3Schema, value);
|
|
33
|
+
const eventType = typeof value === "object" && value !== null
|
|
34
|
+
? value.event_type
|
|
35
|
+
: undefined;
|
|
36
|
+
const variant = typeof eventType === "string" ? variants.get(eventType) : undefined;
|
|
37
|
+
return variant !== undefined && Value.Check(variant, value);
|
|
38
|
+
}
|
|
4
39
|
export function validateEventV3(value) {
|
|
5
|
-
if (!
|
|
40
|
+
if (!checkEventSchemaV3(value)) {
|
|
6
41
|
return {
|
|
7
42
|
ok: false,
|
|
8
43
|
issues: [...Value.Errors(EventV3Schema, value)]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type NativeParentObservation = {
|
|
2
|
+
state: "observed";
|
|
3
|
+
parent_session_id: string;
|
|
4
|
+
source_witness: string;
|
|
5
|
+
} | {
|
|
6
|
+
state: "none";
|
|
7
|
+
reason: string;
|
|
8
|
+
source_witness: string;
|
|
9
|
+
} | {
|
|
10
|
+
state: "unknown";
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
export interface NativeParentRequest {
|
|
14
|
+
adapter: string;
|
|
15
|
+
native_session_id?: string;
|
|
16
|
+
transcript_path?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface NativeParentReaderOptions {
|
|
19
|
+
discoverTranscript?: (sessionId: string, transcriptPath?: string) => string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Native relationship evidence only; no prompt bodies, state writes or recipient authority. */
|
|
22
|
+
export declare function readNativeParentReference(request: NativeParentRequest, options?: NativeParentReaderOptions): NativeParentObservation;
|
|
23
|
+
//# sourceMappingURL=native-parent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-parent.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/native-parent.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,uBAAuB,GAC/B;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,yBAAyB;IACxC,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CACzF;AAYD,gGAAgG;AAChG,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,yBAA8B,GACtC,uBAAuB,CAuDzB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { discoverCodexSessionTranscript } from "./runtime-telemetry.js";
|
|
4
|
+
const HEADER_LIMIT = 1024 * 1024;
|
|
5
|
+
const NATIVE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
const ORDINARY_SOURCES = new Set(["cli", "exec", "vscode", "app-server"]);
|
|
7
|
+
function object(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Native relationship evidence only; no prompt bodies, state writes or recipient authority. */
|
|
13
|
+
export function readNativeParentReference(request, options = {}) {
|
|
14
|
+
if (request.adapter !== "codex")
|
|
15
|
+
return unknown("native_parent_adapter_unsupported");
|
|
16
|
+
const child = request.native_session_id;
|
|
17
|
+
if (!child || !NATIVE_ID.test(child))
|
|
18
|
+
return unknown("native_child_identity_unavailable");
|
|
19
|
+
let fd;
|
|
20
|
+
try {
|
|
21
|
+
const path = (options.discoverTranscript ?? discoverCodexSessionTranscript)(child, request.transcript_path);
|
|
22
|
+
if (!path)
|
|
23
|
+
return unknown("native_parent_transcript_unavailable");
|
|
24
|
+
const entry = lstatSync(path);
|
|
25
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
26
|
+
return unknown("native_parent_not_regular");
|
|
27
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
28
|
+
const before = fstatSync(fd);
|
|
29
|
+
if (!before.isFile() || before.dev !== entry.dev || before.ino !== entry.ino) {
|
|
30
|
+
return unknown("native_parent_file_changed");
|
|
31
|
+
}
|
|
32
|
+
const header = readHeader(fd);
|
|
33
|
+
if (!header)
|
|
34
|
+
return unknown("native_parent_header_incomplete");
|
|
35
|
+
// Appending later rows is normal. The same header must remain unchanged.
|
|
36
|
+
const checked = readHeader(fd);
|
|
37
|
+
const after = lstatSync(path);
|
|
38
|
+
if (!checked?.equals(header) ||
|
|
39
|
+
!after.isFile() ||
|
|
40
|
+
after.isSymbolicLink() ||
|
|
41
|
+
before.dev !== after.dev ||
|
|
42
|
+
before.ino !== after.ino) {
|
|
43
|
+
return unknown("native_parent_file_changed");
|
|
44
|
+
}
|
|
45
|
+
const row = object(JSON.parse(header.toString("utf8")));
|
|
46
|
+
const payload = object(row?.payload);
|
|
47
|
+
if (row?.type !== "session_meta" || payload?.id !== child) {
|
|
48
|
+
return unknown("native_parent_child_mismatch");
|
|
49
|
+
}
|
|
50
|
+
const source_witness = createHash("sha256").update(header).digest("hex");
|
|
51
|
+
if (typeof payload.source === "string" && ORDINARY_SOURCES.has(payload.source)) {
|
|
52
|
+
return { state: "none", reason: "native_ordinary_session", source_witness };
|
|
53
|
+
}
|
|
54
|
+
const source = object(payload.source);
|
|
55
|
+
const spawn = object(object(source?.subagent)?.thread_spawn);
|
|
56
|
+
const parent = spawn?.parent_thread_id;
|
|
57
|
+
if (typeof parent !== "string" || !NATIVE_ID.test(parent)) {
|
|
58
|
+
return unknown("native_parent_not_reported");
|
|
59
|
+
}
|
|
60
|
+
if (parent.toLowerCase() === child.toLowerCase())
|
|
61
|
+
return unknown("native_parent_self_reference");
|
|
62
|
+
return { state: "observed", parent_session_id: parent, source_witness };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return unknown("native_parent_metadata_unreadable");
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
if (fd !== undefined)
|
|
69
|
+
closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function readHeader(fd) {
|
|
73
|
+
const chunks = [];
|
|
74
|
+
let offset = 0;
|
|
75
|
+
while (offset < HEADER_LIMIT) {
|
|
76
|
+
const buffer = Buffer.alloc(Math.min(16 * 1024, HEADER_LIMIT - offset));
|
|
77
|
+
const bytes = readSync(fd, buffer, 0, buffer.length, offset);
|
|
78
|
+
if (bytes === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
const data = buffer.subarray(0, bytes);
|
|
81
|
+
const newline = data.indexOf(10);
|
|
82
|
+
if (newline >= 0)
|
|
83
|
+
return Buffer.concat([...chunks, data.subarray(0, newline)]);
|
|
84
|
+
chunks.push(data);
|
|
85
|
+
offset += bytes;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
function unknown(reason) {
|
|
90
|
+
return { state: "unknown", reason };
|
|
91
|
+
}
|
package/dist/core/hooks/cli.js
CHANGED
|
@@ -32,12 +32,13 @@ import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.
|
|
|
32
32
|
import { ensureLiveCoordinationHeartbeat } from "../agents/state/live-coordination-writer.js";
|
|
33
33
|
import { assignName, readForkParent, readForkState, recordForkDetection, resolveName, } from "../agents/state/names.js";
|
|
34
34
|
import { writePidmapRow } from "../agents/state/pidmap.js";
|
|
35
|
-
import { agentsRequireGitFinalization, coordFreshnessSeconds, hostPromptContextConfig, resolveBinName, } from "../config.js";
|
|
35
|
+
import { agentsRequireGitFinalization, coordFreshnessSeconds, hostPromptContextConfig, readAdmissionConfig, resolveBinName, } from "../config.js";
|
|
36
36
|
import { checkpointContext, completeContextRecovery, extractContextSample, markContextCompactionCompleted, prepareContextRecovery, readContextState, recordContextSample, } from "../context/index.js";
|
|
37
37
|
import { tryWriteLiveDisplayV3 } from "../events/v3/live-feed.js";
|
|
38
38
|
import { liveHookSignalDefersDrainV3, reconcileLivePendingRuntimeContextV3, recordLiveDelegatedChildSessionV3, recordLiveHookSignalV3, resolveLiveEventLedgerRouteV3, } from "../events/v3/live-routing.js";
|
|
39
39
|
import { captureSpanClockV3 } from "../events/v3/span-state.js";
|
|
40
40
|
import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.js";
|
|
41
|
+
import { importMail, publishMail } from "../presence/mail.js";
|
|
41
42
|
import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.js";
|
|
42
43
|
import { stableScopeId } from "../workflow/scope-id.js";
|
|
43
44
|
import { adapterBehavior } from "./adapter/behaviors/index.js";
|
|
@@ -57,6 +58,7 @@ import { resolveOwner } from "./resolve/owner.js";
|
|
|
57
58
|
import { inspectSessionNameDisplayImmediately, scanAssistantStatusBoxPresent, scanStatusBoxPresent, scanTranscriptRuntime, } from "./resolve/transcript.js";
|
|
58
59
|
import { clearHookRunMarker, writeHookRunMarker } from "./run-markers.js";
|
|
59
60
|
import { sessionNamePresence } from "./session-name-presence.js";
|
|
61
|
+
import { shellCommitTreeReason } from "./shell-commit-tree.js";
|
|
60
62
|
import { shellRmReason } from "./shell-rm.js";
|
|
61
63
|
import { shellWaiterReason } from "./shell-waiter.js";
|
|
62
64
|
let hookHealthState;
|
|
@@ -227,7 +229,18 @@ function healForkLineage(coordRoot, instanceId, payload) {
|
|
|
227
229
|
function ensureSessionDisplayName(coordRoot, instanceId, sessionId, adapter, payload) {
|
|
228
230
|
if (coordEnv("WORKFLOW_CHILD") === "1")
|
|
229
231
|
return;
|
|
230
|
-
|
|
232
|
+
// This runs on every tool call. The heartbeat cache is the surface this
|
|
233
|
+
// function stamps, so a cache that already carries a name, or marks a
|
|
234
|
+
// session that never takes one, means there is nothing to do. Only a cache
|
|
235
|
+
// that cannot answer pays for the authoritative read, which re-validates the
|
|
236
|
+
// whole active ledger and cost about half a second per call at 22 MB.
|
|
237
|
+
const cached = readHeartbeat(coordRoot, instanceId);
|
|
238
|
+
if (cached?.kind === "subagent" || cached?.kind === "transient" || cached?.workflow_run_id) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (cached?.name?.trim())
|
|
242
|
+
return;
|
|
243
|
+
const row = readLiveCoordinationRow(coordRoot, instanceId) ?? cached;
|
|
231
244
|
if (row?.kind === "subagent" || row?.kind === "transient" || row?.workflow_run_id)
|
|
232
245
|
return;
|
|
233
246
|
if (row?.name?.trim())
|
|
@@ -1009,8 +1022,23 @@ function recordContextTelemetry(run) {
|
|
|
1009
1022
|
}
|
|
1010
1023
|
}
|
|
1011
1024
|
}
|
|
1025
|
+
/** Cross-machine peer messages (ADR 0222) ride the presence fetch: import
|
|
1026
|
+
* whatever mail refs are already local. Reads local refs only, at most once
|
|
1027
|
+
* per import interval; never blocks on the network. */
|
|
1028
|
+
async function receiveRemoteMail(coordRoot) {
|
|
1029
|
+
const { peerInbox } = await import("../inbox/peer-messages.js");
|
|
1030
|
+
importMail(coordRoot, peerInbox(coordRoot), { throttle: true });
|
|
1031
|
+
}
|
|
1012
1032
|
async function handleSessionStarted(run) {
|
|
1013
1033
|
const { coordRoot, adapter, payload, owner, sessionId, data } = run;
|
|
1034
|
+
// Import mail from refs fetched earlier, so a message sent from another
|
|
1035
|
+
// machine while this task was away is in the inbox this context renders.
|
|
1036
|
+
try {
|
|
1037
|
+
await receiveRemoteMail(coordRoot);
|
|
1038
|
+
}
|
|
1039
|
+
catch (err) {
|
|
1040
|
+
logError(coordRoot, err, { phase: "session-start-mail" });
|
|
1041
|
+
}
|
|
1014
1042
|
// A configured backup runs out of process and is freshness-gated per host.
|
|
1015
1043
|
// Startup never waits for restic or a remote provider; the child owns no
|
|
1016
1044
|
// hook stdio, so the harness is not held open until it finishes. A failed
|
|
@@ -1124,6 +1152,7 @@ async function handleTurnStarted(run) {
|
|
|
1124
1152
|
// interval; the systemMessage below reads whatever is locally known).
|
|
1125
1153
|
try {
|
|
1126
1154
|
fetchPresence(coordRoot);
|
|
1155
|
+
await receiveRemoteMail(coordRoot);
|
|
1127
1156
|
}
|
|
1128
1157
|
catch (err) {
|
|
1129
1158
|
logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
|
|
@@ -1187,6 +1216,7 @@ async function handleStop(run) {
|
|
|
1187
1216
|
// its daemon intentionally exits when a machine has no live sessions.
|
|
1188
1217
|
try {
|
|
1189
1218
|
publishPresence(coordRoot);
|
|
1219
|
+
publishMail(coordRoot);
|
|
1190
1220
|
ensureRelayDaemon(coordRoot);
|
|
1191
1221
|
}
|
|
1192
1222
|
catch (err) {
|
|
@@ -1310,6 +1340,12 @@ async function handleToolRequested(run) {
|
|
|
1310
1340
|
emitDeny(adapter, rmReason);
|
|
1311
1341
|
return 0;
|
|
1312
1342
|
}
|
|
1343
|
+
const commitTreeReason = shellCommitTreeReason(payload?.tool_name, payload?.tool_input, resolveBinName(coordRoot));
|
|
1344
|
+
if (commitTreeReason) {
|
|
1345
|
+
const { emitDeny } = await import("./adapter/output.js");
|
|
1346
|
+
emitDeny(adapter, commitTreeReason);
|
|
1347
|
+
return 0;
|
|
1348
|
+
}
|
|
1313
1349
|
// G-guard for ALL adapters. Claude Code previously ran this via a
|
|
1314
1350
|
// pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
|
|
1315
1351
|
// that adapter is now deleted, so agent-hook owns the deny for every adapter.
|
|
@@ -1327,8 +1363,38 @@ async function handleToolRequested(run) {
|
|
|
1327
1363
|
}
|
|
1328
1364
|
if (!guardAllowed)
|
|
1329
1365
|
return 0;
|
|
1366
|
+
// Heavy shell checks queue for a machine-wide admission slot when the
|
|
1367
|
+
// project configures one. Fails open: any error runs the command as written.
|
|
1368
|
+
try {
|
|
1369
|
+
if (await emitAdmissionRewrite(coordRoot, adapter, payload))
|
|
1370
|
+
return 0;
|
|
1371
|
+
}
|
|
1372
|
+
catch (err) {
|
|
1373
|
+
logError(coordRoot, err, { phase: "pre-tool-use-admission" });
|
|
1374
|
+
}
|
|
1330
1375
|
return undefined;
|
|
1331
1376
|
}
|
|
1377
|
+
async function emitAdmissionRewrite(coordRoot, adapter, payload) {
|
|
1378
|
+
const resource = readAdmissionConfig(coordRoot)?.guard?.resource;
|
|
1379
|
+
if (!resource)
|
|
1380
|
+
return false;
|
|
1381
|
+
const { admissionHookOutput } = await import("./shell-admission.js");
|
|
1382
|
+
const output = admissionHookOutput({
|
|
1383
|
+
adapter,
|
|
1384
|
+
resource,
|
|
1385
|
+
bin: resolveBinName(coordRoot),
|
|
1386
|
+
projectRoot: coordRoot,
|
|
1387
|
+
toolName: payload?.tool_name,
|
|
1388
|
+
toolInput: payload?.tool_input,
|
|
1389
|
+
cwd: payload?.cwd,
|
|
1390
|
+
skip: coordEnv("ADMISSION_SKIP") === "1" || Boolean(coordEnv("ADMISSION_HELD")),
|
|
1391
|
+
platform: process.platform,
|
|
1392
|
+
});
|
|
1393
|
+
if (!output)
|
|
1394
|
+
return false;
|
|
1395
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1332
1398
|
async function handlePostToolUse(run) {
|
|
1333
1399
|
const { coordRoot, adapter, eventName, payload, owner } = run;
|
|
1334
1400
|
// Inject the full instruction only at the successful tool boundary that
|
|
@@ -1348,9 +1414,18 @@ async function handlePostToolUse(run) {
|
|
|
1348
1414
|
// retry response already carries the full instruction and an end-turn
|
|
1349
1415
|
// status is the wrong place to ask.
|
|
1350
1416
|
try {
|
|
1351
|
-
const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
|
|
1352
1417
|
// A workflow child has no user-visible title to set.
|
|
1353
|
-
|
|
1418
|
+
if (coordEnv("WORKFLOW_CHILD") === "1")
|
|
1419
|
+
return;
|
|
1420
|
+
// The live row copies every name-display field from a current heartbeat
|
|
1421
|
+
// cache and leaves them unset otherwise, so a raw cache with nothing
|
|
1422
|
+
// pending proves the row has nothing pending either. That skips a full
|
|
1423
|
+
// ledger read on almost every tool call; only a pending title pays for
|
|
1424
|
+
// the authoritative check below.
|
|
1425
|
+
if (!sessionNameDisplayPending(readHeartbeat(coordRoot, owner.instance_id)))
|
|
1426
|
+
return;
|
|
1427
|
+
const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
|
|
1428
|
+
const name = sessionNameDisplayPending(row);
|
|
1354
1429
|
if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
|
|
1355
1430
|
// Record the title before asking for it. The suggestion can still
|
|
1356
1431
|
// change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
|