@brunobracaioli/aibrain 0.3.0-beta.15 → 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 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.13';
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<void> {
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
- fallbackMatch = await nativeSummaryViewMatch(
4941
- messages,
4942
- expectedCount,
4943
- proof.expected_hash,
4944
- proof.expected_message_hashes,
4945
- proof.anchor_counts,
4946
- proof.anchor_hashes,
4947
- );
4948
- const fallbackOffset =
4949
- fallbackMatch === 'active_suffix'
4950
- ? await nativeSummaryMatchedOffset(
4951
- messages,
4952
- expectedCount,
4953
- proof.expected_hash,
4954
- proof.expected_message_hashes,
4955
- proof.anchor_counts,
4956
- proof.anchor_hashes,
4957
- )
4958
- : undefined;
4959
- fallbackProofFields = {
4960
- ...fallbackProofFields,
4961
- proof_match: fallbackMatch,
4962
- ...(fallbackMatch === 'exact' ? { proof_match_offset: 0 } : {}),
4963
- ...(fallbackOffset === undefined ? {} : { proof_match_offset: fallbackOffset }),
4964
- };
4965
- verified = fallbackMatch === 'exact' || fallbackMatch === 'active_suffix';
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
- if (deferMismatch) return;
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
- await confirmPendingProof(api, proofRuntime, true);
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
- await confirmPendingProof(api, proofRuntime);
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
- await queueNativeFallbackProof(
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 (info.isSymbolicLink() || !info.isFile() || (info.mode & 0o077) !== 0)
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
- async function withCaptureStoreLock(
848
- directory,
849
- task,
850
- fsOps = captureFs,
851
- {
852
- now = Date.now,
853
- sleep = (milliseconds) =>
854
- new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)),
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 captureCapacityError({ totalBytes: 0, managedFiles: [] });
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));
@@ -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.13';
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 { nativeVersion, platformCheck, resolveNative } from './platform.mjs';
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.15';
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: {
@@ -85,6 +85,10 @@ const REASONS = new Set([
85
85
  'capture_item_count_exceeded',
86
86
  'private_storage_failed',
87
87
  'storage_capacity_exceeded',
88
+ 'capture_lock_timeout',
89
+ 'capture_lock_legacy',
90
+ 'capture_lock_owner_unverifiable',
91
+ 'capture_lock_backend_unavailable',
88
92
  'attached',
89
93
  'uncertain',
90
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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@brunobracaioli/aibrain",
3
- "version": "0.3.0-beta.15",
3
+ "version": "0.3.0-beta.16",
4
4
  "type": "module"
5
5
  }
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brunobracaioli/aibrain",
3
- "version": "0.3.0-beta.15",
3
+ "version": "0.3.0-beta.16",
4
4
  "private": false,
5
5
  "description": "AI Brain assisted context beta client for Claude Code and Codex",
6
6
  "license": "UNLICENSED",