@brunobracaioli/aibrain 0.3.0-beta.14 → 0.3.0-beta.16
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/README.md +7 -0
- package/dist/assets/claude/hooks/context-compaction.ts +163 -32
- package/dist/assets/codex-context-compaction.mjs +284 -12
- package/dist/assets/jev-paid-pilot-terminal.mjs +126 -22
- package/dist/claude-observation.mjs +2 -1
- package/dist/cli.mjs +13 -2
- package/dist/codex-diagnostics.mjs +23 -2
- package/dist/launch.mjs +3 -1
- package/dist/package.json +1 -1
- package/dist/platform.mjs +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Assisted AI Brain context beta client for Linux/WSL with Node.js 22 or newer.
|
|
4
4
|
|
|
5
|
+
Codex capture also requires the `flock` utility from `util-linux`, normally
|
|
6
|
+
included in Linux/WSL. The launcher and doctor check availability before a
|
|
7
|
+
monitored Codex session starts. Capture coordination is released by the kernel
|
|
8
|
+
after a process exits unexpectedly. An unidentifiable empty lock left by an
|
|
9
|
+
older client requires a verified support migration; do not delete checkpoints,
|
|
10
|
+
notes or lock files to work around a reported storage error.
|
|
11
|
+
|
|
5
12
|
The reviewed native compatibility set is Claude Code 2.1.294, 2.1.295, 2.1.296,
|
|
6
13
|
2.1.299, 2.1.300, 2.1.301, 2.1.302 or 2.1.303, and Codex CLI 0.161.0,
|
|
7
14
|
0.162.0, 0.162.1 or 0.163.0. A newer stable Codex 0.x release may continue only
|
|
@@ -150,7 +150,7 @@ type Register = (
|
|
|
150
150
|
) => void;
|
|
151
151
|
|
|
152
152
|
const MAX_RESULT_BYTES = 256 * 1024;
|
|
153
|
-
const CLAUDE_HOOK_VERSION = '0.3.0-beta.
|
|
153
|
+
const CLAUDE_HOOK_VERSION = '0.3.0-beta.16';
|
|
154
154
|
const MAX_DIAGNOSTIC_BYTE_COUNT = 128 * 1024 * 1024;
|
|
155
155
|
const COOLDOWN_MS = 60_000;
|
|
156
156
|
const MAX_TRUNCATED_CODE_POINTS = 300;
|
|
@@ -974,6 +974,7 @@ type ObservationRecord = {
|
|
|
974
974
|
| 'compact_entry'
|
|
975
975
|
| 'compact_exit'
|
|
976
976
|
| 'native_summary_proof'
|
|
977
|
+
| 'native_fallback_proof'
|
|
977
978
|
| 'native_error';
|
|
978
979
|
readonly category:
|
|
979
980
|
| 'observed'
|
|
@@ -1790,6 +1791,37 @@ async function flushProofDiagnostic(
|
|
|
1790
1791
|
]);
|
|
1791
1792
|
}
|
|
1792
1793
|
|
|
1794
|
+
async function flushNativeFallbackProofDiagnostic(
|
|
1795
|
+
api: HookApi,
|
|
1796
|
+
options: ObservationOptions,
|
|
1797
|
+
operationId: string,
|
|
1798
|
+
sessionId: string,
|
|
1799
|
+
instanceId: string,
|
|
1800
|
+
clientVersion: string,
|
|
1801
|
+
fields: Pick<
|
|
1802
|
+
ObservationRecord,
|
|
1803
|
+
| 'proof_expected_count'
|
|
1804
|
+
| 'proof_actual_count'
|
|
1805
|
+
| 'proof_match'
|
|
1806
|
+
| 'proof_match_offset'
|
|
1807
|
+
| 'proof_view_source'
|
|
1808
|
+
| 'proof_api_view_match'
|
|
1809
|
+
| 'proof_api_count'
|
|
1810
|
+
>,
|
|
1811
|
+
): Promise<void> {
|
|
1812
|
+
if (!options.diagnosticsEnabled) return;
|
|
1813
|
+
await flushObservationDiagnostics(api, options, operationId, sessionId, instanceId, [
|
|
1814
|
+
observationRecord(
|
|
1815
|
+
operationId,
|
|
1816
|
+
'native_fallback_proof',
|
|
1817
|
+
'observed',
|
|
1818
|
+
clientVersion,
|
|
1819
|
+
undefined,
|
|
1820
|
+
fields,
|
|
1821
|
+
),
|
|
1822
|
+
]);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1793
1825
|
async function flushNativeSummaryProofDiagnostic(
|
|
1794
1826
|
api: HookApi,
|
|
1795
1827
|
options: ObservationOptions,
|
|
@@ -3590,6 +3622,8 @@ type PendingFallbackProof = {
|
|
|
3590
3622
|
readonly expected_hash?: string;
|
|
3591
3623
|
readonly expected_count?: number;
|
|
3592
3624
|
readonly expected_message_hashes?: readonly string[];
|
|
3625
|
+
readonly delivery_mode?: 'atomic_marker';
|
|
3626
|
+
readonly marker_digest?: string;
|
|
3593
3627
|
readonly anchor_counts?: readonly number[];
|
|
3594
3628
|
readonly anchor_hashes?: readonly string[];
|
|
3595
3629
|
readonly expires_at: string;
|
|
@@ -4131,6 +4165,8 @@ function structurallyValidPendingProof(value: unknown): value is PendingProof {
|
|
|
4131
4165
|
'expected_hash',
|
|
4132
4166
|
'expected_count',
|
|
4133
4167
|
'expected_message_hashes',
|
|
4168
|
+
'delivery_mode',
|
|
4169
|
+
'marker_digest',
|
|
4134
4170
|
'anchor_counts',
|
|
4135
4171
|
'anchor_hashes',
|
|
4136
4172
|
'expires_at',
|
|
@@ -4161,6 +4197,10 @@ function structurallyValidPendingProof(value: unknown): value is PendingProof {
|
|
|
4161
4197
|
value.expected_message_hashes.every(
|
|
4162
4198
|
(hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{16}$/u.test(hash),
|
|
4163
4199
|
))) &&
|
|
4200
|
+
(value.delivery_mode === undefined || value.delivery_mode === 'atomic_marker') &&
|
|
4201
|
+
(value.marker_digest === undefined || /^[a-f0-9]{64}$/u.test(String(value.marker_digest))) &&
|
|
4202
|
+
((value.delivery_mode === undefined && value.marker_digest === undefined) ||
|
|
4203
|
+
(value.delivery_mode === 'atomic_marker' && typeof value.marker_digest === 'string')) &&
|
|
4164
4204
|
((value.anchor_counts === undefined && value.anchor_hashes === undefined) ||
|
|
4165
4205
|
(Array.isArray(value.anchor_counts) &&
|
|
4166
4206
|
Array.isArray(value.anchor_hashes) &&
|
|
@@ -4803,6 +4843,26 @@ function getTelemetry(
|
|
|
4803
4843
|
return state.current;
|
|
4804
4844
|
}
|
|
4805
4845
|
|
|
4846
|
+
async function nativeFallbackMarker(
|
|
4847
|
+
operationId: string,
|
|
4848
|
+
sessionRef: string,
|
|
4849
|
+
baselineHash: string,
|
|
4850
|
+
): Promise<{ readonly text: string; readonly digest: string }> {
|
|
4851
|
+
const digest = await sha256(
|
|
4852
|
+
`aibrain-native-fallback-v1|${operationId}|${sessionRef}|${baselineHash}`,
|
|
4853
|
+
);
|
|
4854
|
+
return { digest, text: `[AI Brain native fallback receipt v1:${digest}]` };
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
function nativeFallbackDeliveryResult(nativeResult: unknown, marker: string): unknown {
|
|
4858
|
+
const messages = nativeResultMessages(nativeResult);
|
|
4859
|
+
if (messages === undefined || messages.length === 0) return nativeResult;
|
|
4860
|
+
return {
|
|
4861
|
+
...(nativeResult as Record<string, unknown>),
|
|
4862
|
+
messages: [...messages, { role: 'user', text: marker, toolUses: [] }],
|
|
4863
|
+
};
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4806
4866
|
async function queueNativeFallbackProof(
|
|
4807
4867
|
api: HookApi,
|
|
4808
4868
|
runtime: ProofRuntimeState,
|
|
@@ -4813,7 +4873,7 @@ async function queueNativeFallbackProof(
|
|
|
4813
4873
|
nativeResult: unknown,
|
|
4814
4874
|
beforeVisibleMessages: readonly string[] | undefined,
|
|
4815
4875
|
activeOffset: number | undefined,
|
|
4816
|
-
): Promise<
|
|
4876
|
+
): Promise<unknown> {
|
|
4817
4877
|
const expectedMessages = nativeResultMessages(nativeResult);
|
|
4818
4878
|
const expectedHash =
|
|
4819
4879
|
expectedMessages === undefined
|
|
@@ -4831,6 +4891,10 @@ async function queueNativeFallbackProof(
|
|
|
4831
4891
|
beforeVisibleMessages === undefined || expectedMessages === undefined
|
|
4832
4892
|
? undefined
|
|
4833
4893
|
: await nativeProofAnchors(beforeVisibleMessages, expectedMessages.length, activeOffset);
|
|
4894
|
+
const marker =
|
|
4895
|
+
expectedMessages === undefined
|
|
4896
|
+
? undefined
|
|
4897
|
+
: await nativeFallbackMarker(operationId, sessionRef, baselineHash);
|
|
4834
4898
|
const proof: PendingFallbackProof = {
|
|
4835
4899
|
schema_version: 1,
|
|
4836
4900
|
kind: 'native_fallback',
|
|
@@ -4844,6 +4908,9 @@ async function queueNativeFallbackProof(
|
|
|
4844
4908
|
...(expectedMessageHashes === undefined
|
|
4845
4909
|
? {}
|
|
4846
4910
|
: { expected_message_hashes: expectedMessageHashes }),
|
|
4911
|
+
...(marker === undefined
|
|
4912
|
+
? {}
|
|
4913
|
+
: { delivery_mode: 'atomic_marker' as const, marker_digest: marker.digest }),
|
|
4847
4914
|
...(anchors === undefined
|
|
4848
4915
|
? {}
|
|
4849
4916
|
: { anchor_counts: anchors.counts, anchor_hashes: anchors.hashes }),
|
|
@@ -4854,6 +4921,9 @@ async function queueNativeFallbackProof(
|
|
|
4854
4921
|
};
|
|
4855
4922
|
runtime.pendingProofMemory = proof;
|
|
4856
4923
|
runtime.pendingProofPersistenceFailed = !(await savePendingProof(api, proof));
|
|
4924
|
+
return runtime.pendingProofPersistenceFailed || marker === undefined
|
|
4925
|
+
? nativeResult
|
|
4926
|
+
: nativeFallbackDeliveryResult(nativeResult, marker.text);
|
|
4857
4927
|
}
|
|
4858
4928
|
|
|
4859
4929
|
async function confirmPendingProof(
|
|
@@ -4937,38 +5007,83 @@ async function confirmPendingProof(
|
|
|
4937
5007
|
proof_actual_count: messages.length,
|
|
4938
5008
|
};
|
|
4939
5009
|
if (proof.expected_hash !== undefined && expectedCount !== undefined) {
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
5010
|
+
if (proof.delivery_mode === 'atomic_marker' && proof.marker_digest !== undefined) {
|
|
5011
|
+
try {
|
|
5012
|
+
const apiView = await boundedApiMessages(api);
|
|
5013
|
+
const marker = await nativeFallbackMarker(
|
|
5014
|
+
proof.operation_id,
|
|
5015
|
+
sessionRef,
|
|
5016
|
+
proof.baseline_hash,
|
|
5017
|
+
);
|
|
5018
|
+
const markerValid = marker.digest === proof.marker_digest;
|
|
5019
|
+
const markerCount = exactTextCount(apiView, marker.text);
|
|
5020
|
+
fallbackProofFields = {
|
|
5021
|
+
...fallbackProofFields,
|
|
5022
|
+
proof_view_source: 'api',
|
|
5023
|
+
proof_api_view_match: markerValid && markerCount === 1 ? 'exact' : 'mismatch',
|
|
5024
|
+
proof_api_count: apiView.length,
|
|
5025
|
+
};
|
|
5026
|
+
if (markerValid && markerCount === 1) {
|
|
5027
|
+
fallbackMatch = 'exact';
|
|
5028
|
+
fallbackProofFields = {
|
|
5029
|
+
...fallbackProofFields,
|
|
5030
|
+
proof_match: 'exact',
|
|
5031
|
+
proof_match_offset: 0,
|
|
5032
|
+
};
|
|
5033
|
+
verified = true;
|
|
5034
|
+
}
|
|
5035
|
+
} catch {
|
|
5036
|
+
fallbackProofFields = {
|
|
5037
|
+
...fallbackProofFields,
|
|
5038
|
+
proof_view_source: 'api',
|
|
5039
|
+
proof_api_view_match: 'unavailable',
|
|
5040
|
+
};
|
|
5041
|
+
}
|
|
5042
|
+
} else {
|
|
5043
|
+
// Compatibility with proofs written by beta15 and earlier. These
|
|
5044
|
+
// proofs predate the API-view fingerprint and retain the old strict
|
|
5045
|
+
// default-view check.
|
|
5046
|
+
fallbackMatch = await nativeSummaryViewMatch(
|
|
5047
|
+
messages,
|
|
5048
|
+
expectedCount,
|
|
5049
|
+
proof.expected_hash,
|
|
5050
|
+
proof.expected_message_hashes,
|
|
5051
|
+
proof.anchor_counts,
|
|
5052
|
+
proof.anchor_hashes,
|
|
5053
|
+
);
|
|
5054
|
+
const fallbackOffset =
|
|
5055
|
+
fallbackMatch === 'active_suffix'
|
|
5056
|
+
? await nativeSummaryMatchedOffset(
|
|
5057
|
+
messages,
|
|
5058
|
+
expectedCount,
|
|
5059
|
+
proof.expected_hash,
|
|
5060
|
+
proof.expected_message_hashes,
|
|
5061
|
+
proof.anchor_counts,
|
|
5062
|
+
proof.anchor_hashes,
|
|
5063
|
+
)
|
|
5064
|
+
: undefined;
|
|
5065
|
+
fallbackProofFields = {
|
|
5066
|
+
...fallbackProofFields,
|
|
5067
|
+
proof_view_source: 'default',
|
|
5068
|
+
proof_match: fallbackMatch,
|
|
5069
|
+
...(fallbackMatch === 'exact' ? { proof_match_offset: 0 } : {}),
|
|
5070
|
+
...(fallbackOffset === undefined ? {} : { proof_match_offset: fallbackOffset }),
|
|
5071
|
+
};
|
|
5072
|
+
verified = fallbackMatch === 'exact' || fallbackMatch === 'active_suffix';
|
|
5073
|
+
}
|
|
4966
5074
|
}
|
|
4967
5075
|
}
|
|
4968
5076
|
if (runtime.pendingProofPersistenceFailed) verified = false;
|
|
4969
5077
|
const telemetry = getTelemetry(api, runtime.brainServer, runtime.options, runtime.telemetryState);
|
|
4970
5078
|
if (!verified) {
|
|
4971
|
-
|
|
5079
|
+
// Only the atomic native fallback marker may wait for the host's active
|
|
5080
|
+
// API view. A prepared JEV capsule remains strict at every callback.
|
|
5081
|
+
if (
|
|
5082
|
+
deferMismatch &&
|
|
5083
|
+
proof.kind === 'native_fallback' &&
|
|
5084
|
+
proof.delivery_mode === 'atomic_marker'
|
|
5085
|
+
)
|
|
5086
|
+
return;
|
|
4972
5087
|
runtime.pendingProofMemory = undefined;
|
|
4973
5088
|
runtime.pendingProofPersistenceFailed = false;
|
|
4974
5089
|
await clearPendingProof(api, sessionRef);
|
|
@@ -4993,6 +5108,16 @@ async function confirmPendingProof(
|
|
|
4993
5108
|
runtime.pendingProofMemory = undefined;
|
|
4994
5109
|
runtime.pendingProofPersistenceFailed = false;
|
|
4995
5110
|
await clearPendingProof(api, sessionRef);
|
|
5111
|
+
if (proof.kind === 'native_fallback')
|
|
5112
|
+
await flushNativeFallbackProofDiagnostic(
|
|
5113
|
+
api,
|
|
5114
|
+
runtime.observation,
|
|
5115
|
+
proof.operation_id,
|
|
5116
|
+
sessionRef,
|
|
5117
|
+
runtime.observationInstanceId,
|
|
5118
|
+
nativeClientVersion(runtime.options),
|
|
5119
|
+
fallbackProofFields,
|
|
5120
|
+
);
|
|
4996
5121
|
if (proof.kind === 'custom') {
|
|
4997
5122
|
const dropped = proof.pairs.filter((pair) => pair.action === 'drop_pair').length;
|
|
4998
5123
|
const reduced = proof.pairs.filter((pair) => pair.action === 'truncate_result').length;
|
|
@@ -5579,7 +5704,10 @@ export const register: Register = (on, options) => {
|
|
|
5579
5704
|
|
|
5580
5705
|
on('prompt.submit', async (api, rawEvent, next) => {
|
|
5581
5706
|
const event = rawEvent as unknown as PromptSubmitHookEvent;
|
|
5582
|
-
|
|
5707
|
+
// The host has completed the prior native turn before accepting a new
|
|
5708
|
+
// prompt. This is the final proof opportunity for a fallback whose first
|
|
5709
|
+
// turn.complete callback observed the retired/default view.
|
|
5710
|
+
await confirmPendingProof(api, proofRuntime);
|
|
5583
5711
|
if (
|
|
5584
5712
|
proofRuntime.compactionStrategy === 'native_summary_notes' &&
|
|
5585
5713
|
enabled &&
|
|
@@ -5654,7 +5782,10 @@ export const register: Register = (on, options) => {
|
|
|
5654
5782
|
});
|
|
5655
5783
|
|
|
5656
5784
|
on('turn.complete', async (api, event, next) => {
|
|
5657
|
-
|
|
5785
|
+
// The native host can publish the result before its active API view catches
|
|
5786
|
+
// up. Keep only the atomic fallback marker pending through this lifecycle
|
|
5787
|
+
// edge; prompt.submit performs the strict confirmation on the next turn.
|
|
5788
|
+
await confirmPendingProof(api, proofRuntime, true);
|
|
5658
5789
|
if (
|
|
5659
5790
|
proofRuntime.compactionStrategy === 'native_summary_notes' &&
|
|
5660
5791
|
enabled &&
|
|
@@ -5826,7 +5957,7 @@ export const register: Register = (on, options) => {
|
|
|
5826
5957
|
nativeContinuationStarted = true;
|
|
5827
5958
|
const nativeResult = await next(event);
|
|
5828
5959
|
if (fallbackCapture !== undefined)
|
|
5829
|
-
|
|
5960
|
+
return queueNativeFallbackProof(
|
|
5830
5961
|
api,
|
|
5831
5962
|
proofRuntime,
|
|
5832
5963
|
fallbackCapture.operationId,
|
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
chmod,
|
|
5
|
+
link as hardlink,
|
|
5
6
|
lstat,
|
|
6
7
|
mkdir,
|
|
7
8
|
open,
|
|
8
9
|
readFile,
|
|
10
|
+
readlink,
|
|
9
11
|
readdir,
|
|
10
12
|
rename,
|
|
11
13
|
unlink,
|
|
12
14
|
writeFile,
|
|
13
15
|
} from 'node:fs/promises';
|
|
16
|
+
import { constants as fsConstants } from 'node:fs';
|
|
14
17
|
import { spawn } from 'node:child_process';
|
|
15
18
|
import { createHash, randomUUID } from 'node:crypto';
|
|
16
19
|
import { dirname, relative, resolve } from 'node:path';
|
|
@@ -21,6 +24,7 @@ import { preflightSelectionState } from './context-selection-fitting.mjs';
|
|
|
21
24
|
|
|
22
25
|
const captureFs = {
|
|
23
26
|
chmod,
|
|
27
|
+
hardlink,
|
|
24
28
|
lstat,
|
|
25
29
|
mkdir,
|
|
26
30
|
open,
|
|
@@ -116,6 +120,10 @@ const DIAGNOSTIC_REASONS = new Set([
|
|
|
116
120
|
'capture_item_count_exceeded',
|
|
117
121
|
'private_storage_failed',
|
|
118
122
|
'storage_capacity_exceeded',
|
|
123
|
+
'capture_lock_timeout',
|
|
124
|
+
'capture_lock_legacy',
|
|
125
|
+
'capture_lock_owner_unverifiable',
|
|
126
|
+
'capture_lock_backend_unavailable',
|
|
119
127
|
'attached',
|
|
120
128
|
'uncertain',
|
|
121
129
|
'disconnected',
|
|
@@ -613,7 +621,12 @@ async function ensurePrivateDirectory(directory, fsOps = captureFs) {
|
|
|
613
621
|
}
|
|
614
622
|
async function assertPrivateFile(file, label, fsOps = captureFs) {
|
|
615
623
|
const info = await fsOps.lstat(file);
|
|
616
|
-
if (
|
|
624
|
+
if (
|
|
625
|
+
info.isSymbolicLink() ||
|
|
626
|
+
!info.isFile() ||
|
|
627
|
+
(info.mode & 0o077) !== 0 ||
|
|
628
|
+
(typeof process.getuid === 'function' && info.uid !== process.getuid())
|
|
629
|
+
)
|
|
617
630
|
throw new Error(`${label} is not a private regular file`);
|
|
618
631
|
return info;
|
|
619
632
|
}
|
|
@@ -844,16 +857,250 @@ async function readCaptureDirectoryState(directory, fsOps = captureFs) {
|
|
|
844
857
|
return state;
|
|
845
858
|
}
|
|
846
859
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
860
|
+
function captureLockError(reason) {
|
|
861
|
+
const messages = {
|
|
862
|
+
capture_lock_timeout: 'capture lock is held by a live or unverified owner',
|
|
863
|
+
capture_lock_legacy: 'capture lock uses an unidentifiable legacy sentinel',
|
|
864
|
+
capture_lock_owner_unverifiable: 'capture lock owner identity is unavailable',
|
|
865
|
+
capture_lock_backend_unavailable: 'capture lock backend is unavailable',
|
|
866
|
+
};
|
|
867
|
+
const error = new Error(messages[reason] ?? 'capture lock unavailable');
|
|
868
|
+
error.diagnosticReason = reason;
|
|
869
|
+
return error;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const CAPTURE_LOCK_MARKER = `${JSON.stringify({ backend: 'flock', schema: 2 })}\n`;
|
|
873
|
+
const CAPTURE_GUARD_NAME = '.capture.guard';
|
|
874
|
+
|
|
875
|
+
async function observerKernelIdentity() {
|
|
876
|
+
try {
|
|
877
|
+
return {
|
|
878
|
+
bootId: (await readFile('/proc/sys/kernel/random/boot_id', 'utf8')).trim(),
|
|
879
|
+
pidNamespace: await readlink('/proc/self/ns/pid'),
|
|
880
|
+
};
|
|
881
|
+
} catch {
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async function processIdentity(pid) {
|
|
887
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return { status: 'invalid' };
|
|
888
|
+
const observer = await observerKernelIdentity();
|
|
889
|
+
if (observer === null) return { status: 'unverifiable' };
|
|
890
|
+
try {
|
|
891
|
+
const ownerNamespace = await readlink(`/proc/${pid}/ns/pid`);
|
|
892
|
+
if (ownerNamespace !== observer.pidNamespace || observer.bootId.length === 0)
|
|
893
|
+
return { status: 'foreign' };
|
|
894
|
+
const stat = await readFile(`/proc/${pid}/stat`, 'utf8');
|
|
895
|
+
const closing = stat.lastIndexOf(')');
|
|
896
|
+
const fields =
|
|
897
|
+
closing < 0
|
|
898
|
+
? []
|
|
899
|
+
: stat
|
|
900
|
+
.slice(closing + 2)
|
|
901
|
+
.trim()
|
|
902
|
+
.split(/\s+/u);
|
|
903
|
+
const startTime = fields[19];
|
|
904
|
+
if (typeof startTime !== 'string' || !/^[0-9]+$/u.test(startTime))
|
|
905
|
+
return { status: 'unverifiable' };
|
|
906
|
+
return { status: 'live', startTime, bootId: observer.bootId, pidNamespace: ownerNamespace };
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (error?.code === 'ENOENT') return { status: 'dead' };
|
|
909
|
+
return { status: 'unverifiable' };
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async function captureSentinelOwner(lockPath, fsOps) {
|
|
914
|
+
let info;
|
|
915
|
+
try {
|
|
916
|
+
info = await fsOps.lstat(lockPath);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (error?.code === 'ENOENT') return { status: 'absent' };
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
if (info.isSymbolicLink() || !info.isFile() || (info.mode & 0o077) !== 0)
|
|
922
|
+
throw new Error('capture lock is not a private regular file');
|
|
923
|
+
if (info.size === 0) return { status: 'legacy' };
|
|
924
|
+
if (info.size > 4096) return { status: 'unverifiable' };
|
|
925
|
+
let value;
|
|
926
|
+
try {
|
|
927
|
+
value = JSON.parse(await fsOps.readFile(lockPath, 'utf8'));
|
|
928
|
+
} catch {
|
|
929
|
+
return { status: 'unverifiable' };
|
|
930
|
+
}
|
|
931
|
+
if (
|
|
932
|
+
!isRecord(value) ||
|
|
933
|
+
!hasOnlyKeys(value, [
|
|
934
|
+
'schema',
|
|
935
|
+
'backend',
|
|
936
|
+
'pid',
|
|
937
|
+
'startTime',
|
|
938
|
+
'bootId',
|
|
939
|
+
'pidNamespace',
|
|
940
|
+
'nonce',
|
|
941
|
+
]) ||
|
|
942
|
+
value.schema !== 1 ||
|
|
943
|
+
value.backend !== 'flock' ||
|
|
944
|
+
!isSafeBoundedInteger(value.pid, 1, 4_000_000_000) ||
|
|
945
|
+
typeof value.startTime !== 'string' ||
|
|
946
|
+
!/^[0-9]+$/u.test(value.startTime) ||
|
|
947
|
+
typeof value.bootId !== 'string' ||
|
|
948
|
+
!/^[a-f0-9-]{36}$/iu.test(value.bootId) ||
|
|
949
|
+
typeof value.pidNamespace !== 'string' ||
|
|
950
|
+
!/^pid:\[\d+\]$/u.test(value.pidNamespace) ||
|
|
951
|
+
typeof value.nonce !== 'string' ||
|
|
952
|
+
!isUuid(value.nonce)
|
|
953
|
+
)
|
|
954
|
+
return { status: 'unverifiable' };
|
|
955
|
+
const observer = await observerKernelIdentity();
|
|
956
|
+
if (observer === null) return { status: 'unverifiable' };
|
|
957
|
+
if (observer.bootId !== value.bootId) return { status: 'dead' };
|
|
958
|
+
if (observer.pidNamespace !== value.pidNamespace) return { status: 'foreign' };
|
|
959
|
+
const current = await processIdentity(value.pid);
|
|
960
|
+
if (current.status !== 'live') return { status: current.status };
|
|
961
|
+
if (current.bootId !== value.bootId || current.pidNamespace !== value.pidNamespace)
|
|
962
|
+
return { status: 'foreign' };
|
|
963
|
+
return current.startTime === value.startTime ? { status: 'live' } : { status: 'dead' };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function ensureCaptureSentinel(lockPath, task) {
|
|
967
|
+
const identity = await processIdentity(process.pid);
|
|
968
|
+
if (identity.status !== 'live') throw captureLockError('capture_lock_owner_unverifiable');
|
|
969
|
+
const metadata = `${JSON.stringify({
|
|
970
|
+
schema: 1,
|
|
971
|
+
backend: 'flock',
|
|
972
|
+
pid: process.pid,
|
|
973
|
+
startTime: identity.startTime,
|
|
974
|
+
bootId: identity.bootId,
|
|
975
|
+
pidNamespace: identity.pidNamespace,
|
|
976
|
+
nonce: randomUUID(),
|
|
977
|
+
})}\n`;
|
|
978
|
+
while (true) {
|
|
979
|
+
const temporary = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
980
|
+
let contention = false;
|
|
981
|
+
let published = false;
|
|
982
|
+
try {
|
|
983
|
+
let handle;
|
|
984
|
+
try {
|
|
985
|
+
const flags =
|
|
986
|
+
fsConstants.O_WRONLY |
|
|
987
|
+
fsConstants.O_CREAT |
|
|
988
|
+
fsConstants.O_EXCL |
|
|
989
|
+
(fsConstants.O_NOFOLLOW ?? 0);
|
|
990
|
+
handle = await captureFs.open(temporary, flags, 0o600);
|
|
991
|
+
await handle.writeFile(metadata, 'utf8');
|
|
992
|
+
await handle.sync?.();
|
|
993
|
+
} finally {
|
|
994
|
+
await handle?.close().catch(() => undefined);
|
|
995
|
+
}
|
|
996
|
+
await captureFs.hardlink(temporary, lockPath);
|
|
997
|
+
published = true;
|
|
998
|
+
const publishedInfo = await captureFs.lstat(lockPath);
|
|
999
|
+
try {
|
|
1000
|
+
return await task();
|
|
1001
|
+
} finally {
|
|
1002
|
+
const current = await captureFs.lstat(lockPath).catch(() => null);
|
|
1003
|
+
if (
|
|
1004
|
+
current !== null &&
|
|
1005
|
+
current.dev === publishedInfo.dev &&
|
|
1006
|
+
current.ino === publishedInfo.ino
|
|
1007
|
+
)
|
|
1008
|
+
await captureFs.unlink(lockPath).catch(() => undefined);
|
|
1009
|
+
}
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (!published && error?.code === 'EEXIST') contention = true;
|
|
1012
|
+
else throw error;
|
|
1013
|
+
} finally {
|
|
1014
|
+
await captureFs.unlink(temporary).catch(() => undefined);
|
|
1015
|
+
}
|
|
1016
|
+
if (!contention) throw captureLockError('capture_lock_owner_unverifiable');
|
|
1017
|
+
const owner = await captureSentinelOwner(lockPath, captureFs);
|
|
1018
|
+
if (owner.status === 'dead') {
|
|
1019
|
+
await captureFs.unlink(lockPath).catch((unlinkError) => {
|
|
1020
|
+
if (unlinkError?.code !== 'ENOENT') throw unlinkError;
|
|
1021
|
+
});
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (owner.status === 'legacy') throw captureLockError('capture_lock_legacy');
|
|
1025
|
+
throw captureLockError('capture_lock_owner_unverifiable');
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function withAdvisoryCaptureLock(directory, task) {
|
|
1030
|
+
const guardPath = resolve(directory, CAPTURE_GUARD_NAME);
|
|
1031
|
+
const lockPath = resolve(directory, '.capture.lock');
|
|
1032
|
+
let guardHandle;
|
|
1033
|
+
try {
|
|
1034
|
+
const flags = fsConstants.O_RDWR | fsConstants.O_CREAT | (fsConstants.O_NOFOLLOW ?? 0);
|
|
1035
|
+
guardHandle = await captureFs.open(guardPath, flags, 0o600);
|
|
1036
|
+
const info = await guardHandle.stat();
|
|
1037
|
+
if (
|
|
1038
|
+
!info.isFile() ||
|
|
1039
|
+
info.isSymbolicLink?.() ||
|
|
1040
|
+
info.nlink !== 1 ||
|
|
1041
|
+
(typeof process.getuid === 'function' && info.uid !== process.getuid()) ||
|
|
1042
|
+
(info.mode & 0o077) !== 0 ||
|
|
1043
|
+
info.size > 4096
|
|
1044
|
+
)
|
|
1045
|
+
throw captureLockError('capture_lock_owner_unverifiable');
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
await guardHandle?.close().catch(() => undefined);
|
|
1048
|
+
if (error?.code === 'ENOENT') throw captureLockError('capture_lock_backend_unavailable');
|
|
1049
|
+
throw error;
|
|
1050
|
+
}
|
|
1051
|
+
const started = Date.now();
|
|
1052
|
+
while (true) {
|
|
1053
|
+
let helper;
|
|
1054
|
+
try {
|
|
1055
|
+
helper = spawn('flock', ['-x', '-n', '3'], {
|
|
1056
|
+
stdio: ['ignore', 'ignore', 'ignore', guardHandle.fd],
|
|
1057
|
+
});
|
|
1058
|
+
const acquired = await new Promise((resolvePromise, rejectPromise) => {
|
|
1059
|
+
const timer = setTimeout(() => {
|
|
1060
|
+
helper.kill('SIGKILL');
|
|
1061
|
+
rejectPromise(captureLockError('capture_lock_backend_unavailable'));
|
|
1062
|
+
}, 2_000);
|
|
1063
|
+
helper.once('error', (error) => {
|
|
1064
|
+
clearTimeout(timer);
|
|
1065
|
+
if (error?.code === 'ENOENT')
|
|
1066
|
+
rejectPromise(captureLockError('capture_lock_backend_unavailable'));
|
|
1067
|
+
else rejectPromise(error);
|
|
1068
|
+
});
|
|
1069
|
+
helper.once('exit', (code) => {
|
|
1070
|
+
clearTimeout(timer);
|
|
1071
|
+
if (code === 0) resolvePromise(true);
|
|
1072
|
+
else if (code === 1) resolvePromise(false);
|
|
1073
|
+
else rejectPromise(captureLockError('capture_lock_backend_unavailable'));
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
if (acquired) {
|
|
1077
|
+
let marker = await guardHandle.readFile('utf8');
|
|
1078
|
+
if (marker.length === 0 || marker !== CAPTURE_LOCK_MARKER) {
|
|
1079
|
+
await guardHandle.truncate(0);
|
|
1080
|
+
await guardHandle.write(CAPTURE_LOCK_MARKER, 0, 'utf8');
|
|
1081
|
+
await guardHandle.sync?.();
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
return await ensureCaptureSentinel(lockPath, task);
|
|
1085
|
+
} finally {
|
|
1086
|
+
await guardHandle.close().catch(() => undefined);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
if (helper && helper.exitCode === null) helper.kill('SIGTERM');
|
|
1091
|
+
await guardHandle.close().catch(() => undefined);
|
|
1092
|
+
throw error;
|
|
1093
|
+
}
|
|
1094
|
+
if (helper && helper.exitCode === null) helper.kill('SIGTERM');
|
|
1095
|
+
if (Date.now() - started >= CAPTURE_LOCK_WAIT_MS) {
|
|
1096
|
+
await guardHandle.close().catch(() => undefined);
|
|
1097
|
+
throw captureLockError('capture_lock_timeout');
|
|
1098
|
+
}
|
|
1099
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async function withInjectedCaptureLock(directory, task, fsOps, { now, sleep }) {
|
|
857
1104
|
const lockPath = resolve(directory, '.capture.lock');
|
|
858
1105
|
const started = now();
|
|
859
1106
|
while (true) {
|
|
@@ -870,12 +1117,29 @@ async function withCaptureStoreLock(
|
|
|
870
1117
|
if (handle) throw error;
|
|
871
1118
|
if (error?.code !== 'EEXIST') throw error;
|
|
872
1119
|
if (now() - started >= CAPTURE_LOCK_WAIT_MS)
|
|
873
|
-
throw
|
|
1120
|
+
throw captureLockError('capture_lock_owner_unverifiable');
|
|
874
1121
|
await sleep(10);
|
|
875
1122
|
}
|
|
876
1123
|
}
|
|
877
1124
|
}
|
|
878
1125
|
|
|
1126
|
+
async function withCaptureStoreLock(
|
|
1127
|
+
directory,
|
|
1128
|
+
task,
|
|
1129
|
+
fsOps = captureFs,
|
|
1130
|
+
{
|
|
1131
|
+
now = Date.now,
|
|
1132
|
+
sleep = (milliseconds) =>
|
|
1133
|
+
new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)),
|
|
1134
|
+
} = {},
|
|
1135
|
+
) {
|
|
1136
|
+
const nativeFs =
|
|
1137
|
+
fsOps === captureFs ||
|
|
1138
|
+
(fsOps.open === open && fsOps.lstat === lstat && fsOps.writeFile === writeFile);
|
|
1139
|
+
if (nativeFs) return withAdvisoryCaptureLock(directory, task);
|
|
1140
|
+
return withInjectedCaptureLock(directory, task, fsOps, { now, sleep });
|
|
1141
|
+
}
|
|
1142
|
+
|
|
879
1143
|
async function writePrivateFile(file, serialized, fsOps = captureFs) {
|
|
880
1144
|
const directory = await ensurePrivateDirectory(dirname(file), fsOps);
|
|
881
1145
|
const target = resolve(
|
|
@@ -1324,6 +1588,14 @@ export async function __testWriteBoundedCapture(
|
|
|
1324
1588
|
validateCaptureSnapshot(value, value.sourceRevision);
|
|
1325
1589
|
return writeBoundedCapture(file, serialized, value, fs, { now, sleep });
|
|
1326
1590
|
}
|
|
1591
|
+
|
|
1592
|
+
export async function __testHoldCaptureStoreLock(directory, readyPath) {
|
|
1593
|
+
await ensurePrivateDirectory(directory);
|
|
1594
|
+
return withCaptureStoreLock(directory, async () => {
|
|
1595
|
+
await writeFile(readyPath, 'ready\n', { flag: 'wx', mode: 0o600 });
|
|
1596
|
+
await new Promise((resolvePromise) => process.stdin.once('end', resolvePromise));
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1327
1599
|
async function atomicWriteTextPrivate(file, text) {
|
|
1328
1600
|
if (Buffer.byteLength(text) > MAX_NOTE_BYTES) throw new Error('recovery notes exceed bound');
|
|
1329
1601
|
const directory = await ensurePrivateDirectory(dirname(file));
|
|
@@ -9,6 +9,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
10
10
|
|
|
11
11
|
const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
const MAX_MONITOR_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
const MAX_MONITOR_PENDING_BYTES = 32 * 1024 * 1024;
|
|
12
14
|
const MAX_QUEUED_FRAMES = 64;
|
|
13
15
|
const MAX_QUEUED_BYTES = 32 * 1024 * 1024;
|
|
14
16
|
const STARTUP_HANDOFF_GRACE_MS = 5_000;
|
|
@@ -113,6 +115,12 @@ async function main() {
|
|
|
113
115
|
let monitorLineCount = 0;
|
|
114
116
|
let monitorByteCount = 0;
|
|
115
117
|
let monitorExitCode = null;
|
|
118
|
+
let transportAbortReason = null;
|
|
119
|
+
let transportAbortBytes = null;
|
|
120
|
+
let transportAbortMethod = null;
|
|
121
|
+
let maxMonitorFrameBytes = 0;
|
|
122
|
+
let maxMonitorFrameMethod = null;
|
|
123
|
+
let outputPauseCount = 0;
|
|
116
124
|
let terminalExitCode = null;
|
|
117
125
|
let server;
|
|
118
126
|
let webSocketServer;
|
|
@@ -189,7 +197,10 @@ async function main() {
|
|
|
189
197
|
child: null,
|
|
190
198
|
connection: null,
|
|
191
199
|
retiring: false,
|
|
192
|
-
|
|
200
|
+
monitorFrameParts: [],
|
|
201
|
+
monitorFrameBytes: 0,
|
|
202
|
+
monitorPendingChunk: Buffer.alloc(0),
|
|
203
|
+
monitorPaused: false,
|
|
193
204
|
monitorWritable: true,
|
|
194
205
|
pendingToMonitor: [],
|
|
195
206
|
queuedFrames: 0,
|
|
@@ -232,22 +243,30 @@ async function main() {
|
|
|
232
243
|
});
|
|
233
244
|
child.stdout.on('data', (chunk) => {
|
|
234
245
|
if (generation.retiring || shuttingDown) return;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
const part = Buffer.from(chunk);
|
|
247
|
+
if (generation.monitorPaused) {
|
|
248
|
+
if (generation.monitorPendingChunk.length + part.length > MAX_MONITOR_PENDING_BYTES) {
|
|
249
|
+
abortGeneration(
|
|
250
|
+
generation,
|
|
251
|
+
'monitor_backpressure_limit',
|
|
252
|
+
generation.monitorPendingChunk.length + part.length,
|
|
253
|
+
);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
generation.monitorPendingChunk =
|
|
257
|
+
generation.monitorPendingChunk.length === 0
|
|
258
|
+
? part
|
|
259
|
+
: Buffer.concat([generation.monitorPendingChunk, part]);
|
|
260
|
+
return;
|
|
245
261
|
}
|
|
262
|
+
processMonitorChunk(generation, part);
|
|
246
263
|
});
|
|
247
264
|
child.on('exit', (code) => {
|
|
248
265
|
if (generation.retiring || shuttingDown) {
|
|
249
266
|
generation.pendingToMonitor.length = 0;
|
|
250
|
-
generation.
|
|
267
|
+
generation.monitorFrameParts = [];
|
|
268
|
+
generation.monitorFrameBytes = 0;
|
|
269
|
+
generation.monitorPendingChunk = Buffer.alloc(0);
|
|
251
270
|
return;
|
|
252
271
|
}
|
|
253
272
|
fail(code ?? 0);
|
|
@@ -256,6 +275,56 @@ async function main() {
|
|
|
256
275
|
};
|
|
257
276
|
|
|
258
277
|
let initialGeneration;
|
|
278
|
+
const resumeMonitor = (generation) => {
|
|
279
|
+
if (!generation.monitorPaused || generation.retiring || shuttingDown) return;
|
|
280
|
+
generation.monitorPaused = false;
|
|
281
|
+
processMonitorBuffer(generation);
|
|
282
|
+
if (!generation.retiring && !shuttingDown && !generation.monitorPaused)
|
|
283
|
+
generation.child.stdout.resume();
|
|
284
|
+
};
|
|
285
|
+
const pauseMonitor = (generation) => {
|
|
286
|
+
if (generation.monitorPaused || generation.retiring || shuttingDown) return;
|
|
287
|
+
generation.monitorPaused = true;
|
|
288
|
+
outputPauseCount += 1;
|
|
289
|
+
generation.child.stdout.pause();
|
|
290
|
+
};
|
|
291
|
+
const processMonitorChunk = (generation, incoming) => {
|
|
292
|
+
if (generation.retiring || shuttingDown) return;
|
|
293
|
+
let offset = 0;
|
|
294
|
+
while (offset < incoming.length && !generation.monitorPaused && !generation.retiring) {
|
|
295
|
+
const newline = incoming.indexOf(10, offset);
|
|
296
|
+
const end = newline < 0 ? incoming.length : newline;
|
|
297
|
+
const part = incoming.subarray(offset, end);
|
|
298
|
+
if (generation.monitorFrameBytes + part.length > MAX_MONITOR_RESPONSE_BYTES) {
|
|
299
|
+
abortGeneration(
|
|
300
|
+
generation,
|
|
301
|
+
'monitor_frame_limit',
|
|
302
|
+
generation.monitorFrameBytes + part.length,
|
|
303
|
+
);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (part.length > 0) {
|
|
307
|
+
generation.monitorFrameParts.push(part);
|
|
308
|
+
generation.monitorFrameBytes += part.length;
|
|
309
|
+
}
|
|
310
|
+
offset = newline < 0 ? incoming.length : newline + 1;
|
|
311
|
+
if (newline >= 0) {
|
|
312
|
+
const line = Buffer.concat(generation.monitorFrameParts, generation.monitorFrameBytes);
|
|
313
|
+
generation.monitorFrameParts = [];
|
|
314
|
+
generation.monitorFrameBytes = 0;
|
|
315
|
+
handleMonitorLine(generation, line);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (offset < incoming.length) generation.monitorPendingChunk = incoming.subarray(offset);
|
|
319
|
+
};
|
|
320
|
+
const processMonitorBuffer = (generation) => {
|
|
321
|
+
if (generation.retiring || shuttingDown) return;
|
|
322
|
+
if (generation.monitorPendingChunk.length > 0) {
|
|
323
|
+
const pending = generation.monitorPendingChunk;
|
|
324
|
+
generation.monitorPendingChunk = Buffer.alloc(0);
|
|
325
|
+
processMonitorChunk(generation, pending);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
259
328
|
const flushGeneration = (generation) => {
|
|
260
329
|
while (generation.monitorWritable && generation.pendingToMonitor.length > 0) {
|
|
261
330
|
const frame = generation.pendingToMonitor.shift();
|
|
@@ -287,12 +356,31 @@ async function main() {
|
|
|
287
356
|
generation.retirementTimer = setTimeout(() => {
|
|
288
357
|
killOwned(generation.child, 'SIGKILL');
|
|
289
358
|
generation.pendingToMonitor.length = 0;
|
|
290
|
-
generation.
|
|
359
|
+
generation.monitorFrameParts = [];
|
|
360
|
+
generation.monitorFrameBytes = 0;
|
|
361
|
+
generation.monitorPendingChunk = Buffer.alloc(0);
|
|
291
362
|
generations.delete(generation);
|
|
292
363
|
}, 500);
|
|
293
364
|
};
|
|
294
|
-
const abortGeneration = (generation, reason) => {
|
|
365
|
+
const abortGeneration = (generation, reason, bytes = null, method = null) => {
|
|
295
366
|
const connection = generation.connection;
|
|
367
|
+
const primaryConnection = connection?.isCandidate !== true && connection?.isAuxiliary !== true;
|
|
368
|
+
if (transportAbortReason === null || primaryConnection) {
|
|
369
|
+
transportAbortReason = reason;
|
|
370
|
+
transportAbortBytes = Number.isSafeInteger(bytes) ? bytes : null;
|
|
371
|
+
transportAbortMethod = typeof method === 'string' ? method : null;
|
|
372
|
+
}
|
|
373
|
+
if (primaryConnection && cleanupInitiator === 'unknown') {
|
|
374
|
+
cleanupInitiator = `transport_${reason}`;
|
|
375
|
+
process.stderr.write(
|
|
376
|
+
`${JSON.stringify({
|
|
377
|
+
event: 'codex_terminal_bridge_error',
|
|
378
|
+
category: reason,
|
|
379
|
+
...(transportAbortBytes === null ? {} : { bytes: transportAbortBytes }),
|
|
380
|
+
...(transportAbortMethod === null ? {} : { method: transportAbortMethod }),
|
|
381
|
+
})}\n`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
296
384
|
if (connection?.isCandidate) {
|
|
297
385
|
rejectCandidate(connection, 1013, reason);
|
|
298
386
|
return;
|
|
@@ -374,6 +462,12 @@ async function main() {
|
|
|
374
462
|
monitor_line_count: monitorLineCount,
|
|
375
463
|
monitor_byte_count: monitorByteCount,
|
|
376
464
|
monitor_exit_code: monitorExitCode,
|
|
465
|
+
transport_abort_reason: transportAbortReason,
|
|
466
|
+
transport_abort_bytes: transportAbortBytes,
|
|
467
|
+
transport_abort_method: transportAbortMethod,
|
|
468
|
+
max_monitor_frame_bytes: maxMonitorFrameBytes,
|
|
469
|
+
max_monitor_frame_method: maxMonitorFrameMethod,
|
|
470
|
+
output_pause_count: outputPauseCount,
|
|
377
471
|
terminal_exit_code: terminalExitCode,
|
|
378
472
|
handoff_count: handoffCount,
|
|
379
473
|
handoff_attempt_count: handoffAttemptCount,
|
|
@@ -523,11 +617,12 @@ async function main() {
|
|
|
523
617
|
if (line.length === 0) return;
|
|
524
618
|
const connection = generation.connection;
|
|
525
619
|
if (!connection || connection.retiring) return;
|
|
526
|
-
if (line.length >
|
|
527
|
-
abortGeneration(generation, 'monitor_frame_limit');
|
|
620
|
+
if (line.length > MAX_MONITOR_RESPONSE_BYTES) {
|
|
621
|
+
abortGeneration(generation, 'monitor_frame_limit', line.length);
|
|
528
622
|
return;
|
|
529
623
|
}
|
|
530
624
|
let parsed;
|
|
625
|
+
let responseMethod;
|
|
531
626
|
try {
|
|
532
627
|
parsed = JSON.parse(line.toString('utf8'));
|
|
533
628
|
const isInitializeResponse =
|
|
@@ -587,6 +682,7 @@ async function main() {
|
|
|
587
682
|
connection.pendingRequests.has(parsed.id)
|
|
588
683
|
) {
|
|
589
684
|
const method = connection.pendingRequests.get(parsed.id);
|
|
685
|
+
responseMethod = method;
|
|
590
686
|
connection.pendingRequests.delete(parsed.id);
|
|
591
687
|
if (parsed.error === undefined && PRIMARY_DISCOVERY.has(method)) {
|
|
592
688
|
connection.discoverySatisfied = true;
|
|
@@ -617,8 +713,13 @@ async function main() {
|
|
|
617
713
|
: 'native_internal_error';
|
|
618
714
|
}
|
|
619
715
|
}
|
|
716
|
+
if (line.length > maxMonitorFrameBytes) {
|
|
717
|
+
maxMonitorFrameBytes = line.length;
|
|
718
|
+
maxMonitorFrameMethod = responseMethod ?? null;
|
|
719
|
+
}
|
|
620
720
|
} catch {
|
|
621
|
-
|
|
721
|
+
abortGeneration(generation, 'monitor_frame_invalid', line.length, responseMethod);
|
|
722
|
+
return;
|
|
622
723
|
}
|
|
623
724
|
if (connection.isAuxiliary && parsed === null) {
|
|
624
725
|
auxiliaryUnknownResponseCount += 1;
|
|
@@ -626,11 +727,14 @@ async function main() {
|
|
|
626
727
|
return;
|
|
627
728
|
}
|
|
628
729
|
if (connection.socket.readyState !== WebSocket.OPEN) return;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
730
|
+
pauseMonitor(generation);
|
|
731
|
+
connection.socket.send(line.toString('utf8'), { binary: false }, (error) => {
|
|
732
|
+
if (error && !generation.retiring && !shuttingDown) {
|
|
733
|
+
abortGeneration(generation, 'bridge_output_failed', line.length, responseMethod);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
resumeMonitor(generation);
|
|
737
|
+
});
|
|
634
738
|
};
|
|
635
739
|
const handleClientMessage = (connection, data, isBinary) => {
|
|
636
740
|
if (shuttingDown) return;
|
|
@@ -16,7 +16,7 @@ export const MAX_LOG_FILES = 3;
|
|
|
16
16
|
const LOCK_WAIT_MS = 250;
|
|
17
17
|
const LOCK_STALE_MS = 30_000;
|
|
18
18
|
const ADMISSION_MS = 60 * 60 * 1000;
|
|
19
|
-
export const ADAPTER_VERSION = '0.3.0-beta.
|
|
19
|
+
export const ADAPTER_VERSION = '0.3.0-beta.16';
|
|
20
20
|
const MAX_DIAGNOSTIC_COUNT = 10_000_000;
|
|
21
21
|
const MAX_DIAGNOSTIC_BYTES = 128 * 1024 * 1024;
|
|
22
22
|
const CHECKPOINT_BACKENDS = new Set(['native_store', 'private_file']);
|
|
@@ -136,6 +136,7 @@ const DIAGNOSTIC_STAGES = new Set([
|
|
|
136
136
|
'compact_entry',
|
|
137
137
|
'compact_exit',
|
|
138
138
|
'native_summary_proof',
|
|
139
|
+
'native_fallback_proof',
|
|
139
140
|
'native_error',
|
|
140
141
|
]);
|
|
141
142
|
const DIAGNOSTIC_CATEGORIES = new Set([
|
package/dist/cli.mjs
CHANGED
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
stateRoot,
|
|
16
16
|
tokenDigest,
|
|
17
17
|
} from './state.mjs';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
captureLockPlatformCheck,
|
|
20
|
+
nativeVersion,
|
|
21
|
+
platformCheck,
|
|
22
|
+
resolveNative,
|
|
23
|
+
} from './platform.mjs';
|
|
19
24
|
import { launchClaude, launchCodex } from './launch.mjs';
|
|
20
25
|
import { claudeCompatibilityError, resolveClaudeCompatibility } from './claude-compatibility.mjs';
|
|
21
26
|
import { codexCompatibilityError, resolveCodexCompatibility } from './codex-compatibility.mjs';
|
|
@@ -24,7 +29,7 @@ import { codexPolicyJson, makePolicy, publicPolicy, isProfile } from './policy.m
|
|
|
24
29
|
import { readDiagnostics } from './claude-observation.mjs';
|
|
25
30
|
import { readCodexDiagnostics } from './codex-diagnostics.mjs';
|
|
26
31
|
|
|
27
|
-
const VERSION = '0.3.0-beta.
|
|
32
|
+
const VERSION = '0.3.0-beta.16';
|
|
28
33
|
const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
|
|
29
34
|
const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url));
|
|
30
35
|
const LEGACY_DISCLOSURE_VERSION = 'aibrain-paid-context-v1-2026-10-09';
|
|
@@ -531,6 +536,7 @@ async function doctor(json) {
|
|
|
531
536
|
};
|
|
532
537
|
const needsClaude = config?.clientSelection === 'claude' || config?.clientSelection === 'both';
|
|
533
538
|
const needsCodex = config?.clientSelection === 'codex' || config?.clientSelection === 'both';
|
|
539
|
+
const captureLockPlatform = needsCodex ? captureLockPlatformCheck() : null;
|
|
534
540
|
const nativeReady =
|
|
535
541
|
(!needsClaude ||
|
|
536
542
|
(native.claude !== null &&
|
|
@@ -539,6 +545,7 @@ async function doctor(json) {
|
|
|
539
545
|
(!needsCodex ||
|
|
540
546
|
(native.codex !== null &&
|
|
541
547
|
native.node !== null &&
|
|
548
|
+
captureLockPlatform?.ok === true &&
|
|
542
549
|
(codexCompatibility?.status === 'qualified' ||
|
|
543
550
|
codexCompatibility?.status === 'protocol_match')));
|
|
544
551
|
const result = {
|
|
@@ -560,6 +567,10 @@ async function doctor(json) {
|
|
|
560
567
|
codexCompatibility: codexCompatibility?.status ?? 'unavailable',
|
|
561
568
|
codexCompatibilityReason: codexCompatibility?.reason ?? 'codex_not_found',
|
|
562
569
|
codexCompatibilityVersion: codexCompatibility?.version ?? null,
|
|
570
|
+
captureLockBackend:
|
|
571
|
+
captureLockPlatform?.ok === true
|
|
572
|
+
? 'available'
|
|
573
|
+
: (captureLockPlatform?.code ?? 'not_required'),
|
|
563
574
|
nativeReady,
|
|
564
575
|
context: publicPolicy(policy),
|
|
565
576
|
telemetry: {
|
|
@@ -2,11 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { chmod, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
5
|
-
import { constants } from 'node:fs';
|
|
5
|
+
import { constants, readFileSync } from 'node:fs';
|
|
6
6
|
import { join, resolve } from 'node:path';
|
|
7
7
|
|
|
8
8
|
export const CODEX_DIAGNOSTIC_SCHEMA_VERSION = 1;
|
|
9
|
-
|
|
9
|
+
const PACKAGE_VERSION_PATTERN =
|
|
10
|
+
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/u;
|
|
11
|
+
function packageVersion() {
|
|
12
|
+
for (const packageUrl of [
|
|
13
|
+
new URL('./package.json', import.meta.url),
|
|
14
|
+
new URL('../package.json', import.meta.url),
|
|
15
|
+
]) {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(readFileSync(packageUrl, 'utf8'));
|
|
18
|
+
if (typeof value?.version === 'string' && PACKAGE_VERSION_PATTERN.test(value.version))
|
|
19
|
+
return value.version;
|
|
20
|
+
} catch {
|
|
21
|
+
// Source and packaged dist use different relative package metadata locations.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
throw new Error('codex_diagnostics_package_version_unavailable');
|
|
25
|
+
}
|
|
26
|
+
export const CODEX_DIAGNOSTIC_ADAPTER_VERSION = packageVersion();
|
|
10
27
|
export const CODEX_DIAGNOSTIC_MAX_LOG_BYTES = 512 * 1024;
|
|
11
28
|
export const CODEX_DIAGNOSTIC_MAX_LOG_FILES = 3;
|
|
12
29
|
export const CODEX_DIAGNOSTIC_MAX_RECORDS = 128;
|
|
@@ -68,6 +85,10 @@ const REASONS = new Set([
|
|
|
68
85
|
'capture_item_count_exceeded',
|
|
69
86
|
'private_storage_failed',
|
|
70
87
|
'storage_capacity_exceeded',
|
|
88
|
+
'capture_lock_timeout',
|
|
89
|
+
'capture_lock_legacy',
|
|
90
|
+
'capture_lock_owner_unverifiable',
|
|
91
|
+
'capture_lock_backend_unavailable',
|
|
71
92
|
'attached',
|
|
72
93
|
'uncertain',
|
|
73
94
|
'disconnected',
|
package/dist/launch.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { access, chmod, lstat, mkdir, mkdtemp, open, rm, writeFile } from 'node:
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { constants } from 'node:fs';
|
|
6
6
|
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
7
|
-
import { nativeVersion, resolveNative } from './platform.mjs';
|
|
7
|
+
import { captureLockPlatformCheck, nativeVersion, resolveNative } from './platform.mjs';
|
|
8
8
|
import { claudeCompatibilityError, resolveClaudeCompatibility } from './claude-compatibility.mjs';
|
|
9
9
|
import { codexCompatibilityError, resolveCodexCompatibility } from './codex-compatibility.mjs';
|
|
10
10
|
import {
|
|
@@ -565,6 +565,8 @@ export async function launchClaude(config, root, args) {
|
|
|
565
565
|
export async function launchCodex(config, root, args) {
|
|
566
566
|
const validated = assertConfig(config);
|
|
567
567
|
assertCodexArgs(args);
|
|
568
|
+
const captureLockPlatform = captureLockPlatformCheck();
|
|
569
|
+
if (!captureLockPlatform.ok) throw new Error(captureLockPlatform.code);
|
|
568
570
|
const executable = resolveNative('codex');
|
|
569
571
|
const node = resolveNative('node');
|
|
570
572
|
if (!executable || !node) throw new Error('codex_or_node_not_found');
|
package/dist/package.json
CHANGED
package/dist/platform.mjs
CHANGED
|
@@ -23,6 +23,19 @@ export function platformCheck() {
|
|
|
23
23
|
return { ok: true };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** @param {NodeJS.ProcessEnv} [environment] */
|
|
27
|
+
export function captureLockPlatformCheck(environment = process.env) {
|
|
28
|
+
const platform = platformCheck();
|
|
29
|
+
if (!platform.ok) return platform;
|
|
30
|
+
if (resolveNative('flock', environment) === null)
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
code: 'CODEX_CAPTURE_LOCK_BACKEND_UNAVAILABLE',
|
|
34
|
+
message: 'Codex capture requires util-linux flock on Linux/WSL.',
|
|
35
|
+
};
|
|
36
|
+
return { ok: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
/** @param {string} name @param {NodeJS.ProcessEnv} [environment] */
|
|
27
40
|
export function resolveNative(name, environment = process.env) {
|
|
28
41
|
if (!/^[A-Za-z0-9._+-]+$/u.test(name)) return null;
|