@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.
- package/BACKLOG.md +5 -1
- package/CHANGELOG.md +6 -0
- package/README.md +9 -7
- package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-actors/LICENSE +21 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
- package/node_modules/@llblab/pi-actors/package.json +4 -3
- package/node_modules/@llblab/pi-claude-usage/AGENTS.md +6 -3
- package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +2 -1
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +8 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +48 -3
- package/node_modules/@llblab/pi-claude-usage/index.ts +8 -1159
- package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
- package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
- package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
- package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
- package/node_modules/@llblab/pi-claude-usage/package.json +9 -5
- package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
- package/node_modules/@llblab/pi-clean-room/README.md +1 -1
- package/node_modules/@llblab/pi-clean-room/package.json +3 -2
- package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
- package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
- package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
- package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
- package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
- package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
- package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
- package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
- package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
- package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
- package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
- package/node_modules/@llblab/pi-command-fast/README.md +42 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
- package/node_modules/@llblab/pi-command-fast/package.json +49 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +6 -5
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +7 -1
- package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
- package/node_modules/@llblab/pi-state-flow/README.md +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +238 -46
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +16 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +60 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +8 -5
- package/node_modules/@llblab/pi-state-flow/dist/package.json +10 -9
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -7
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +4 -2
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +14 -13
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +221 -46
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +14 -5
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +52 -1
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +7 -5
- package/node_modules/@llblab/pi-state-flow/package.json +10 -9
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +7 -7
|
@@ -34,9 +34,9 @@ import { readProjectedState, readStatePath } from "./query.ts";
|
|
|
34
34
|
import { selectedBoundaryFailure, selectRetainedCheckpoint, waitForRecovery } from "./recovery.ts";
|
|
35
35
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
36
36
|
import { TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
37
|
-
import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
37
|
+
import { discoverSnapshotData, findAssistantToolBatch, findBranchPolicy, findPassiveStopBoundary, hasPendingFork, hasPriorConversation, hasUncheckpointedConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
38
38
|
import { hasCompiledSkillArtifact, hashSkillSource, registeredSkillResolver, SkillReadTracker, type SuccessfulSkillRead } from "./skills.ts";
|
|
39
|
-
import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
|
|
39
|
+
import { HistoryBoundaryExpiredError, emptySnapshot, migrationFailure, preRuntimeCheckpoint, type InactiveMode, type RetainedBoundaryCheckpoint, type RetainedPiCheckpoint, type Snapshot, type StateFlowMode } from "./snapshot.ts";
|
|
40
40
|
import { emptyState, projectStateForModel, type AtomicScopePatches, type SemanticState, type ModelState, type ScopedSemanticStates, type ScopedStates, type StateScope } from "./state.ts";
|
|
41
41
|
import { compactStatus, detailedStatus, STATUS_KEY, type StatusDiagnostics } from "./status.ts";
|
|
42
42
|
import { PublicationBusyError } from "./storage.ts";
|
|
@@ -97,15 +97,18 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
97
97
|
let startActivation: { controller: AbortController; operation?: Promise<StateFlowTelegramControlResult> } | undefined;
|
|
98
98
|
let forkInitialization = false;
|
|
99
99
|
let branchRestoration: BranchRestoration | undefined;
|
|
100
|
+
let deferredBranch: { reason: unknown; owner: string; cwd: string } | undefined;
|
|
100
101
|
const restorationOperations = new Set<Promise<void>>();
|
|
101
102
|
let responseReconciliation: AbortController | undefined;
|
|
102
103
|
let sharedInspectionLifetime = new AbortController();
|
|
104
|
+
let memoryToolLifetime = new AbortController();
|
|
103
105
|
let completedRunAccepted = false;
|
|
104
106
|
let turnAcceptedForBackup = false;
|
|
105
107
|
let compactionPlan: StateFlowCompactionPlan | undefined;
|
|
106
108
|
let compactionInFlight = false;
|
|
107
109
|
let compactionStopped = false;
|
|
108
|
-
const
|
|
110
|
+
const compactionPrefix = `state-flow-boundary:${randomUUID()}:`;
|
|
111
|
+
let compactionMarker: string | undefined;
|
|
109
112
|
let passiveContinuation: PassiveContinuation | undefined;
|
|
110
113
|
let bootstrapContinuation: PassiveContinuation | undefined;
|
|
111
114
|
const contextProjection = new ContextProjection();
|
|
@@ -117,7 +120,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
117
120
|
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
118
121
|
const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => notifyActiveContext(message));
|
|
119
122
|
let backupPending = false;
|
|
120
|
-
|
|
123
|
+
let backupLifetime = new AbortController();
|
|
121
124
|
const backupOperations = new Set<Promise<string | undefined>>();
|
|
122
125
|
let shuttingDown = false;
|
|
123
126
|
let pushFailureNotified = false;
|
|
@@ -230,6 +233,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
230
233
|
sharedInspectionLifetime = new AbortController();
|
|
231
234
|
}
|
|
232
235
|
|
|
236
|
+
function cancelBackupWork(): void {
|
|
237
|
+
backupLifetime.abort();
|
|
238
|
+
backupLifetime = new AbortController();
|
|
239
|
+
backupPending = false;
|
|
240
|
+
}
|
|
241
|
+
|
|
233
242
|
function cancelInferencePreparation(): void {
|
|
234
243
|
inferencePreparation?.controller.abort();
|
|
235
244
|
inferencePreparation = undefined;
|
|
@@ -319,17 +328,28 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
319
328
|
|
|
320
329
|
async function inspectTelegramState(scope: StateFlowTelegramScope): Promise<StateFlowTelegramInspection> {
|
|
321
330
|
if (!activeContext || shuttingDown) throw new Error("State Flow is not attached to an active session yet");
|
|
322
|
-
|
|
323
|
-
const
|
|
331
|
+
const ctx = activeContext;
|
|
332
|
+
const owner = sessionAddress(ctx).key;
|
|
333
|
+
const cwd = ctx.cwd;
|
|
334
|
+
const off = snapshot.config.mode === "off";
|
|
335
|
+
const privateScope = scope === "session" || scope === "effective";
|
|
336
|
+
if (!off && privateScope) assertSelectedBranchAvailable();
|
|
337
|
+
// Off operator reads are disposable: they confer no selected-branch/cache authority.
|
|
338
|
+
const selected = off ? createRuntime(ctx) : runtime ??= createRuntime(ctx);
|
|
324
339
|
const signal = sharedInspectionLifetime.signal;
|
|
325
|
-
|
|
340
|
+
let privateAvailable = true;
|
|
341
|
+
if (off && privateScope) privateAvailable = await selected.refreshCurrentMemory(signal) !== undefined;
|
|
342
|
+
else if (!selected.view || (scope !== "session" && !modePersistenceError)) await selected.refreshShared(signal);
|
|
326
343
|
signal.throwIfAborted();
|
|
327
|
-
if (
|
|
328
|
-
|
|
344
|
+
if (activeContext !== ctx || ctx.cwd !== cwd || sessionAddress(ctx).key !== owner || (!off && runtime !== selected)) {
|
|
345
|
+
throw new Error("State Flow inspection selection changed; select the scope again");
|
|
346
|
+
}
|
|
347
|
+
if (!privateAvailable) throw new Error("Current State Flow session memory is unavailable");
|
|
348
|
+
if (!off && privateScope) assertSelectedBranchAvailable();
|
|
329
349
|
if (!selected.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
330
|
-
installScopeStates();
|
|
331
|
-
const state = scope === "effective" ?
|
|
332
|
-
return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions:
|
|
350
|
+
if (!off) installScopeStates();
|
|
351
|
+
const state = selected.readView(0, scope === "effective" ? undefined : scope);
|
|
352
|
+
return { state: { ...projectModelState(state), ...(state.lazy === undefined ? {} : { lazy: structuredClone(state.lazy) }) }, revisions: temporalScopeRevisions(selected.view), signal };
|
|
333
353
|
}
|
|
334
354
|
|
|
335
355
|
function syncStateFlowTools(): void {
|
|
@@ -473,8 +493,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
473
493
|
}
|
|
474
494
|
|
|
475
495
|
/** Select the active native branch under one owned restoration lifetime; only current accepted work installs memory. */
|
|
476
|
-
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController): Promise<void> {
|
|
496
|
+
function restoreActiveBranch(ctx: ExtensionContext, sessionStartReason?: unknown, notifyRecovery = true, startOwner?: AbortController, requestedMode?: InactiveMode): Promise<void> {
|
|
477
497
|
contextProjection.reset();
|
|
498
|
+
memoryToolLifetime.abort();
|
|
499
|
+
memoryToolLifetime = new AbortController();
|
|
478
500
|
cancelBranchRestoration();
|
|
479
501
|
// Start-owned attachment/fork recovery keeps its owner; only accepted Start cancels Stop.
|
|
480
502
|
if (!startOwner) {
|
|
@@ -488,6 +510,51 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
488
510
|
artifactReads.setCandidates([]);
|
|
489
511
|
telegramStartPending = false;
|
|
490
512
|
activeContext = ctx;
|
|
513
|
+
if (!startOwner) {
|
|
514
|
+
let mode: StateFlowMode = config.inactiveMode;
|
|
515
|
+
let adoptDefault = false;
|
|
516
|
+
let withoutRuntime = false;
|
|
517
|
+
let persistenceError: string | undefined;
|
|
518
|
+
let pendingFork = false;
|
|
519
|
+
try {
|
|
520
|
+
const branch = ctx.sessionManager.getBranch();
|
|
521
|
+
const discovery = discoverSnapshotData(branch);
|
|
522
|
+
withoutRuntime = discovery.candidates.length === 0 && discovery.errors.length === 0;
|
|
523
|
+
adoptDefault = withoutRuntime && isNewSession(sessionStartReason, branch);
|
|
524
|
+
pendingFork = hasPendingFork(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
525
|
+
const inherited = sessionStartReason === "fork" || pendingFork;
|
|
526
|
+
const policy = findBranchPolicy(branch, inherited ? undefined : ctx.sessionManager.getSessionId(),
|
|
527
|
+
inherited || retainsPhysicalSessionProjection(sessionStartReason) ? PASSIVE_STOP_ENTRY_TYPE : undefined, config.inactiveMode);
|
|
528
|
+
mode = policy?.mode ?? (adoptDefault ? config.mode : config.inactiveMode);
|
|
529
|
+
persistenceError = inherited
|
|
530
|
+
? findBranchPolicy(branch, ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode)?.persistenceError
|
|
531
|
+
: policy?.persistenceError;
|
|
532
|
+
} catch {
|
|
533
|
+
// Off needs only policy; unavailable native evidence grants no memory authority.
|
|
534
|
+
}
|
|
535
|
+
if (mode === "off") {
|
|
536
|
+
cancelBackupWork();
|
|
537
|
+
const owner = sessionAddress(ctx).key;
|
|
538
|
+
const reason = pendingFork || (deferredBranch?.owner === owner && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork"
|
|
539
|
+
&& retainsPhysicalSessionProjection(sessionStartReason)) ? "fork" : sessionStartReason;
|
|
540
|
+
if (reason === "fork" && !pendingFork) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { owner: ctx.sessionManager.getSessionId(), forkPending: true });
|
|
541
|
+
deferredBranch = { reason, owner, cwd: ctx.cwd };
|
|
542
|
+
runtime = undefined;
|
|
543
|
+
snapshot = emptySnapshot("off");
|
|
544
|
+
branchStartsWithoutRuntime = withoutRuntime;
|
|
545
|
+
selectedHistoryExpired = false;
|
|
546
|
+
modePersistenceError = persistenceError;
|
|
547
|
+
forkInitialization = reason === "fork";
|
|
548
|
+
installScopeStates();
|
|
549
|
+
artifactHints = {};
|
|
550
|
+
if (adoptDefault) appendCheckpoint();
|
|
551
|
+
syncStateFlowTools();
|
|
552
|
+
updateUi(ctx);
|
|
553
|
+
return Promise.resolve();
|
|
554
|
+
}
|
|
555
|
+
if (pendingFork) sessionStartReason = "fork";
|
|
556
|
+
}
|
|
557
|
+
deferredBranch = undefined;
|
|
491
558
|
runtime = createRuntime(ctx);
|
|
492
559
|
installScopeStates();
|
|
493
560
|
branchStartsWithoutRuntime = false;
|
|
@@ -548,9 +615,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
548
615
|
snapshot = migrationFailure({}, "State Flow branch restoration is pending", selection.kind === "current" ? selection.mode
|
|
549
616
|
: selection.kind === "auto-start" ? config.inactiveMode : inactiveSelection(selection.checkpoint.mode));
|
|
550
617
|
}
|
|
618
|
+
if (requestedMode) snapshot = deactivateEpisode(snapshot, requestedMode);
|
|
551
619
|
const pending: BranchRestoration = {
|
|
552
620
|
controller: new AbortController(),
|
|
553
621
|
awaitingAcceptance: selection.kind !== "settled" && selection.kind !== "current",
|
|
622
|
+
...(requestedMode === undefined ? {} : { requestedMode }),
|
|
554
623
|
};
|
|
555
624
|
branchRestoration = pending;
|
|
556
625
|
const operation = attachBranch(ctx, pending, selection, sessionStartReason, notifyRecovery, skipped);
|
|
@@ -687,25 +756,24 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
687
756
|
return passive;
|
|
688
757
|
}
|
|
689
758
|
|
|
759
|
+
function selectedContinuation(ctx: ExtensionContext, reason: unknown): PassiveContinuation | undefined {
|
|
760
|
+
if (!retainsPhysicalSessionProjection(reason) || !runtime?.view) return undefined;
|
|
761
|
+
const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
762
|
+
return boundary === undefined ? undefined : createPassiveContinuation(
|
|
763
|
+
projectModelState(effectiveState), boundary.at, boundary.from, boundary.preserveContext,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
690
767
|
function settleSelection(ctx: ExtensionContext, reason: unknown, notifyRecovery: boolean, skipped: number, selectedMemory: boolean): void {
|
|
691
768
|
const failure = !isActive() && snapshot.meta.validation?.attempt === 0 ? snapshot.meta.validation.error : undefined;
|
|
692
769
|
if (failure !== undefined && notifyRecovery && !modePersistenceError) {
|
|
693
770
|
if (selectedHistoryExpired) notifyProblem(ctx, "State Flow history is outside the retained temporal window; /state-flow-active can use current session memory.", "warning");
|
|
694
771
|
else notifyProblem(ctx, `State Flow restore failed: ${failure}`, "error");
|
|
695
772
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
projectModelState(effectiveState),
|
|
701
|
-
boundary.at,
|
|
702
|
-
boundary.from,
|
|
703
|
-
boundary.preserveContext,
|
|
704
|
-
);
|
|
705
|
-
// Off retains the handoff boundary for a later Passive or Active choice but never projects it.
|
|
706
|
-
if (!isActive()) passiveContinuation = continuation;
|
|
707
|
-
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
708
|
-
}
|
|
773
|
+
const continuation = selectedMemory ? selectedContinuation(ctx, reason) : undefined;
|
|
774
|
+
if (continuation !== undefined) {
|
|
775
|
+
if (!isActive()) passiveContinuation = continuation;
|
|
776
|
+
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
709
777
|
}
|
|
710
778
|
if (notifyRecovery && isActive() && skipped > 0) {
|
|
711
779
|
notifyProblem(ctx, `State Flow skipped ${skipped} malformed snapshot(s); restored the last valid one.`, "warning");
|
|
@@ -784,6 +852,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
784
852
|
return new Text(separatedOutput(theme.fg("dim", formatPatchStateArguments(context.args))), 0, 0);
|
|
785
853
|
},
|
|
786
854
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
855
|
+
signal = signal ? AbortSignal.any([signal, memoryToolLifetime.signal]) : memoryToolLifetime.signal;
|
|
787
856
|
try {
|
|
788
857
|
if (!memoryToolsAvailable()) throw new Error("State Flow tools are off for this session");
|
|
789
858
|
assertPublicationAvailable();
|
|
@@ -850,7 +919,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
850
919
|
let attempted: unknown;
|
|
851
920
|
try { attempted = structuredClone(params); } catch { attempted = undefined; }
|
|
852
921
|
const cause = diagnosticText(error);
|
|
853
|
-
recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
|
|
922
|
+
if (!signal.aborted && memoryToolsAvailable()) recordDiagnostic(cause, /concurrently|advanced/.test(cause) ? "publication-conflict" : "invalid-patch", ctx, {
|
|
854
923
|
input: attempted,
|
|
855
924
|
tool: PATCH_STATE_TOOL_NAME,
|
|
856
925
|
toolCallId,
|
|
@@ -891,7 +960,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
891
960
|
return { ok: false, message: "State Flow activation was superseded", signal: pending.signal };
|
|
892
961
|
};
|
|
893
962
|
try {
|
|
894
|
-
if (
|
|
963
|
+
if (deferredBranch?.reason === "fork") await waitForRecovery(restoreActiveBranch(ctx, "fork", false, pending), signal);
|
|
964
|
+
else if (!activeContext) await waitForRecovery(restoreActiveBranch(ctx, undefined, false, pending), signal);
|
|
895
965
|
else if (branchRestoration?.operation) await waitForRecovery(branchRestoration.operation, signal);
|
|
896
966
|
if (!isOwner()) return superseded();
|
|
897
967
|
if (isActive()) return { ok: true, message: "State Flow is already active", signal: sharedInspectionLifetime.signal };
|
|
@@ -922,7 +992,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
922
992
|
const initiallyActive = isActive();
|
|
923
993
|
let accepted = false;
|
|
924
994
|
let receipt = AbortSignal.any([pending.signal, sharedInspectionLifetime.signal]);
|
|
925
|
-
const
|
|
995
|
+
const signal = ctx.signal ? AbortSignal.any([pending.signal, ctx.signal]) : pending.signal;
|
|
996
|
+
const isCurrent = () => startActivation?.controller === pending && !signal.aborted && runtime === selected && !shuttingDown
|
|
926
997
|
&& ctx.cwd === cwd && ctx.sessionManager.getSessionId() === owner && ctx.sessionManager.getSessionFile() === file
|
|
927
998
|
&& ctx.sessionManager.getHeader()?.timestamp === timestamp && isActive() === (accepted || initiallyActive);
|
|
928
999
|
const superseded = (): StateFlowTelegramControlResult => {
|
|
@@ -931,13 +1002,20 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
931
1002
|
};
|
|
932
1003
|
try {
|
|
933
1004
|
const activation = createRuntime(ctx);
|
|
934
|
-
const
|
|
1005
|
+
const deferred = deferredBranch;
|
|
1006
|
+
if (deferred) {
|
|
1007
|
+
const discovery = discoverSnapshotData(ctx.sessionManager.getBranch());
|
|
1008
|
+
const retained = selectRetainedCheckpoint(discovery.candidates, config.inactiveMode);
|
|
1009
|
+
branchStartsWithoutRuntime = retained.kind === "pre-runtime" || (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
1010
|
+
}
|
|
1011
|
+
// Off activation owns one cancellable current-head publication, not an independent read-only restore.
|
|
935
1012
|
const result = await activation.withStartTransaction((current, publish) => {
|
|
936
1013
|
signal.throwIfAborted();
|
|
937
1014
|
if (!isCurrent()) throw new Error("State Flow activation selection changed while awaiting publication");
|
|
938
1015
|
if (!current && !branchStartsWithoutRuntime) throw new Error(snapshot.meta.validation?.error ?? "Current State Flow session memory is unavailable");
|
|
939
|
-
const
|
|
940
|
-
|
|
1016
|
+
const boundary = deferred && retainsPhysicalSessionProjection(deferred.reason)
|
|
1017
|
+
? findPassiveStopBoundary(ctx.sessionManager.getBranch(), owner, PASSIVE_STOP_ENTRY_TYPE) : undefined;
|
|
1018
|
+
const bootstrap = hasPriorConversation(ctx.sessionManager.getBranch()) || passiveContinuation !== undefined || boundary !== undefined;
|
|
941
1019
|
const activated = current ? resumeEpisode(current, bootstrap) : startEpisode(bootstrap);
|
|
942
1020
|
const recoveredCurrent = selectedHistoryExpired;
|
|
943
1021
|
const publication = publish(activated);
|
|
@@ -949,6 +1027,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
949
1027
|
selectedHistoryExpired = false;
|
|
950
1028
|
modePersistenceError = undefined;
|
|
951
1029
|
installScopeStates();
|
|
1030
|
+
const continuation = passiveContinuation ?? bootstrapContinuation ?? (deferred ? selectedContinuation(ctx, deferred.reason) : undefined);
|
|
1031
|
+
deferredBranch = undefined;
|
|
952
1032
|
clearRunTransient();
|
|
953
1033
|
contextProjection.reset();
|
|
954
1034
|
passiveContinuation = undefined;
|
|
@@ -958,6 +1038,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
958
1038
|
recordPolicyPublication(publication, ctx);
|
|
959
1039
|
syncStateFlowTools();
|
|
960
1040
|
updateUi(ctx);
|
|
1041
|
+
if (forkInitialization) {
|
|
1042
|
+
forkInitialization = false;
|
|
1043
|
+
pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { reset: true, owner });
|
|
1044
|
+
}
|
|
961
1045
|
appendCheckpoint();
|
|
962
1046
|
ctx.ui.notify(
|
|
963
1047
|
recoveredCurrent
|
|
@@ -1014,8 +1098,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1014
1098
|
}
|
|
1015
1099
|
|
|
1016
1100
|
function deactivateStateFlow(ctx: ExtensionContext, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
|
|
1017
|
-
cancelStartActivation();
|
|
1018
1101
|
if (shuttingDown) return Promise.resolve({ ok: false, message: "State Flow is shutting down" });
|
|
1102
|
+
if (mode === "off") return selectOff(ctx);
|
|
1103
|
+
cancelStartActivation();
|
|
1019
1104
|
telegramStartPending = false;
|
|
1020
1105
|
const persisting = inactivePersistence;
|
|
1021
1106
|
if (persisting?.operation && !persisting.published) {
|
|
@@ -1028,7 +1113,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1028
1113
|
// Read-only recovery also retains the latest policy; its native write fence is updated below.
|
|
1029
1114
|
if (restoring) restoring.requestedMode = mode;
|
|
1030
1115
|
if (restoring?.awaitingAcceptance) {
|
|
1031
|
-
//
|
|
1116
|
+
// Passive keeps independently owned restoration; Off withdraws it through selectOff.
|
|
1032
1117
|
applyInactiveMode(ctx, mode);
|
|
1033
1118
|
return Promise.resolve({ ok: true, message: `State Flow ${mode}; memory restoration continues` });
|
|
1034
1119
|
}
|
|
@@ -1040,13 +1125,96 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1040
1125
|
cancelInactivePersistence();
|
|
1041
1126
|
const pending: NonNullable<typeof inactivePersistence> = { controller: new AbortController() };
|
|
1042
1127
|
inactivePersistence = pending;
|
|
1043
|
-
const operation =
|
|
1128
|
+
const operation = deferredBranch && mode === "passive" && !branchStartsWithoutRuntime
|
|
1129
|
+
? acquirePassiveMode(ctx, pending, deferredBranch.reason)
|
|
1130
|
+
: persistInactiveMode(ctx, pending, mode);
|
|
1044
1131
|
pending.operation = operation;
|
|
1045
1132
|
const finished = () => { if (inactivePersistence === pending) inactivePersistence = undefined; };
|
|
1046
1133
|
void operation.then(finished, finished);
|
|
1047
1134
|
return operation;
|
|
1048
1135
|
}
|
|
1049
1136
|
|
|
1137
|
+
/** Off owns cancellation and native policy only; no canonical transaction or semantic handoff. */
|
|
1138
|
+
function selectOff(ctx: ExtensionContext): Promise<StateFlowTelegramControlResult> {
|
|
1139
|
+
const address = sessionAddress(ctx);
|
|
1140
|
+
const owner = ctx.sessionManager.getSessionId();
|
|
1141
|
+
const branch = ctx.sessionManager.getBranch();
|
|
1142
|
+
const selected = runtime;
|
|
1143
|
+
const sameOwner = selected?.sessionKey === address.key && selected.cwd === ctx.cwd;
|
|
1144
|
+
const forkRecorded = hasPendingFork(branch, owner, PASSIVE_STOP_ENTRY_TYPE);
|
|
1145
|
+
const pendingFork = forkRecorded || (sameOwner && forkInitialization) || (deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd && deferredBranch.reason === "fork");
|
|
1146
|
+
const policy = findBranchPolicy(branch, owner, PASSIVE_STOP_ENTRY_TYPE, config.inactiveMode);
|
|
1147
|
+
const sameDeferred = deferredBranch?.owner === address.key && deferredBranch.cwd === ctx.cwd;
|
|
1148
|
+
const current = sameOwner || sameDeferred ? snapshot : emptySnapshot("off");
|
|
1149
|
+
if (!sameOwner && !sameDeferred) modePersistenceError = policy?.persistenceError;
|
|
1150
|
+
const alreadyOff = current.config.mode === "off" && policy?.mode === "off";
|
|
1151
|
+
const incoming = sameOwner || sameDeferred ? current.meta.bootstrap ? bootstrapContinuation : passiveContinuation : undefined;
|
|
1152
|
+
const unfinished = current.meta.specification !== undefined || (inferencePreparation?.prompt !== undefined && !inferencePreparation.accepted)
|
|
1153
|
+
|| (isActive() && hasUncheckpointedConversation(branch));
|
|
1154
|
+
const at = incoming?.startedAt ?? Date.now();
|
|
1155
|
+
const from = incoming ? incoming.activeRunStartedAt : (sameOwner || sameDeferred) && (!ctx.isIdle() || unfinished) ? runAnchorTimestamp : undefined;
|
|
1156
|
+
const preserveContext = modePersistenceError !== undefined || (incoming ? incoming.preserveContext : current.meta.bootstrap === true || (unfinished && from === undefined));
|
|
1157
|
+
const discovery = discoverSnapshotData(branch);
|
|
1158
|
+
const preRuntime = (sameOwner && branchStartsWithoutRuntime) || (discovery.candidates.length === 0 && discovery.errors.length === 0);
|
|
1159
|
+
let checkpoint: RetainedPiCheckpoint | undefined;
|
|
1160
|
+
try {
|
|
1161
|
+
checkpoint = !modePersistenceError && current.meta.validation?.attempt !== 0
|
|
1162
|
+
? preRuntime ? preRuntimeCheckpoint("off") : sameOwner && selected?.view ? selected.retainedCheckpoint(deactivateEpisode(current, "off")) : undefined
|
|
1163
|
+
: undefined;
|
|
1164
|
+
} catch {
|
|
1165
|
+
// An unusable cached bookmark cannot prevent native Off policy or authorize replacement memory.
|
|
1166
|
+
}
|
|
1167
|
+
cancelStartActivation();
|
|
1168
|
+
cancelBranchRestoration();
|
|
1169
|
+
cancelInactivePersistence();
|
|
1170
|
+
cancelBackupWork();
|
|
1171
|
+
memoryToolLifetime.abort();
|
|
1172
|
+
memoryToolLifetime = new AbortController();
|
|
1173
|
+
clearRunTransient();
|
|
1174
|
+
contextProjection.reset();
|
|
1175
|
+
telegramStartPending = false;
|
|
1176
|
+
bootstrapContinuation = undefined;
|
|
1177
|
+
passiveContinuation = undefined;
|
|
1178
|
+
artifactInvalidations = [];
|
|
1179
|
+
artifactHints = {};
|
|
1180
|
+
artifactReads.setCandidates([]);
|
|
1181
|
+
runtime = undefined;
|
|
1182
|
+
snapshot = emptySnapshot("off");
|
|
1183
|
+
selectedHistoryExpired = false;
|
|
1184
|
+
branchStartsWithoutRuntime = preRuntime;
|
|
1185
|
+
forkInitialization = pendingFork;
|
|
1186
|
+
deferredBranch = { owner: address.key, cwd: ctx.cwd, reason: pendingFork ? "fork" : undefined };
|
|
1187
|
+
activeContext = ctx;
|
|
1188
|
+
installScopeStates();
|
|
1189
|
+
syncStateFlowTools();
|
|
1190
|
+
updateUi(ctx);
|
|
1191
|
+
try {
|
|
1192
|
+
if (!alreadyOff || (pendingFork && !forkRecorded)) {
|
|
1193
|
+
if (!checkpoint || !preRuntime || pendingFork || preserveContext || from !== undefined || modePersistenceError) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, {
|
|
1194
|
+
owner, at, mode: "off", memoryDeferred: true,
|
|
1195
|
+
...(from === undefined ? {} : { from }),
|
|
1196
|
+
...(preserveContext ? { preserveContext: true } : {}),
|
|
1197
|
+
...(pendingFork ? { forkPending: true } : {}),
|
|
1198
|
+
...(modePersistenceError === undefined ? {} : { persistenceError: modePersistenceError }),
|
|
1199
|
+
});
|
|
1200
|
+
if (checkpoint) pi.appendEntry(SNAPSHOT_ENTRY_TYPE, checkpoint);
|
|
1201
|
+
}
|
|
1202
|
+
return Promise.resolve({ ok: true, message: alreadyOff ? "State Flow is already off" : "State Flow off" });
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
const message = conciseDiagnostic(`State Flow off; native mode recording failed: ${diagnosticText(error)}`);
|
|
1205
|
+
notifyProblem(ctx, message, "error");
|
|
1206
|
+
return Promise.resolve({ ok: false, message });
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
async function acquirePassiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, reason: unknown): Promise<StateFlowTelegramControlResult> {
|
|
1211
|
+
await restoreActiveBranch(ctx, reason, true, pending.controller, "passive");
|
|
1212
|
+
if (inactivePersistence !== pending || pending.controller.signal.aborted || shuttingDown) {
|
|
1213
|
+
return { ok: false, message: "State Flow mode change was superseded" };
|
|
1214
|
+
}
|
|
1215
|
+
return persistInactiveMode(ctx, pending, "passive");
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1050
1218
|
async function persistInactiveMode(ctx: ExtensionContext, pending: NonNullable<typeof inactivePersistence>, mode: InactiveMode): Promise<StateFlowTelegramControlResult> {
|
|
1051
1219
|
let selected = runtime;
|
|
1052
1220
|
const owner = ctx.sessionManager.getSessionId();
|
|
@@ -1082,7 +1250,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1082
1250
|
modePersistenceError !== undefined || (incomingBoundary ? incomingBoundary.preserveContext : current.meta.bootstrap === true || (unfinished && anchor === undefined)),
|
|
1083
1251
|
)
|
|
1084
1252
|
: undefined;
|
|
1085
|
-
//
|
|
1253
|
+
// Repeated Passive choices keep the already selected continuation boundary.
|
|
1086
1254
|
passiveContinuation = handoff ?? (!wasActive ? passiveContinuation : undefined);
|
|
1087
1255
|
return handoff;
|
|
1088
1256
|
};
|
|
@@ -1383,20 +1551,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1383
1551
|
});
|
|
1384
1552
|
|
|
1385
1553
|
pi.on("session_before_compact", (event) => {
|
|
1386
|
-
if (!
|
|
1387
|
-
|
|
1388
|
-
|
|
1554
|
+
if (event.reason !== "manual" || !event.customInstructions?.startsWith("state-flow-boundary:")) return;
|
|
1555
|
+
const marker = compactionMarker;
|
|
1556
|
+
if (shuttingDown || compactionStopped || !isActive() || !compactionPlan || !marker || event.customInstructions !== marker) return { cancel: true };
|
|
1557
|
+
const result = stateFlowCompactionResult(compactionPlan, marker, event);
|
|
1389
1558
|
if (result === undefined || "cancel" in result) return result;
|
|
1390
1559
|
return { compaction: result };
|
|
1391
1560
|
});
|
|
1392
1561
|
|
|
1393
1562
|
pi.on("agent_before_settle", async (_event, ctx) => {
|
|
1394
|
-
if (shuttingDown || !turnAcceptedForBackup) return;
|
|
1563
|
+
if (shuttingDown || !memoryToolsAvailable() || !turnAcceptedForBackup) return;
|
|
1395
1564
|
turnAcceptedForBackup = false;
|
|
1396
1565
|
if (!backupPending) return;
|
|
1397
1566
|
backupPending = false;
|
|
1398
1567
|
const operationSignal = ctx.signal;
|
|
1399
|
-
const
|
|
1568
|
+
const lifetime = backupLifetime.signal;
|
|
1569
|
+
const signal = operationSignal ? AbortSignal.any([lifetime, operationSignal]) : lifetime;
|
|
1400
1570
|
const selected = runtime;
|
|
1401
1571
|
try {
|
|
1402
1572
|
if (existsSync(join(repositoryRoot, ".git"))) {
|
|
@@ -1407,9 +1577,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1407
1577
|
backupOperations.add(operation);
|
|
1408
1578
|
try { await operation; }
|
|
1409
1579
|
finally { backupOperations.delete(operation); }
|
|
1410
|
-
if (signal.aborted) return;
|
|
1580
|
+
if (signal.aborted || snapshot.config.mode === "off") return;
|
|
1411
1581
|
startStateFlowBackupPush(repositoryRoot, (error) => {
|
|
1412
|
-
if (shuttingDown) return;
|
|
1582
|
+
if (shuttingDown || lifetime.aborted || snapshot.config.mode === "off") return;
|
|
1413
1583
|
const message = diagnosticText(error);
|
|
1414
1584
|
const recorded = diagnosticWriter.recordBackupPushFailure(pushSessionId, pushCwd, message);
|
|
1415
1585
|
if (pushFailureNotified) return;
|
|
@@ -1417,7 +1587,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1417
1587
|
notifyActiveContext(recorded
|
|
1418
1588
|
? `State Flow Git backup push failed; state is saved locally. Details: ${stateFlowLogPath(agentDir)}. A later accepted turn retries.`
|
|
1419
1589
|
: `State Flow Git backup push failed; local diagnostics unavailable: ${message}`);
|
|
1420
|
-
}, () => { pushFailureNotified = false; });
|
|
1590
|
+
}, () => { pushFailureNotified = false; }, lifetime);
|
|
1421
1591
|
}
|
|
1422
1592
|
} catch (error) {
|
|
1423
1593
|
if (signal.aborted || runtime !== selected) return;
|
|
@@ -1444,11 +1614,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1444
1614
|
const plan = planStateFlowCompaction(entries, runtime.causalBasis(), snapshot.meta.step, runAnchorTimestamp);
|
|
1445
1615
|
if (!plan) return;
|
|
1446
1616
|
compactionPlan = plan;
|
|
1617
|
+
const marker = compactionMarker = `${compactionPrefix}${randomUUID()}`;
|
|
1447
1618
|
compactionInFlight = true;
|
|
1448
1619
|
// Pi dispatches deferred companion prompts after all settled handlers return.
|
|
1449
1620
|
await new Promise<void>((resolve) => {
|
|
1450
|
-
const finished = () => {
|
|
1451
|
-
|
|
1621
|
+
const finished = () => {
|
|
1622
|
+
if (compactionMarker === marker) { compactionPlan = undefined; compactionInFlight = false; }
|
|
1623
|
+
resolve();
|
|
1624
|
+
};
|
|
1625
|
+
ctx.compact({ customInstructions: marker, onComplete: finished, onError: finished });
|
|
1452
1626
|
});
|
|
1453
1627
|
});
|
|
1454
1628
|
|
|
@@ -1466,6 +1640,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1466
1640
|
});
|
|
1467
1641
|
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
1468
1642
|
shuttingDown = true;
|
|
1643
|
+
memoryToolLifetime.abort();
|
|
1469
1644
|
contextProjection.reset();
|
|
1470
1645
|
const restoring = cancelBranchRestoration();
|
|
1471
1646
|
const starting = cancelStartActivation();
|
|
@@ -206,14 +206,15 @@ export function backupCurrentStateFlowFiles(repositoryRoot: string, signal?: Abo
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/** Skip overlapping pushes; the next accepted turn can push the latest HEAD. */
|
|
209
|
-
export function startStateFlowBackupPush(repositoryRoot: string, onFailure: (error: unknown) => void, onSuccess?: () => void): boolean {
|
|
209
|
+
export function startStateFlowBackupPush(repositoryRoot: string, onFailure: (error: unknown) => void, onSuccess?: () => void, signal?: AbortSignal): boolean {
|
|
210
210
|
const root = resolve(repositoryRoot);
|
|
211
|
-
if (activePushes.has(root)) return false;
|
|
212
|
-
const push = pushCurrentStateFlowBackup(root).then(
|
|
211
|
+
if (signal?.aborted || activePushes.has(root)) return false;
|
|
212
|
+
const push = pushCurrentStateFlowBackup(root, signal).then(
|
|
213
213
|
(result) => {
|
|
214
|
-
if (result) { try { onSuccess?.(); } catch { /* Reporting cannot change push acceptance. */ } }
|
|
214
|
+
if (result && !signal?.aborted) { try { onSuccess?.(); } catch { /* Reporting cannot change push acceptance. */ } }
|
|
215
215
|
},
|
|
216
216
|
(error) => {
|
|
217
|
+
if (signal?.aborted) return;
|
|
217
218
|
try { onFailure(error); } catch { /* Reporting cannot revive a failed push. */ }
|
|
218
219
|
},
|
|
219
220
|
).finally(() => { activePushes.delete(root); });
|
|
@@ -228,15 +229,17 @@ export async function awaitInFlightBackupPushes(repositoryRoot: string): Promise
|
|
|
228
229
|
}
|
|
229
230
|
|
|
230
231
|
/** Push the current backup commit to its explicitly configured branch remote without blocking settlement. */
|
|
231
|
-
export function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{ commit: string; remote: string; ref: string } | undefined> {
|
|
232
|
+
export function pushCurrentStateFlowBackup(repositoryRoot: string, signal?: AbortSignal): Promise<{ commit: string; remote: string; ref: string } | undefined> {
|
|
232
233
|
return new Promise((resolvePush, rejectPush) => {
|
|
233
234
|
let root: string;
|
|
234
235
|
let commit: string | undefined;
|
|
235
236
|
let destination: { remote: string; ref: string } | undefined;
|
|
236
237
|
try {
|
|
238
|
+
signal?.throwIfAborted();
|
|
237
239
|
root = assertRepositoryRoot(repositoryRoot);
|
|
238
240
|
commit = currentHead(root);
|
|
239
241
|
destination = configuredPushDestination(root);
|
|
242
|
+
signal?.throwIfAborted();
|
|
240
243
|
} catch (error) {
|
|
241
244
|
rejectPush(new Error(redactGitDiagnostic(error instanceof Error ? error.message : String(error))));
|
|
242
245
|
return;
|
|
@@ -273,6 +276,7 @@ export function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{ co
|
|
|
273
276
|
if (settled) return;
|
|
274
277
|
settled = true;
|
|
275
278
|
clearTimeout(timeout);
|
|
279
|
+
signal?.removeEventListener("abort", cancelled);
|
|
276
280
|
if (error) rejectPush(error);
|
|
277
281
|
else resolvePush({ commit: commit!, ...destination! });
|
|
278
282
|
}
|
|
@@ -284,5 +288,10 @@ export function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{ co
|
|
|
284
288
|
child.once("exit", () => { if (failure) child.stderr?.destroy(); });
|
|
285
289
|
child.once("close", (code, endedBy) => finish(failure ?? (code === 0 ? undefined
|
|
286
290
|
: new Error(`Git backup push failed (${endedBy ?? code}): ${redactGitDiagnostic(stderr.trim()) || "no diagnostic output"}`))));
|
|
291
|
+
function cancelled(): void {
|
|
292
|
+
terminate(signal?.reason instanceof Error ? signal.reason : new Error("Git backup push cancelled"));
|
|
293
|
+
}
|
|
294
|
+
signal?.addEventListener("abort", cancelled, { once: true });
|
|
295
|
+
if (signal?.aborted) cancelled();
|
|
287
296
|
});
|
|
288
297
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseRetainedPiCheckpoint, type InactiveMode } from "./snapshot.ts";
|
|
1
|
+
import { parseRetainedPiCheckpoint, readCheckpointMode, type InactiveMode, type StateFlowMode } from "./snapshot.ts";
|
|
2
2
|
|
|
3
3
|
export const SNAPSHOT_ENTRY_TYPE = "state-flow-snapshot";
|
|
4
4
|
|
|
@@ -22,6 +22,8 @@ export interface PassiveStopBoundary {
|
|
|
22
22
|
persistenceError?: string;
|
|
23
23
|
/** The inactive mode selected by that failed change; legacy markers omit it. */
|
|
24
24
|
mode?: InactiveMode;
|
|
25
|
+
/** Native-only Off choice; canonical runtime mode is deliberately not updated. */
|
|
26
|
+
memoryDeferred?: true;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface SnapshotDiscovery {
|
|
@@ -44,6 +46,55 @@ export function discoverSnapshotData(branch: readonly BranchEntry[]): SnapshotDi
|
|
|
44
46
|
return { candidates, errors };
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/** Read native mode policy only; semantic checkpoint validity remains the recovery owner's concern. */
|
|
50
|
+
export function findBranchPolicy(
|
|
51
|
+
branch: readonly BranchEntry[], sessionId: string | undefined, stopEntryType: string | undefined, inactiveMode: InactiveMode,
|
|
52
|
+
): { mode: StateFlowMode; persistenceError?: string } | undefined {
|
|
53
|
+
let stopReset = false;
|
|
54
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
55
|
+
try {
|
|
56
|
+
const entry = branch[index];
|
|
57
|
+
if (entry?.type !== "custom") continue;
|
|
58
|
+
if (entry.customType === SNAPSHOT_ENTRY_TYPE) {
|
|
59
|
+
const mode = readCheckpointMode(entry.data, inactiveMode);
|
|
60
|
+
if (mode !== undefined) return { mode };
|
|
61
|
+
const data = entry.data as { disabled?: unknown; mode?: unknown; enabled?: unknown } | undefined;
|
|
62
|
+
if (data?.disabled === true && !Object.hasOwn(data, "mode") && !Object.hasOwn(data, "enabled")) return { mode: inactiveMode };
|
|
63
|
+
} else if (stopEntryType !== undefined && entry.customType === stopEntryType) {
|
|
64
|
+
const data = entry.data as PassiveStopBoundary & { owner?: unknown; reset?: unknown } | undefined;
|
|
65
|
+
if (!data || stopReset || (sessionId === undefined ? typeof data.owner !== "string" || !data.owner.trim() : data.owner !== sessionId)) continue;
|
|
66
|
+
if (data.reset === true) { stopReset = true; continue; }
|
|
67
|
+
if (Number.isSafeInteger(data.at) && data.at >= 0 && data.memoryDeferred === true && data.mode === "off") {
|
|
68
|
+
return { mode: "off", ...(typeof data.persistenceError === "string" && data.persistenceError.trim() ? { persistenceError: data.persistenceError } : {}) };
|
|
69
|
+
}
|
|
70
|
+
if (Number.isSafeInteger(data.at) && data.at >= 0 && typeof data.persistenceError === "string" && data.persistenceError.trim()) {
|
|
71
|
+
return { mode: data.mode === "off" || data.mode === "passive" ? data.mode : inactiveMode, persistenceError: data.persistenceError };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Malformed native policy cannot prevent another explicit retained choice from being read.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Native policy bookkeeping retains an unacquired fork across extension reloads. */
|
|
82
|
+
export function hasPendingFork(branch: readonly BranchEntry[], sessionId: string, entryType: string): boolean {
|
|
83
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
84
|
+
try {
|
|
85
|
+
const entry = branch[index];
|
|
86
|
+
if (entry?.type !== "custom" || entry.customType !== entryType) continue;
|
|
87
|
+
const data = entry.data as { owner?: unknown; reset?: unknown; forkPending?: unknown } | undefined;
|
|
88
|
+
if (data?.owner !== sessionId) continue;
|
|
89
|
+
if (data.reset === true) return false;
|
|
90
|
+
if (data.forkPending === true) return true;
|
|
91
|
+
} catch {
|
|
92
|
+
// Unrelated native entries grant no fork authority.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
47
98
|
export function snapshotDataNewestFirst(branch: readonly BranchEntry[]): unknown[] {
|
|
48
99
|
return discoverSnapshotData(branch).candidates;
|
|
49
100
|
}
|
|
@@ -26,10 +26,12 @@ export interface SnapshotConfig {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Read
|
|
30
|
-
*
|
|
29
|
+
* Read only mode policy, without validating or accessing semantic checkpoint metadata.
|
|
30
|
+
* Missing/invalid policy stays undecided; callers must not treat this as restoration proof.
|
|
31
|
+
* Legacy `enabled:false` follows the caller's inactive policy.
|
|
31
32
|
*/
|
|
32
|
-
function
|
|
33
|
+
export function readCheckpointMode(value: unknown, inactiveMode: InactiveMode = "passive"): StateFlowMode | undefined {
|
|
34
|
+
if (!isObject(value)) return undefined;
|
|
33
35
|
if (Object.hasOwn(value, "mode")) return Object.hasOwn(value, "enabled") || !isStateFlowMode(value.mode) ? undefined : value.mode;
|
|
34
36
|
return typeof value.enabled === "boolean" ? value.enabled ? "active" : inactiveMode : undefined;
|
|
35
37
|
}
|
|
@@ -134,7 +136,7 @@ export function parseSessionRuntime(
|
|
|
134
136
|
let runtime: unknown;
|
|
135
137
|
try {
|
|
136
138
|
const settings: unknown = JSON.parse(config);
|
|
137
|
-
const mode = isObject(settings) && Object.keys(settings).length === 1 ?
|
|
139
|
+
const mode = isObject(settings) && Object.keys(settings).length === 1 ? readCheckpointMode(settings, "passive") : undefined;
|
|
138
140
|
if (mode === undefined) throw new Error("Invalid State Flow runtime configuration or counters");
|
|
139
141
|
runtime = { config: { mode }, meta: JSON.parse(runtimeSource) };
|
|
140
142
|
} catch (error) {
|
|
@@ -230,7 +232,7 @@ export function parseRetainedPiCheckpoint(value: unknown, inactiveMode: Inactive
|
|
|
230
232
|
if (keys.length === 1 && value.disabled === true) return { mode: inactiveMode };
|
|
231
233
|
if (keys.length === 1 && (value.mode === "passive" || value.mode === "off")) return { mode: value.mode };
|
|
232
234
|
const allowed = new Set(["boundary", "mode", "enabled", "step", "bootstrap", "specification"]);
|
|
233
|
-
const mode =
|
|
235
|
+
const mode = readCheckpointMode(value, inactiveMode);
|
|
234
236
|
if (keys.some((key) => !allowed.has(key))
|
|
235
237
|
|| typeof value.boundary !== "string" || value.boundary.trim().length === 0
|
|
236
238
|
|| mode === undefined
|