@llblab/pi-kit 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/BACKLOG.md +5 -1
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +9 -7
  4. package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
  5. package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
  6. package/node_modules/@llblab/pi-actors/LICENSE +21 -0
  7. package/node_modules/@llblab/pi-actors/README.md +1 -1
  8. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
  9. package/node_modules/@llblab/pi-actors/package.json +4 -3
  10. package/node_modules/@llblab/pi-claude-usage/AGENTS.md +6 -3
  11. package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +2 -1
  12. package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +8 -0
  13. package/node_modules/@llblab/pi-claude-usage/README.md +48 -3
  14. package/node_modules/@llblab/pi-claude-usage/index.ts +8 -1159
  15. package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
  16. package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
  17. package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
  18. package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
  19. package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
  20. package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
  21. package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
  22. package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
  23. package/node_modules/@llblab/pi-claude-usage/package.json +9 -5
  24. package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
  25. package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
  26. package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
  27. package/node_modules/@llblab/pi-clean-room/README.md +1 -1
  28. package/node_modules/@llblab/pi-clean-room/package.json +3 -2
  29. package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
  30. package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
  31. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
  32. package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
  33. package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
  34. package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
  35. package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
  36. package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
  37. package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
  38. package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
  39. package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
  40. package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
  41. package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
  42. package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
  43. package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
  44. package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
  45. package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
  46. package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
  47. package/node_modules/@llblab/pi-command-fast/README.md +42 -0
  48. package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
  49. package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
  50. package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
  51. package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
  52. package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
  53. package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
  54. package/node_modules/@llblab/pi-command-fast/package.json +49 -0
  55. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
  56. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
  57. package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
  58. package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
  59. package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
  60. package/node_modules/@llblab/pi-state-flow/AGENTS.md +6 -5
  61. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -2
  62. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +7 -1
  63. package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
  64. package/node_modules/@llblab/pi-state-flow/README.md +5 -5
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +1 -1
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +1 -1
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +238 -46
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +16 -5
  70. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +10 -1
  71. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +60 -1
  72. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +6 -0
  73. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +8 -5
  74. package/node_modules/@llblab/pi-state-flow/dist/package.json +10 -9
  75. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -7
  76. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
  77. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +4 -2
  78. package/node_modules/@llblab/pi-state-flow/docs/usage.md +14 -13
  79. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +2 -2
  80. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +221 -46
  81. package/node_modules/@llblab/pi-state-flow/lib/git.ts +14 -5
  82. package/node_modules/@llblab/pi-state-flow/lib/session.ts +52 -1
  83. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +7 -5
  84. package/node_modules/@llblab/pi-state-flow/package.json +10 -9
  85. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  86. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  87. package/node_modules/jsonc-parser/README.md +364 -0
  88. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  89. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  90. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  91. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  92. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  93. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  94. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  95. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  96. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  97. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  98. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  99. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  100. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  101. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  102. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  103. package/node_modules/jsonc-parser/package.json +37 -0
  104. package/package.json +7 -7
@@ -20,7 +20,7 @@ import { assistantToolCallCount, conciseDiagnostic, diagnosticText, finalizedAss
20
20
  import { readProjectedState, readStatePath } from "./query.js";
21
21
  import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.js";
22
22
  import { TemporalRuntime } from "./runtime.js";
23
- import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.js";
23
+ import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.js";
24
24
  import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker } from "./skills.js";
25
25
  import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint } from "./snapshot.js";
26
26
  import { emptyState, projectStateForModel } from "./state.js";
