@quantiya/codevibe-codex-plugin 2.0.48 → 2.0.49

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.
@@ -250,6 +250,21 @@ interface RegisteredShadowRoot extends AnchoredFsIdentity {
250
250
  canonicalPath?: string;
251
251
  birthtimeNs?: string;
252
252
  }
253
+ /**
254
+ * FU-1 — a registered physical-copy root whose pathname is gone and whose bytes could NOT be proven reclaimed.
255
+ *
256
+ * Carries the two paths the user needs to act, because the generic surfacing cannot: the implementor-round
257
+ * classifier returns a closed label and never the raw message, and the durable log projection strips message,
258
+ * name, code and path. Both are deliberate wire-safety controls (#SR-2), so the recovery instructions travel as
259
+ * STRUCTURED FIELDS on the error rather than as text the classifier would have to parse or leak. Rendering these
260
+ * two paths in full — desktop, hosted events and durable diagnostics — is FU1-D2, an owner decision made with the
261
+ * trade-off stated: they are CodeVibe-registered root paths, never raw error text and never user content.
262
+ */
263
+ export declare class StaleShadowRootError extends Error {
264
+ readonly stalePath: string;
265
+ readonly registryPath: string;
266
+ constructor(stalePath: string, registryPath: string);
267
+ }
253
268
  /**
254
269
  * Durable quota-scope registry. Every process sharing `stateRoot` registers its
255
270
  * physical shadow root before copying, so the aggregate scan cannot be split by
@@ -30495,13 +30495,18 @@ async function hasUnreclaimedExposureRetention(stateRoot) {
30495
30495
  }
30496
30496
  return !1;
30497
30497
  }
30498
+ var StaleShadowRootError = class extends Error {
30499
+ constructor(stalePath, registryPath) {
30500
+ super("registered physical-copy root disappeared while quota authority is retained"), this.name = "StaleShadowRootError", this.stalePath = stalePath, this.registryPath = registryPath;
30501
+ }
30502
+ };
30498
30503
  function isSameRegisteredRootAuthority(prior, current) {
30499
30504
  return prior.dev === current.dev && prior.ino === current.ino && prior.version === current.version && prior.canonicalPath === current.canonicalPath && prior.birthtimeNs === current.birthtimeNs;
30500
30505
  }
30501
30506
  function isSameRootAfterDeviceReincarnation(prior, current) {
30502
30507
  return prior.dev !== current.dev && prior.ino === current.ino && prior.version === current.version && prior.canonicalPath === current.canonicalPath && prior.birthtimeNs === current.birthtimeNs;
30503
30508
  }
30504
- async function collectDurablyReferencedShadowRoots(stateRoot, stateRootAuthority, maxStateMarkerBytes, registeredShadowRoots, afterTaskEntry, afterMarkerEntry, afterMarkerStat, allowUnrecognizedRegularFiles = !1, allowUnregisteredReferences = !1, allowConcurrentSiblingTeardown = !1) {
30509
+ async function collectDurablyReferencedShadowRoots(stateRoot, stateRootAuthority, maxStateMarkerBytes, registeredShadowRoots, afterTaskEntry, afterMarkerEntry, afterMarkerStat, allowUnrecognizedRegularFiles = !1, allowUnregisteredReferences = !1, allowConcurrentSiblingTeardown = !1, markerIndex) {
30505
30510
  let retryConcurrentSiblingTeardown = (error, target) => {
30506
30511
  let code = error?.code, message = error instanceof Error ? error.message : String(error), isSiblingChurn = code === "ENOENT" || message.includes("workspace root changed while opening") || message.includes("workspace root cwd identity mismatch") || message.includes("workspace root changed during authority capture") || message.includes("bounded-directory target identity mismatch") || message.includes("bounded-directory parent changed while opening") || message.includes("bounded-directory parent changed while descending") || message.includes("anchored read root authority unavailable") || message.includes("anchored read root identity mismatch") || message.includes("anchored read root incarnation mismatch") || message.includes("anchored read directory chain changed") || message.includes("anchored read directory changed while descending") || message.includes("anchored read cwd identity mismatch") || message.includes("anchored read file identity mismatch") || message.includes("anchored read file changed during read") || message.includes("anchored read file path changed") || message.includes("anchored read file changed after read");
30507
30512
  throw allowConcurrentSiblingTeardown && isSiblingChurn ? new Error(
@@ -30591,7 +30596,7 @@ async function collectDurablyReferencedShadowRoots(stateRoot, stateRootAuthority
30591
30596
  if (allowUnregisteredReferences) continue;
30592
30597
  throw new Error("physical-copy root reference marker names an unregistered root");
30593
30598
  }
30594
- referenced.add(markerRoot2);
30599
+ referenced.add(markerRoot2), markerIndex && markerIndex.set(markerRoot2, [...markerIndex.get(markerRoot2) ?? [], stateFile]);
30595
30600
  continue;
30596
30601
  }
30597
30602
  let preliminary = value;
@@ -30602,7 +30607,7 @@ async function collectDurablyReferencedShadowRoots(stateRoot, stateRootAuthority
30602
30607
  if (allowUnregisteredReferences) continue;
30603
30608
  throw new Error("physical-copy root exposure marker names an unregistered root");
30604
30609
  }
30605
- referenced.add(markerRoot);
30610
+ referenced.add(markerRoot), markerIndex && markerIndex.set(markerRoot, [...markerIndex.get(markerRoot) ?? [], stateFile]);
30606
30611
  }
30607
30612
  }
30608
30613
  return referenced;
@@ -30640,7 +30645,7 @@ async function registerAndListQuotaShadowRoots(stateRoot, stateRootIdentity, cur
30640
30645
  else
30641
30646
  throw new Error("physical-copy root changed identity while quota authority is retained");
30642
30647
  if (!priorCurrent && mode === "register-current" && consumeMetadataPath(registryBudget, currentShadowRoot, "physical-copy root registry"), mode === "register-current" && byPath.set(currentShadowRoot, { path: currentShadowRoot, ...currentShadowRootIdentity }), byPath.size > 256) throw new Error("physical-copy root registry exceeds 256 roots");
30643
- let retained = [], scanBudget = createPathBudget(), referencedShadowRoots = null, hasPhysicalCopyEntry = async (candidate) => {
30648
+ let retained = [], scanBudget = createPathBudget(), referencedShadowRoots = null, vanishedMarkerIndex = null, hasPhysicalCopyEntry = async (candidate) => {
30644
30649
  let liveRoot;
30645
30650
  try {
30646
30651
  liveRoot = await captureWorkspaceRootAuthority(candidate.path);
@@ -30668,7 +30673,79 @@ async function registerAndListQuotaShadowRoots(stateRoot, stateRootIdentity, cur
30668
30673
  afterQuotaReferenceTaskEntry,
30669
30674
  afterQuotaReferenceMarkerEntry,
30670
30675
  afterQuotaReferenceMarkerStat
30671
- ), !referencedShadowRoots.has(candidate.path));
30676
+ ), !referencedShadowRoots.has(candidate.path)), vanishedRootInodeSurvives = async (candidate) => {
30677
+ let parent = path26.dirname(candidate.path);
30678
+ if (parent === candidate.path) return !0;
30679
+ try {
30680
+ await fs19.lstat(parent);
30681
+ } catch (err) {
30682
+ return err.code !== "ENOENT";
30683
+ }
30684
+ let entries;
30685
+ try {
30686
+ entries = await readDirectoryBounded(
30687
+ parent,
30688
+ scanBudget,
30689
+ "physical-copy root reconcile parent scan",
30690
+ parent,
30691
+ { includeStats: !0 }
30692
+ );
30693
+ } catch {
30694
+ return !0;
30695
+ }
30696
+ for (let entry of entries) {
30697
+ let stat13 = boundedDirectoryStat(entry);
30698
+ if (stat13 && stat13.ino === candidate.ino) return !0;
30699
+ }
30700
+ return !1;
30701
+ }, canRetireVanishedRoot = async (candidate) => {
30702
+ if (await vanishedRootInodeSurvives(candidate)) return !1;
30703
+ let referenced = referencedShadowRoots, markers = vanishedMarkerIndex;
30704
+ if ((referenced === null || markers === null) && (markers = /* @__PURE__ */ new Map(), referenced = await collectDurablyReferencedShadowRoots(
30705
+ stateRoot,
30706
+ stateRootIdentity,
30707
+ maxStateMarkerBytes,
30708
+ new Set(byPath.keys()),
30709
+ afterQuotaReferenceTaskEntry,
30710
+ afterQuotaReferenceMarkerEntry,
30711
+ afterQuotaReferenceMarkerStat,
30712
+ // TOLERANT on purpose. (Stage 1 F4: an earlier version of this comment said "unlike the
30713
+ // retirement-mode call above" — that was wrong, and had been wrong on main too: the retirement-mode
30714
+ // call already passes all three flags true. The scan that stays STRICT is the one behind
30715
+ // `canRetireReincarnatedRoot`.)
30716
+ //
30717
+ // This scan answers one narrow question — is the DEAD root still referenced? — and it runs on a state root
30718
+ // shared by every CodeVibe session and agent on the machine. Strict mode aborts the whole reconcile on
30719
+ // anything unexpected anywhere in that directory, which in practice means: a stray regular file (the
30720
+ // registry's own `.bak` copies — including the ones `codevibe doctor --repair` used to write there before
30721
+ // Stage 1 F5 moved every write it makes to `~/.codevibe/shadow-root-backups/`), a marker naming a
30722
+ // root some other agent never cleaned up (six such directories existed on the dogfood machine), or a
30723
+ // sibling session tearing down concurrently. Any of those would re-wedge the user with
30724
+ // `physical-copy root reference state entry changed type` and no way out — which is exactly what the
30725
+ // real-surface E2E hit.
30726
+ //
30727
+ // Tolerating them cannot lose quota authority: an entry this scan skips is simply not counted as a
30728
+ // reference, and the retirement still requires the root's inode to be proven freed first.
30729
+ !0,
30730
+ // allowUnrecognizedRegularFiles
30731
+ !0,
30732
+ // allowUnregisteredReferences
30733
+ !0,
30734
+ // allowConcurrentSiblingTeardown — surfaces as a retryable enumeration change
30735
+ markers
30736
+ ), referencedShadowRoots = referenced, vanishedMarkerIndex = markers), !referenced.has(candidate.path)) return !0;
30737
+ for (let markerFile of markers.get(candidate.path) ?? []) {
30738
+ await anchoredRemoveFile(markerFile);
30739
+ let taskStateDir = path26.dirname(markerFile);
30740
+ try {
30741
+ await anchoredRemoveEmptyDirectory(taskStateDir, void 0, { expectedParent: stateRootIdentity });
30742
+ } catch (err) {
30743
+ let code = err.code;
30744
+ if (code !== "ENOTEMPTY" && code !== "ENOENT" && code !== "EEXIST") throw err;
30745
+ }
30746
+ }
30747
+ return !0;
30748
+ };
30672
30749
  if (mode === "retire-current-if-unused") {
30673
30750
  let candidate = priorCurrent;
30674
30751
  if (await assertWorkspaceRootAuthority(
@@ -30731,8 +30808,8 @@ async function registerAndListQuotaShadowRoots(stateRoot, stateRootIdentity, cur
30731
30808
  )).some((entry) => entry.isDirectory() && !entry.isSymbolicLink() && (SHADOW_DIR_RE.test(entry.name) || SHADOW_STAGING_DIR_RE.test(entry.name) || SHADOW_TREE_TOMBSTONE_RE.test(entry.name))) ? retained.push(candidate) : reincarnatedDevice !== null && (await canRetireReincarnatedRoot(candidate) || retained.push(candidate));
30732
30809
  } catch (err) {
30733
30810
  if (err.code === "ENOENT") {
30734
- if (!await canRetireReincarnatedRoot(candidate))
30735
- throw new Error("registered physical-copy root disappeared while quota authority is retained");
30811
+ if (!await canRetireReincarnatedRoot(candidate) && !await canRetireVanishedRoot(candidate))
30812
+ throw new StaleShadowRootError(candidate.path, registryPath);
30736
30813
  byPath.delete(candidate.path);
30737
30814
  continue;
30738
30815
  }
@@ -52157,6 +52234,8 @@ var AUTHORITY_REFUSAL_LABEL = {
52157
52234
  function classifyImplementorRoundFailure(err) {
52158
52235
  if (err instanceof AuthorityError)
52159
52236
  return AUTHORITY_REFUSAL_LABEL[err.refusal.category] ?? "the implementor was refused authority";
52237
+ if (err instanceof StaleShadowRootError)
52238
+ return `the recorded workspace-copy folder ${err.stalePath} is missing and its contents could not be accounted for (they may have been moved). Run \`codevibe doctor\` to review and clear it`;
52160
52239
  let msg = err instanceof Error ? err.message.toLowerCase() : "";
52161
52240
  return msg.includes("enoent") || msg.includes("spawn") || msg.includes("command not found") || msg.includes("not installed") ? "the coding agent could not be started" : "an unexpected error occurred";
52162
52241
  }
@@ -55236,7 +55315,12 @@ ${section}`);
55236
55315
  }
55237
55316
  logger.warn("[QuorumLoop] implementor round failed", {
55238
55317
  gateId,
55239
- ...errorShapeOnly(err)
55318
+ ...errorShapeOnly(err),
55319
+ // FU-1 — the ONE exception to the shape-only projection, decided as FU1-D2. These are CodeVibe-registered
55320
+ // root pathnames carried as structured fields, never raw error text and never user content; without them
55321
+ // the log records `messageBytes` and nothing else, and a machine-wide outage is undiagnosable after the
55322
+ // fact (it was, twice, on 2026-09-17).
55323
+ ...err instanceof StaleShadowRootError ? { staleShadowRoot: err.stalePath, shadowRootRegistry: err.registryPath } : {}
55240
55324
  });
55241
55325
  let reason = classifyImplementorRoundFailure(err);
55242
55326
  this.surfaceHalt(`The task could not proceed \u2014 ${reason}.`);
@@ -0,0 +1,44 @@
1
+ interface RegisteredRoot {
2
+ path: string;
3
+ dev?: string;
4
+ ino?: string;
5
+ canonicalPath?: string;
6
+ birthtimeNs?: string;
7
+ version?: number;
8
+ }
9
+ /** What the registry says about one root, and what the filesystem says back. */
10
+ export interface RootDiagnosis {
11
+ root: RegisteredRoot;
12
+ /** The registered pathname resolves to a directory right now. */
13
+ present: boolean;
14
+ /**
15
+ * The recorded inode was found among the root's siblings — the folder was renamed and its copies still exist,
16
+ * so the entry is still real quota authority. `undefined` when the parent EXISTS but could not be enumerated,
17
+ * which is a refusal to guess rather than an answer; a parent that is gone reads `false` (F1-10).
18
+ */
19
+ inodeSurvives?: boolean;
20
+ /** Durable markers that name this root (`dirname(shadowDir)` / `dirname(finalShadowDir)`). */
21
+ markers: string[];
22
+ verdict: 'healthy' | 'reconcilable' | 'bytes-may-exist' | 'unknown';
23
+ }
24
+ /** The product's own state root — never a second spelling of it (Stage 1 F7). */
25
+ export declare function shadowStateRoot(): string;
26
+ /** Read the registry. A missing or unreadable registry is reported, never repaired. */
27
+ export declare function readRegistry(stateRoot: string): {
28
+ roots: RegisteredRoot[];
29
+ raw: string;
30
+ } | null;
31
+ /** Every durable marker under the state root, mapped to the root it names. */
32
+ export declare function collectMarkers(stateRoot: string): Map<string, string[]>;
33
+ /**
34
+ * Does the recorded inode still exist among the root's siblings?
35
+ *
36
+ * `false` — proven gone, so the entry is reconcilable. `true` — a sibling still holds it, so the bytes are real.
37
+ * `undefined` — the parent exists but could not be READ, which is a refusal to guess rather than an answer.
38
+ */
39
+ export declare function inodeSurvivesInParent(root: RegisteredRoot): boolean | undefined;
40
+ export declare function diagnose(stateRoot: string, roots: RegisteredRoot[]): RootDiagnosis[];
41
+ export declare function runDoctorCli(argv: string[], opts?: {
42
+ stateRoot?: string;
43
+ }): Promise<number>;
44
+ export {};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-core",
3
- "version": "2.0.41",
3
+ "version": "2.0.42",
4
4
  "description": "Core library for CodeVibe plugins - shared keychain, crypto, AppSync, and auth functionality",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,7 +18,7 @@
18
18
  "typecheck": "tsc --noEmit",
19
19
  "emit-types": "tsc -p tsconfig.types.json --emitDeclarationOnly",
20
20
  "build": "rm -rf dist && npm run emit-types && esbuild src/index.ts --bundle --platform=node --target=node18 --minify --packages=external --define:__CODEVIBE_TEST_BUILD__=false --outfile=dist/index.js && esbuild src/orchestration-shell/cli.ts --bundle --platform=node --target=node18 --minify-syntax --packages=external --define:__CODEVIBE_TEST_BUILD__=false --outfile=dist/orchestration-shell/cli.js --banner:js=\"#!/usr/bin/env node\" && esbuild src/orchestration-shell/web/web-extract-worker.ts --bundle --platform=node --target=node18 --minify --packages=external --outfile=dist/orchestration-shell/web-extract-worker.js && npm run verify-no-test-hooks && npm run verify-no-test-types",
21
- "verify-no-test-hooks": "if grep -ERl 'CODEVIBE_TEST_FORCE_QUOTA_FAIL|CODEVIBE_TEST_FORCE_IMPLEMENTOR_AGENT|CODEVIBE_ANCHORED_PUBLICATION_AUTHORITY_ROOT|codevibe-anchored-publications-|cv-publication-authority-vitest-|codevibe\\.test\\.publication-authority-|setPublicationAuthorityRootForTestBuild|__CODEVIBE_TEST_BUILD__|_onBeforeLinkForTesting|_onBeforeSummariesLockForTesting|__codevibe_test_on_before_link|__codevibe_test_on_before_summaries_lock' dist; then echo 'ERROR: a test-only hook leaked into a published artifact (prod-unreachability invariant)'; exit 1; fi; echo 'OK no test-only hooks in published artifacts'",
21
+ "verify-no-test-hooks": "if grep -ERl 'CODEVIBE_TEST_FORCE_QUOTA_FAIL|CODEVIBE_TEST_FORCE_IMPLEMENTOR_AGENT|CODEVIBE_ANCHORED_PUBLICATION_AUTHORITY_ROOT|codevibe-anchored-publications-|cv-publication-authority-vitest-|codevibe\\.test\\.publication-authority-|setPublicationAuthorityRootForTestBuild|__CODEVIBE_TEST_BUILD__|_onBeforeLinkForTesting|_onBeforeSummariesLockForTesting|__codevibe_test_on_before_link|__codevibe_test_on_before_summaries_lock' dist; then echo 'ERROR: a test-only hook leaked into a published artifact (prod-unreachability invariant)'; exit 1; fi; echo 'OK \u2014 no test-only hooks in published artifacts'",
22
22
  "verify-no-test-types": "if find dist -type f \\( -path '*/__tests__/*' -o -name '*.test.d.ts' -o -name 'test-publication-authority-setup.d.ts' \\) | grep -q .; then echo 'verify-no-test-types: test declarations present in dist (tsconfig.types.json must exclude test sources)' >&2; exit 1; fi",
23
23
  "clean": "rm -rf dist",
24
24
  "prepublishOnly": "npm run build",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-codex-plugin",
3
- "version": "2.0.48",
4
- "description": "Control OpenAI Codex CLI from your iPhone and Android real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
3
+ "version": "2.0.49",
4
+ "description": "Control OpenAI Codex CLI from your iPhone and Android \u2014 real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
5
5
  "main": "dist/server.js",
6
6
  "codevibe": {
7
7
  "companionLauncher": "./libexec/companion-launcher"
@@ -47,7 +47,7 @@
47
47
  "node": ">=22.0.0"
48
48
  },
49
49
  "dependencies": {
50
- "@quantiya/codevibe-core": "^2.0.41",
50
+ "@quantiya/codevibe-core": "^2.0.42",
51
51
  "@quantiya/quorum-core": "^1.0.1",
52
52
  "chokidar": "^4.0.0",
53
53
  "dotenv": "^16.6.1",