@@ -49,15 +49,18 @@ export default function stateFlowExtension(pi, options = {}) {
49
49
  let startActivation;
50
50
  let forkInitialization = false;
51
51
  let branchRestoration;
52
+ let deferredBranch;
52
53
  const restorationOperations = new Set();
53
54
  let responseReconciliation;
54
55
  let sharedInspectionLifetime = new AbortController();
56
+ let memoryToolLifetime = new AbortController();
55
57
  let completedRunAccepted = false;
56
58
  let turnAcceptedForBackup = false;
57
59
  let compactionPlan;
58
60
  let compactionInFlight = false;
59
61
  let compactionStopped = false;
60
- const compactionMarker = `state-flow-boundary:${randomUUID()}`;
62
+ const compactionPrefix = `state-flow-boundary:${randomUUID()}:`;
63
+ let compactionMarker;
61
64
  let passiveContinuation;
62
65
  let bootstrapContinuation;
63
66
  const contextProjection = new ContextProjection();
@@ -69,7 +72,7 @@ export default function stateFlowExtension(pi, options = {}) {
69
72
  const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
70
73
  const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
71
74
  let backupPending = false;
72
- const backupLifetime = new AbortController();
75
+ let backupLifetime = new AbortController();
73
76
  const backupOperations = new Set();
74
77
  let shuttingDown = false;
75
78
  let pushFailureNotified = false;
@@ -170,6 +173,11 @@ export default function stateFlowExtension(pi, options = {}) {
170
173
  sharedInspectionLifetime.abort(new Error("State Flow inspection was cancelled; select the scope again"));
171
174
  sharedInspectionLifetime = new AbortController();
172
175
  }
176
+ function cancelBackupWork() {
177
+ backupLifetime.abort();
178
+ backupLifetime = new AbortController();
179
+ backupPending = false;
180
+ }
173
181
  function cancelInferencePreparation() {
174
182
  inferencePreparation?.controller.abort();
175
183
  inferencePreparation = undefined;
@@ -259,22 +267,35 @@ export default function stateFlowExtension(pi, options = {}) {
259
267
  async function inspectTelegramState(scope) {
260
268
  if (!activeContext || shuttingDown)
261
269
  throw new Error("State Flow is not attached to an active session yet");
262
- if (scope === "session" || scope === "effective")
270
+ const ctx = activeContext;
271
+ const owner = sessionAddress(ctx).key;
272
+ const cwd = ctx.cwd;
273
+ const off = snapshot.config.mode === "off";
274
+ const privateScope = scope === "session" || scope === "effective";
275
+ if (!off && privateScope)
263
276
  assertSelectedBranchAvailable();
264
- const selected = runtime ??= createRuntime(activeContext);
277
+ // Off operator reads are disposable: they confer no selected-branch/cache authority.
278
+ const selected = off ? createRuntime(ctx) : runtime ??= createRuntime(ctx);
265
279
  const signal = sharedInspectionLifetime.signal;
266
- if (!selected.view || (scope !== "session" && !modePersistenceError))
280
+ let privateAvailable = true;
281
+ if (off && privateScope)
282
+ privateAvailable = await selected.refreshCurrentMemory(signal) !== undefined;
283
+ else if (!selected.view || (scope !== "session" && !modePersistenceError))
267
284
  await selected.refreshShared(signal);
268
285
  signal.throwIfAborted();
269
- if (runtime !== selected)
286
+ if (activeContext !== ctx || ctx.cwd !== cwd || sessionAddress(ctx).key !== owner || (!off && runtime !== selected)) {
270
287
  throw new Error("State Flow inspection selection changed; select the scope again");
271
- if (scope === "session" || scope === "effective")
288
+ }
289
+ if (!privateAvailable)
290
+ throw new Error("Current State Flow session memory is unavailable");
291
+ if (!off && privateScope)
272
292
  assertSelectedBranchAvailable();
273
293
  if (!selected.view)
274
294
  throw new Error("State Flow temporal runtime is unavailable");
275
- installScopeStates();
276
- const state = scope === "effective" ? effectiveState : selected.readView(0, scope);
277
- return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: scopeRevisions(), signal };
295
+ if (!off)
296
+ installScopeStates();
297
+ const state = selected.readView(0, scope === "effective" ? undefined : scope);
298
+ return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: temporalScopeRevisions(selected.view), signal };
278
299
  }
279
300
  function syncStateFlowTools() {
280
301
  const active = pi.getActiveTools();
@@ -414,8 +435,10 @@ export default function stateFlowExtension(pi, options = {}) {
414
435
  return resolveSessionAddress(parent.file, parent.id, parent.timestamp);
415
436
  }
416
437
  /** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
417
- function restoreActiveBranch(ctx, sessionStartReason, notifyRecovery = true, startOwner) {
438
+ function restoreActiveBranch(ctx, sessionStartReason, notifyRecovery = true, startOwner, requestedMode) {
418
439
  contextProjection.reset();
440
+ memoryToolLifetime.abort();
441
+ memoryToolLifetime = new AbortController();
419
442
  cancelBranchRestoration();
420
443
  // Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
421
444
  if (!startOwner) {
@@ -429,6 +452,54 @@ export default function stateFlowExtension(pi, options = {}) {
429
452
  artifactReads.setCandidates([]);
430
453
  telegramStartPending = false;
431
454
  activeContext = ctx;
455
+ if (!startOwner) {
456
+ let mode = config.inactiveMode;
457
+ let adoptDefault = false;
458
+ let withoutRuntime = false;
459
+ let persistenceError;
460
+ let pendingFork = false;
461
+ try {
462
+ const branch = ctx.sessionManager.getBranch();
463
+ const discovery = discoverSnapshotData(branch);
464
+ withoutRuntime = discovery.candidates.length === 0 && discovery.errors.length === 0;
465
+ adoptDefault = withoutRuntime && isNewSession(sessionStartReason, branch);
466
+ pendingFork = hasPendingFork(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
467
+ const inherited = sessionStartReason === "fork" || pendingFork;
468
+ const policy = findBranchPolicy(branch, inherited ? undefined : ctx.sessionManager.getSessionId(), inherited || retainsPhysicalSessionProjection(sessionStartReason) ? PASSIVE_STOP_ENTRY_TYPE : undefined, config.inactiveMode);
469
+ mode = policy?.mode ?? (adoptDefault ? config.mode : config.inactiveMode);
470
+ persistenceError = inherited
471
+ ? findBranchPolicy(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode)?.persistenceError
472
+ : policy?.persistenceError;
473
+ }
474
+ catch {
475
+ // Off needs only policy; unavailable native evidence grants no memory authority.
476
+ }
477
+ if (mode === "off") {
478
+ cancelBackupWork();
479
+ const owner = sessionAddress(ctx).key;
480
+ const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
481
+ && retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
482
+ if (reason === "fork" && !pendingFork)
483
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { owner: ctx.sessionManager.getSessionId(), forkPending: true });
484
+ deferredBranch = { reason, owner, cwd: ctx.cwd };
485
+ runtime = undefined;
486
+ snapshot = emptySnapshot("off");
487
+ branchStartsWithoutRuntime = withoutRuntime;
488
+ selectedHistoryExpired = false;
489
+ modePersistenceError = persistenceError;
490
+ forkInitialization = reason === "fork";
491
+ installScopeStates();
492
+ artifactHints = {};
493
+ if (adoptDefault)
494
+ appendCheckpoint();
495
+ syncStateFlowTools();
496
+ updateUi(ctx);
497
+ return Promise.resolve();
498
+ }
499
+ if (pendingFork)
500
+ sessionStartReason = "fork";
501
+ }
502
+ deferredBranch = undefined;
432
503
  runtime = createRuntime(ctx);
433
504
  installScopeStates();
434
505
  branchStartsWithoutRuntime = false;
@@ -499,9 +570,12 @@ export default function stateFlowExtension(pi, options = {}) {
499
570
  snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
500
571
  : selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
501
572
  }
573
+ if (requestedMode)
574
+ snapshot = deactivateEpisode(snapshot, requestedMode);
502
575
  const pending = {
503
576
  controller: new AbortController(),
504
577
  awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
578
+ ...(requestedMode === undefined ? {} : { requestedMode }),
505
579
  };
506
580
  branchRestoration = pending;
507
581
  const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
@@ -660,6 +734,12 @@ export default function stateFlowExtension(pi, options = {}) {
660
734
  }
661
735
  return passive;
662
736
  }
737
+ function selectedContinuation(ctx, reason) {
738
+ if (!retainsPhysicalSessionProjection(reason) || !runtime?.view)
739
+ return undefined;
740
+ const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
741
+ return boundary === undefined ? undefined : createPassiveContinuation(projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext);
742
+ }
663
743
  function settleSelection(ctx, reason, notifyRecovery, skipped, selectedMemory) {
664
744
  const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
665
745
  if (failure !== undefined && notifyRecovery && !modePersistenceError) {
@@ -668,16 +748,12 @@ export default function stateFlowExtension(pi, options = {}) {
668
748
  else
669
749
  notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
670
750
  }
671
- if (selectedMemory && retainsPhysicalSessionProjection(reason) && runtime?.view) {
672
- const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
673
- if (boundary !== undefined) {
674
- const continuation = createPassiveContinuation(projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext);
675
- // Off retains the handoff boundary for a later Passive or Active choice but never projects it.
676
- if (!isActive())
677
- passiveContinuation = continuation;
678
- else if (snapshot.meta.bootstrap)
679
- bootstrapContinuation = continuation;
680
- }
751
+ const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
752
+ if (continuation !== undefined) {
753
+ if (!isActive())
754
+ passiveContinuation = continuation;
755
+ else if (snapshot.meta.bootstrap)
756
+ bootstrapContinuation = continuation;
681
757
  }
682
758
  if (notifyRecovery && isActive() && skipped > 0) {
683
759
  notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
@@ -764,6 +840,7 @@ export default function stateFlowExtension(pi, options = {}) {
764
840
  return new Text(separatedOutput(theme.fg("dim", formatPatchStateArguments(context.args))), 0, 0);
765
841
  },
766
842
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
843
+ signal = signal ? AbortSignal.any([signal, memoryToolLifetime.signal]) : memoryToolLifetime.signal;
767
844
  try {
768
845
  if (!memoryToolsAvailable())
769
846
  throw new Error("State Flow tools are off for this session");
@@ -844,11 +921,12 @@ export default function stateFlowExtension(pi, options = {}) {
844
921
  attempted = undefined;
845
922
  }
846
923
  const cause = diagnosticText(error);
847
- recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
848
- input: attempted,
849
- tool: PATCH_STATE_TOOL_NAME,
850
- toolCallId,
851
- });
924
+ if (!signal.aborted && memoryToolsAvailable())
925
+ recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
926
+ input: attempted,
927
+ tool: PATCH_STATE_TOOL_NAME,
928
+ toolCallId,
929
+ });
852
930
  throw separatedFailure(error);
853
931
  }
854
932
  },
@@ -886,7 +964,9 @@ export default function stateFlowExtension(pi, options = {}) {
886
964
  return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
887
965
  };
888
966
  try {
889
- if (!activeContext)
967
+ if (deferredBranch?.reason === "fork")
968
+ await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
969
+ else if (!activeContext)
890
970
  await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
891
971
  else if (branchRestoration?.operation)
892
972
  await waitForRecovery(branchRestoration.operation, signal);
@@ -925,7 +1005,8 @@ export default function stateFlowExtension(pi, options = {}) {
925
1005
  const initiallyActive = isActive();
926
1006
  let accepted = false;
927
1007
  let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
928
- const isCurrent = () => startActivation?.controller === pending && runtime === selected && !shuttingDown
1008
+ const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1009
+ const isCurrent = () => startActivation?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
929
1010
  && ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
930
1011
  && ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
931
1012
  const superseded = () => {
@@ -934,15 +1015,22 @@ export default function stateFlowExtension(pi, options = {}) {
934
1015
  };
935
1016
  try {
936
1017
  const activation = createRuntime(ctx);
937
- const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
1018
+ const deferred = deferredBranch;
1019
+ if (deferred) {
1020
+ const discovery = discoverSnapshotData(ctx.sessionManager.getBranch());
1021
+ const retained = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
1022
+ branchStartsWithoutRuntime = retained.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
1023
+ }
1024
+ // Off activation owns one cancellable current-head publication, not an independent read-only restore.
938
1025
  const result = await activation.withStartTransaction((current, publish) => {
939
1026
  signal.throwIfAborted();
940
1027
  if (!isCurrent())
941
1028
  throw new Error("State Flow activation selection changed while awaiting publication");
942
1029
  if (!current && !branchStartsWithoutRuntime)
943
1030
  throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
944
- const continuation = passiveContinuation ?? bootstrapContinuation;
945
- const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined;
1031
+ const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
1032
+ ? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
1033
+ const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
946
1034
  const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
947
1035
  const recoveredCurrent = selectedHistoryExpired;
948
1036
  const publication = publish(activated);
@@ -954,6 +1042,8 @@ export default function stateFlowExtension(pi, options = {}) {
954
1042
  selectedHistoryExpired = false;
955
1043
  modePersistenceError = undefined;
956
1044
  installScopeStates();
1045
+ const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
1046
+ deferredBranch = undefined;
957
1047
  clearRunTransient();
958
1048
  contextProjection.reset();
959
1049
  passiveContinuation = undefined;
@@ -963,6 +1053,10 @@ export default function stateFlowExtension(pi, options = {}) {
963
1053
  recordPolicyPublication(publication, ctx);
964
1054
  syncStateFlowTools();
965
1055
  updateUi(ctx);
1056
+ if (forkInitialization) {
1057
+ forkInitialization = false;
1058
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
1059
+ }
966
1060
  appendCheckpoint();
967
1061
  ctx.ui.notify(recoveredCurrent
968
1062
  ? "State Flow active from current session memory; unavailable historical state was not restored."
@@ -1012,9 +1106,11 @@ export default function stateFlowExtension(pi, options = {}) {
1012
1106
  updateUi(ctx);
1013
1107
  }
1014
1108
  function deactivateStateFlow(ctx, mode) {
1015
- cancelStartActivation();
1016
1109
  if (shuttingDown)
1017
1110
  return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
1111
+ if (mode === "off")
1112
+ return selectOff(ctx);
1113
+ cancelStartActivation();
1018
1114
  telegramStartPending = false;
1019
1115
  const persisting = inactivePersistence;
1020
1116
  if (persisting?.operation && !persisting.published) {
@@ -1030,7 +1126,7 @@ export default function stateFlowExtension(pi, options = {}) {
1030
1126
  if (restoring)
1031
1127
  restoring.requestedMode = mode;
1032
1128
  if (restoring?.awaitingAcceptance) {
1033
- // A mode change never cancels memory restoration or initialization.
1129
+ // Passive keeps independently owned restoration; Off withdraws it through selectOff.
1034
1130
  applyInactiveMode(ctx, mode);
1035
1131
  return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
1036
1132
  }
@@ -1043,13 +1139,99 @@ export default function stateFlowExtension(pi, options = {}) {
1043
1139
  cancelInactivePersistence();
1044
1140
  const pending = { controller: new AbortController() };
1045
1141
  inactivePersistence = pending;
1046
- const operation = persistInactiveMode(ctx, pending, mode);
1142
+ const operation = deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
1143
+ ? acquirePassiveMode(ctx, pending, deferredBranch.reason)
1144
+ : persistInactiveMode(ctx, pending, mode);
1047
1145
  pending.operation = operation;
1048
1146
  const finished = () => { if (inactivePersistence === pending)
1049
1147
  inactivePersistence = undefined; };
1050
1148
  void operation.then(finished, finished);
1051
1149
  return operation;
1052
1150
  }
1151
+ /** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
1152
+ function selectOff(ctx) {
1153
+ const address = sessionAddress(ctx);
1154
+ const owner = ctx.sessionManager.getSessionId();
1155
+ const branch = ctx.sessionManager.getBranch();
1156
+ const selected = runtime;
1157
+ const sameOwner = selected?.sessionKey === address.key && selected.cwd === ctx.cwd;
1158
+ const forkRecorded = hasPendingFork(branch, owner, PASSIVE_STOP_ENTRY_TYPE);
1159
+ const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork");
1160
+ const policy = findBranchPolicy(branch, owner, PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode);
1161
+ const sameDeferred = deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd;
1162
+ const current = sameOwner || sameDeferred ? snapshot : emptySnapshot("off");
1163
+ if (!sameOwner && !sameDeferred)
1164
+ modePersistenceError = policy?.persistenceError;
1165
+ const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
1166
+ const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
1167
+ const unfinished = current.meta.specification !== undefined || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted)
1168
+ || (isActive() && hasUncheckpointedConversation(branch));
1169
+ const at = incoming?.startedAt ?? Date.now();
1170
+ const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
1171
+ const preserveContext = modePersistenceError !== undefined || (incoming ? incoming.preserveContext : current.meta.bootstrap === true || (unfinished && from === undefined));
1172
+ const discovery = discoverSnapshotData(branch);
1173
+ const preRuntime = (sameOwner && branchStartsWithoutRuntime) || (discovery.candidates.length === 0 && discovery.errors.length === 0);
1174
+ let checkpoint;
1175
+ try {
1176
+ checkpoint = !modePersistenceError && current.meta.validation?.attempt !== 0
1177
+ ? preRuntime ? preRuntimeCheckpoint("off") : sameOwner && selected?.view ? selected.retainedCheckpoint(deactivateEpisode(current, "off")) : undefined
1178
+ : undefined;
1179
+ }
1180
+ catch {
1181
+ // An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
1182
+ }
1183
+ cancelStartActivation();
1184
+ cancelBranchRestoration();
1185
+ cancelInactivePersistence();
1186
+ cancelBackupWork();
1187
+ memoryToolLifetime.abort();
1188
+ memoryToolLifetime = new AbortController();
1189
+ clearRunTransient();
1190
+ contextProjection.reset();
1191
+ telegramStartPending = false;
1192
+ bootstrapContinuation = undefined;
1193
+ passiveContinuation = undefined;
1194
+ artifactInvalidations = [];
1195
+ artifactHints = {};
1196
+ artifactReads.setCandidates([]);
1197
+ runtime = undefined;
1198
+ snapshot = emptySnapshot("off");
1199
+ selectedHistoryExpired = false;
1200
+ branchStartsWithoutRuntime = preRuntime;
1201
+ forkInitialization = pendingFork;
1202
+ deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
1203
+ activeContext = ctx;
1204
+ installScopeStates();
1205
+ syncStateFlowTools();
1206
+ updateUi(ctx);
1207
+ try {
1208
+ if (!alreadyOff || (pendingFork && !forkRecorded)) {
1209
+ if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError)
1210
+ pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
1211
+ owner, at, mode: "off", memoryDeferred: true,
1212
+ ...(from === undefined ? {} : { from }),
1213
+ ...(preserveContext ? { preserveContext: true } : {}),
1214
+ ...(pendingFork ? { forkPending: true } : {}),
1215
+ ...(modePersistenceError === undefined ? {} : { persistenceError: modePersistenceError }),
1216
+ });
1217
+ if (checkpoint)
1218
+ pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
1219
+ }
1220
+ return Promise.resolve({ ok: true, message: alreadyOff ? "State Flow is already off" : "State Flow off" });
1221
+ }
1222
+ catch (error) {
1223
+ const message = conciseDiagnostic(`State Flow off; native mode recording failed: ${diagnosticText(error)}`);
1224
+ notifyProblem(ctx, message, "error");
1225
+ return Promise.resolve({ ok: false, message });
1226
+ }
1227
+ }
1228
+ async function acquirePassiveMode(ctx, pending, reason) {
1229
+ await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
1230
+ if (inactivePersistence !== pending || pending.controller.signal.aborted || shuttingDown) {
1231
+ return { ok: false, message: "State Flow mode change was superseded" };
1232
+ }
1233
+ return persistInactiveMode(ctx, pending, "passive");
1234
+ }
1053
1235
  async function persistInactiveMode(ctx, pending, mode) {
1054
1236
  let selected = runtime;
1055
1237
  const owner = ctx.sessionManager.getSessionId();
@@ -1080,7 +1262,7 @@ export default function stateFlowExtension(pi, options = {}) {
1080
1262
  const handoff = (wasActive || modePersistenceError) && selected?.view && current.meta.validation?.attempt !== 0
1081
1263
  ? createPassiveContinuation(projectModelState(effectiveState), incomingBoundary?.startedAt ?? stoppedAt, incomingBoundary ? incomingBoundary.activeRunStartedAt : !idle || unfinished ? anchor : undefined, modePersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)))
1082
1264
  : undefined;
1083
- // Off keeps the frozen boundary for a later Passive/Active choice but never projects it.
1265
+ // Repeated Passive choices keep the already selected continuation boundary.
1084
1266
  passiveContinuation = handoff ?? (!wasActive ? passiveContinuation : undefined);
1085
1267
  return handoff;
1086
1268
  };
@@ -1400,24 +1582,26 @@ export default function stateFlowExtension(pi, options = {}) {
1400
1582
  }
1401
1583
  });
1402
1584
  pi.on("session_before_compact", (event) => {
1403
- if (!compactionPlan)
1585
+ if (event.reason !== "manual" || !event.customInstructions?.startsWith("state-flow-boundary:"))
1404
1586
  return;
1405
- if (compactionStopped && event.reason === "manual" && event.customInstructions === compactionMarker)
1587
+ const marker = compactionMarker;
1588
+ if (shuttingDown || compactionStopped || !isActive() || !compactionPlan || !marker || event.customInstructions !== marker)
1406
1589
  return { cancel: true };
1407
- const result = stateFlowCompactionResult(compactionPlan, compactionMarker, event);
1590
+ const result = stateFlowCompactionResult(compactionPlan, marker, event);
1408
1591
  if (result === undefined || "cancel" in result)
1409
1592
  return result;
1410
1593
  return { compaction: result };
1411
1594
  });
1412
1595
  pi.on("agent_before_settle", async (_event, ctx) => {
1413
- if (shuttingDown || !turnAcceptedForBackup)
1596
+ if (shuttingDown || !memoryToolsAvailable() || !turnAcceptedForBackup)
1414
1597
  return;
1415
1598
  turnAcceptedForBackup = false;
1416
1599
  if (!backupPending)
1417
1600
  return;
1418
1601
  backupPending = false;
1419
1602
  const operationSignal = ctx.signal;
1420
- const signal = operationSignal ? AbortSignal.any([backupLifetime.signal, operationSignal]) : backupLifetime.signal;
1603
+ const lifetime = backupLifetime.signal;
1604
+ const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
1421
1605
  const selected = runtime;
1422
1606
  try {
1423
1607
  if (existsSync(join(repositoryRoot, ".git"))) {
@@ -1432,10 +1616,10 @@ export default function stateFlowExtension(pi, options = {}) {
1432
1616
  finally {
1433
1617
  backupOperations.delete(operation);
1434
1618
  }
1435
- if (signal.aborted)
1619
+ if (signal.aborted || snapshot.config.mode === "off")
1436
1620
  return;
1437
1621
  startStateFlowBackupPush(repositoryRoot, (error) => {
1438
- if (shuttingDown)
1622
+ if (shuttingDown || lifetime.aborted || snapshot.config.mode === "off")
1439
1623
  return;
1440
1624
  const message = diagnosticText(error);
1441
1625
  const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
@@ -1445,7 +1629,7 @@ export default function stateFlowExtension(pi, options = {}) {
1445
1629
  notifyActiveContext(recorded
1446
1630
  ? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
1447
1631
  : `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
1448
- }, () => { pushFailureNotified = false; });
1632
+ }, () => { pushFailureNotified = false; }, lifetime);
1449
1633
  }
1450
1634
  }
1451
1635
  catch (error) {
@@ -1476,11 +1660,18 @@ export default function stateFlowExtension(pi, options = {}) {
1476
1660
  if (!plan)
1477
1661
  return;
1478
1662
  compactionPlan = plan;
1663
+ const marker = compactionMarker = `${compactionPrefix}${randomUUID()}`;
1479
1664
  compactionInFlight = true;
1480
1665
  // Pi dispatches deferred companion prompts after all settled handlers return.
1481
1666
  await new Promise((resolve) => {
1482
- const finished = () => { compactionPlan = undefined; compactionInFlight = false; resolve(); };
1483
- ctx.compact({ customInstructions: compactionMarker, onComplete: finished, onError: finished });
1667
+ const finished = () => {
1668
+ if (compactionMarker === marker) {
1669
+ compactionPlan = undefined;
1670
+ compactionInFlight = false;
1671
+ }
1672
+ resolve();
1673
+ };
1674
+ ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
1484
1675
  });
1485
1676
  });
1486
1677
  pi.on("session_compact", () => { contextProjection.reset(); });
@@ -1497,6 +1688,7 @@ export default function stateFlowExtension(pi, options = {}) {
1497
1688
  });
1498
1689
  pi.on("session_shutdown", async (_event, _ctx) => {
1499
1690
  shuttingDown = true;
1691
+ memoryToolLifetime.abort();
1500
1692
  contextProjection.reset();
1501
1693
  const restoring = cancelBranchRestoration();
1502
1694
  const starting = cancelStartActivation();
@@ -1,11 +1,11 @@
1
1
  /** Await a coherent capture; hosts without cancellation may refuse contention instead of hanging Abort. */
2
2
  export declare function backupCurrentStateFlowFiles(repositoryRoot: string, signal?: AbortSignal, waitForLock?: boolean): Promise<string | undefined>;
3
3
  /** Skip overlapping pushes; the next accepted turn can push the latest HEAD. */
4
- export declare function startStateFlowBackupPush(repositoryRoot: string, onFailure: (error: unknown) => void, onSuccess?: () => void): boolean;
4
+ export declare function startStateFlowBackupPush(repositoryRoot: string, onFailure: (error: unknown) => void, onSuccess?: () => void, signal?: AbortSignal): boolean;
5
5
  /** Resolve only after the push process has closed (including timeout termination). */
6
6
  export declare function awaitInFlightBackupPushes(repositoryRoot: string): Promise<void>;
7
7
  /** Push the current backup commit to its explicitly configured branch remote without blocking settlement. */
8
- export declare function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{
8
+ export declare function pushCurrentStateFlowBackup(repositoryRoot: string, signal?: AbortSignal): Promise<{
9
9
  commit: string;
10
10
  remote: string;
11
11
  ref: string;
@@ -204,18 +204,20 @@ export function backupCurrentStateFlowFiles(repositoryRoot, signal, waitForLock
204
204
  return withBackupLock(repositoryRoot, (root) => commitCurrentOwnedFiles(root, currentHead(root), signal, waitForLock), signal, waitForLock);
205
205
  }
206
206
  /** Skip overlapping pushes; the next accepted turn can push the latest HEAD. */
207
- export function startStateFlowBackupPush(repositoryRoot, onFailure, onSuccess) {
207
+ export function startStateFlowBackupPush(repositoryRoot, onFailure, onSuccess, signal) {
208
208
  const root = resolve(repositoryRoot);
209
- if (activePushes.has(root))
209
+ if (signal?.aborted || activePushes.has(root))
210
210
  return false;
211
- const push = pushCurrentStateFlowBackup(root).then((result) => {
212
- if (result) {
211
+ const push = pushCurrentStateFlowBackup(root, signal).then((result) => {
212
+ if (result && !signal?.aborted) {
213
213
  try {
214
214
  onSuccess?.();
215
215
  }
216
216
  catch { /* Reporting cannot change push acceptance. */ }
217
217
  }
218
218
  }, (error) => {
219
+ if (signal?.aborted)
220
+ return;
219
221
  try {
220
222
  onFailure(error);
221
223
  }
@@ -231,15 +233,17 @@ export async function awaitInFlightBackupPushes(repositoryRoot) {
231
233
  await push;
232
234
  }
233
235
  /** Push the current backup commit to its explicitly configured branch remote without blocking settlement. */
234
- export function pushCurrentStateFlowBackup(repositoryRoot) {
236
+ export function pushCurrentStateFlowBackup(repositoryRoot, signal) {
235
237
  return new Promise((resolvePush, rejectPush) => {
236
238
  let root;
237
239
  let commit;
238
240
  let destination;
239
241
  try {
242
+ signal?.throwIfAborted();
240
243
  root = assertRepositoryRoot(repositoryRoot);
241
244
  commit = currentHead(root);
242
245
  destination = configuredPushDestination(root);
246
+ signal?.throwIfAborted();
243
247
  }
244
248
  catch (error) {
245
249
  rejectPush(new Error(redactGitDiagnostic(error instanceof Error ? error.message : String(error))));
@@ -282,6 +286,7 @@ export function pushCurrentStateFlowBackup(repositoryRoot) {
282
286
  return;
283
287
  settled = true;
284
288
  clearTimeout(timeout);
289
+ signal?.removeEventListener("abort", cancelled);
285
290
  if (error)
286
291
  rejectPush(error);
287
292
  else
@@ -297,5 +302,11 @@ export function pushCurrentStateFlowBackup(repositoryRoot) {
297
302
  child.stderr?.destroy(); });
298
303
  child.once("close", (code, endedBy) => finish(failure ?? (code === 0 ? undefined
299
304
  : new Error(`Git backup push failed (${endedBy ?? code}): ${redactGitDiagnostic(stderr.trim()) || "no diagnostic output"}`))));
305
+ function cancelled() {
306
+ terminate(signal?.reason instanceof Error ? signal.reason : new Error("Git backup push cancelled"));
307
+ }
308
+ signal?.addEventListener("abort", cancelled, { once: true });
309
+ if (signal?.aborted)
310
+ cancelled();
300
311
  });
301
312
  }
@@ -1,4 +1,4 @@
1
- import { type InactiveMode } from "./snapshot.ts";
1
+ import { type InactiveMode, type StateFlowMode } from "./snapshot.ts";
2
2
  export declare const SNAPSHOT_ENTRY_TYPE = "state-flow-snapshot";
3
3
  interface BranchEntry {
4
4
  type?: unknown;
@@ -26,6 +26,8 @@ export interface PassiveStopBoundary {
26
26
  persistenceError?: string;
27
27
  /** The inactive mode selected by that failed change; legacy markers omit it. */
28
28
  mode?: InactiveMode;
29
+ /** Native-only Off choice; canonical runtime mode is deliberately not updated. */
30
+ memoryDeferred?: true;
29
31
  }
30
32
  export interface SnapshotDiscovery {
31
33
  candidates: unknown[];
@@ -33,6 +35,13 @@ export interface SnapshotDiscovery {
33
35
  }
34
36
  /** Enumerate active-branch snapshots newest-first while containing hostile entries. */
35
37
  export declare function discoverSnapshotData(branch: readonly BranchEntry[]): SnapshotDiscovery;
38
+ /** Read native mode policy only; semantic checkpoint validity remains the recovery owner's concern. */
39
+ export declare function findBranchPolicy(branch: readonly BranchEntry[], sessionId: string | undefined, stopEntryType: string | undefined, inactiveMode: InactiveMode): {
40
+ mode: StateFlowMode;
41
+ persistenceError?: string;
42
+ } | undefined;
43
+ /** Native policy bookkeeping retains an unacquired fork across extension reloads. */
44
+ export declare function hasPendingFork(branch: readonly BranchEntry[], sessionId: string, entryType: string): boolean;
36
45
  export declare function snapshotDataNewestFirst(branch: readonly BranchEntry[]): unknown[];
37
46
  export declare function latestSnapshotData(branch: readonly BranchEntry[]): unknown;
38
47
  export declare function hasPriorConversation(branch: readonly BranchEntry[]): boolean;