@llblab/pi-kit 0.16.0 → 0.17.1
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/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +4 -4
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -1
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +24 -0
- package/node_modules/@llblab/pi-state-flow/README.md +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +2 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +35 -240
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +18 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +44 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +21 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +55 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +102 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +16 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +98 -12
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +38 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +10 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +23 -129
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +22 -17
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +15 -6
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +37 -229
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +53 -1
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +18 -4
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +51 -5
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +96 -0
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +99 -11
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +45 -0
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +13 -2
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +79 -0
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +23 -129
- package/package.json +2 -2
|
@@ -34,24 +34,33 @@ The model-facing surface remains small:
|
|
|
34
34
|
|
|
35
35
|
## Semantic model
|
|
36
36
|
|
|
37
|
-
Each scope may contain
|
|
37
|
+
Each scope may contain six semantic planes:
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
40
|
global | CWD | session
|
|
41
41
|
├── artifacts
|
|
42
42
|
├── contract
|
|
43
43
|
├── working
|
|
44
|
+
├── intents
|
|
44
45
|
├── response (session-owned where applicable)
|
|
45
46
|
└── lazy
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
`artifacts`, `contract`, `working`, and `response` remain hot. `lazy` differs only in projection policy:
|
|
49
|
+
`artifacts`, `contract`, `working`, `intents`, and `response` remain hot. `intents` may keep compact active direction while referring to large supporting detail in `lazy`. `lazy` differs only in projection policy:
|
|
49
50
|
|
|
50
51
|
- It is canonical semantic JSON, validated and versioned with its owning scope.
|
|
51
52
|
- It is excluded from the ordinary baseline effective-state body.
|
|
52
53
|
- It becomes model-visible only through bounded baseline navigation hints or explicit `read_state` output.
|
|
53
54
|
- Reading it does not mutate state, freshness, usage metadata, history, or future context.
|
|
54
55
|
|
|
56
|
+
### Semantic references
|
|
57
|
+
|
|
58
|
+
A reference is semantic content, not a runtime type. The optional `{"$ref":"cwd.lazy.plan"}` object provides the structured state-reference form. Inside any ordinary string or paragraph, a semantic-state reference uses `$` immediately followed by one valid `read_state` path, for example `$effective.lazy.memory[7]`. The prefix separates a deliberate reference from incidental path-like text and provides a deterministic seam if code-based parsing is ever justified. File paths, document sections, URIs, artifact locators, Skill identities, and agent identities retain their native syntax.
|
|
59
|
+
|
|
60
|
+
State Flow preserves all forms exactly as ordinary JSON. It does not scan prose, index targets, validate existence, rewrite relative locators, or infer authority, dependency, hydration, execution, or completion. When a reference matters, the agent resolves it explicitly with `read_state` for semantic paths or the appropriate external read/tool for other resources. A locator supports retrieval but does not replace content required for the current decision.
|
|
61
|
+
|
|
62
|
+
Reference repair is reactive, not a maintenance scan. The agent does not enumerate, audit, or resolve references merely to test them. Only after one requested `read_state` value path is missing does State Flow perform one bounded reverse lookup over current model-patchable semantic planes for exact structured `$ref` and `$path` matches. If found, the tool returns `{value:null, hint:[{type:"dangling-reference", message, paths}]}`. `hint` is explicit top-level metadata rather than state data; its action message asks for reconciliation and its path array contains at most three runtime-verified current owners. The null sentinel is never returned alone for this case. Keys, patch, and multi-path reads keep ordinary all-or-error semantics; no durable match retains the missing-path error and does not prove the agent invented the path. The agent may then reconcile a proven stale owning value while preserving surrounding meaning. This applies equally to `$ref` objects and contextual references in prose. Effective-state absence alone does not identify the owner, and unavailable history, inaccessible external resources, or transient read failure do not prove that a durable reference is broken.
|
|
63
|
+
|
|
55
64
|
Valid lazy values include:
|
|
56
65
|
|
|
57
66
|
```json
|
|
@@ -105,7 +114,7 @@ Active obligations, current constraints, unresolved next actions, and facts requ
|
|
|
105
114
|
|
|
106
115
|
`projection` defaults to `value`. A batch uses one projection for every path, evaluates every path against one captured state view, and returns results in request order. Duplicate paths remain duplicate results. If any path is invalid, the whole read fails; there is no mixed partial result.
|
|
107
116
|
|
|
108
|
-
The
|
|
117
|
+
The single `path` form is first-class. The retired top-level `offset` and `scope` inputs are rejected; history and ownership belong in the semantic path itself, such as `cwd[1].lazy.memory`.
|
|
109
118
|
|
|
110
119
|
### Response shape
|
|
111
120
|
|
|
@@ -117,13 +126,13 @@ There are three projections:
|
|
|
117
126
|
| `keys` | `{ "meta": ..., "keys": ... }` | Minimal structural facts followed by immediate keys |
|
|
118
127
|
| `patch` | `{ "patch": ... }` | Historical semantic patch at the selected boundary |
|
|
119
128
|
|
|
120
|
-
A single-path request returns one payload. A multi-path request returns positionally aligned arrays under the same projection fields.
|
|
129
|
+
A single-path request returns one payload. A multi-path request returns positionally aligned arrays under the same projection fields. One missing value path with exact current durable references instead returns `{ "value": null, "hint": [{ "type": "dangling-reference", "message": "Reconcile the verified current values that reference this path.", "paths": ["cwd.working.note"] }] }`; this explicit sentinel is diagnostic metadata, not semantic state.
|
|
121
130
|
|
|
122
|
-
`value` deliberately mirrors the effective-state snapshot injected at iteration start: it is semantic state without revision, provenance, range, transport, or storage fields. `patch` likewise contains only the selected semantic patch. Only structural discovery earns `meta`, and `keys` remains the final and most valuable field in that response.
|
|
131
|
+
`value` otherwise deliberately mirrors the effective-state snapshot injected at iteration start: it is semantic state without revision, provenance, range, transport, or storage fields. `patch` likewise contains only the selected semantic patch. Only structural discovery earns `meta`, and `keys` remains the final and most valuable field in that response.
|
|
123
132
|
|
|
124
133
|
The response does not repeat the requested path or projection and does not return an internal revision. Runtime owns revision selection, locking, CAS, and publication; the model cannot improve correctness by echoing that machinery.
|
|
125
134
|
|
|
126
|
-
Errors use the normal tool-error channel rather than successful JSON containing an `error` field.
|
|
135
|
+
Errors use the normal tool-error channel rather than successful JSON containing an `error` field. The explicit dangling-reference sentinel above is the sole missing-path exception; keys, patch, multi-path, and unmatched value reads still fail.
|
|
127
136
|
|
|
128
137
|
### Path and range model
|
|
129
138
|
|
|
@@ -20,7 +20,7 @@ State Flow does not undo tool effects. After interruption or returning to an old
|
|
|
20
20
|
|
|
21
21
|
Native fork replacement copies the source session checkpoint, retained patch tail and matching provenance into the new session's own storage. Global/CWD streams and provenance stay current and unchanged. An earlier fork selection copies that point's private state, not the parent's later private work. Parent data/history remain intact; selected enablement is retained, so a stopped source does not become enabled automatically.
|
|
22
22
|
|
|
23
|
-
The child starts at step zero and a new temporal origin. Its copied tail is preserved, but pre-origin records are not seven past aligned `
|
|
23
|
+
The child starts at step zero and a new temporal origin. Its copied tail is preserved, but pre-origin records are not seven past aligned causal boundaries addressable through `effective[n]` or scoped paths. The child's own transitions build its hot window; owned checkpoints support normal reload/resume. Parent Stop projection is not inherited, including after child reload.
|
|
24
24
|
|
|
25
25
|
Initial copying requires a native fork start event, a regular canonical direct-parent session file, matching CWD/identity, a readable temporal source and an unused child namespace. Missing/unsafe evidence or CAS conflicts leave the copy unavailable rather than importing unrelated or newer private state. Explicit Start can retry an unaccepted copy in the same loaded fork after the cause is corrected.
|
|
26
26
|
|
|
@@ -144,4 +144,4 @@ The agent should use sufficient materialized knowledge before rereading files. R
|
|
|
144
144
|
|
|
145
145
|
Knowledge discovery finds regular lowercase `*.md` beneath its configured root, hashes opaque bytes, and skips symlinks. Only confirmed missing Markdown paths within an available root are pruned; external/non-Markdown artifacts and state under a missing whole root are preserved. An unavailable root makes freshness unknown in status. Successful reads of stale ordinary candidates require same-path global compilation; Skill reads require CWD compilation. The runtime owns provenance: model patches cannot write or delete individual freshness fields, including legacy spellings. Existing legacy entries remain readable and semantically editable. Missing freshness evidence means unknown-but-usable, not proof that a source was acquired.
|
|
146
146
|
|
|
147
|
-
The
|
|
147
|
+
The packaged `state-flow-guide` Skill answers concrete operational questions about reads, patches, inheritance, acquisition, finalization, and recovery without initiating cleanup. The separate `state-flow-memory` Skill handles explicit bounded curation, feature/release/project phase-boundary reconciliation, and external promotion. Ordinary handoffs clean only touched and obviously stale visible branches; neither Skill is a background maintenance loop. Promotion must verify the destination before removing the only accepted source copy. Artifact/compiler details and model-tool contracts belong in the [architecture](architecture.md#artifact-routing).
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./artifact.ts";
|
|
21
21
|
import { canonicalJson, isJsonValue, isObject } from "./json.ts";
|
|
22
22
|
import { validateScopeStream, validateTemporalState, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
23
|
-
import type
|
|
23
|
+
import { migratePreIntentState, type StateScope } from "./state.ts";
|
|
24
24
|
|
|
25
25
|
const SESSION_KEY_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
26
26
|
const STATE_FILE = "state.json";
|
|
@@ -100,6 +100,7 @@ export function classifyScopeStream(
|
|
|
100
100
|
}
|
|
101
101
|
const temporal = meta?.temporal;
|
|
102
102
|
if (isObject(temporal) && Object.hasOwn(temporal, "checkpoint") && Array.isArray(temporal.patches)) {
|
|
103
|
+
checkpoint = migratePreIntentState(checkpoint) ?? checkpoint;
|
|
103
104
|
const owner = meta?.owner;
|
|
104
105
|
if (scope === "cwd" && expectedCwd !== undefined
|
|
105
106
|
&& (!isObject(owner) || Object.keys(owner).join(",") !== "cwd" || owner.cwd !== resolve(expectedCwd))) {
|
|
@@ -126,6 +127,10 @@ export function classifyScopeStream(
|
|
|
126
127
|
} else if (scope === "cwd" && expectedCwd !== undefined) {
|
|
127
128
|
throw new Error("State Flow CWD scope identity is missing");
|
|
128
129
|
}
|
|
130
|
+
if (isObject(legacyCheckpoint) && isObject(legacyCheckpoint.state)) {
|
|
131
|
+
const migrated = migratePreIntentState(legacyCheckpoint.state);
|
|
132
|
+
if (migrated) legacyCheckpoint = { ...legacyCheckpoint, state: migrated };
|
|
133
|
+
}
|
|
129
134
|
const stream = { checkpoint: legacyCheckpoint, patches };
|
|
130
135
|
validateScopeStream(stream, scope);
|
|
131
136
|
return { kind: "present", stream };
|
|
@@ -4,27 +4,24 @@ import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
|
-
import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./protocol.ts";
|
|
7
|
+
import { assistantToolCallCount, finalizedAssistantResponse, formatPatchStateArguments, normalizePatchStateArguments, separatedFailure, separatedOutput, stateFlowProtocol } from "./protocol.ts";
|
|
8
8
|
import { createPassiveContinuation, currentRunTrajectory, lazyNavigationHint, passiveContinuationMessages, runtimeContextMessage, syntheticUser, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation, type PassiveContinuation } from "./context.ts";
|
|
9
9
|
import { ArtifactReadTracker } from "./acquisition.ts";
|
|
10
10
|
import { loadStateFlowConfig } from "./config.ts";
|
|
11
11
|
import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramLoader } from "./telegram.ts";
|
|
12
|
-
import { isAbsolute,
|
|
12
|
+
import { isAbsolute, resolve } from "node:path";
|
|
13
13
|
import { SkillReadTracker } from "./skills.ts";
|
|
14
14
|
import { emptySnapshot, migrationFailure, persistableSnapshot, RevisionUnavailableError, type Snapshot } from "./snapshot.ts";
|
|
15
15
|
import { readNativeSessionHeader } from "./continuation.ts";
|
|
16
16
|
import { MissingSessionRuntimeError, SharedScopeRemovalConflictError, TemporalRuntime, type RuntimePublication } from "./runtime.ts";
|
|
17
17
|
import { emptyState, overlayStates, projectStateForModel, type AtomicScopePatches, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
|
|
18
18
|
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility, type StagedScopedTransition } from "./transition.ts";
|
|
19
|
-
import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
19
|
+
import { discoverSnapshotData, findAssistantToolBatch, findPassiveStopBoundary, hasPriorConversation, isNewSession, retainsPhysicalSessionProjection, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
20
20
|
import { compactStatus, detailedStatus, STATUS_KEY, type PendingPublicationDiagnostic, type StatusDiagnostics } from "./status.ts";
|
|
21
21
|
import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.ts";
|
|
22
22
|
import { recoverSnapshot } from "./recovery.ts";
|
|
23
23
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
24
|
-
import { resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.ts";
|
|
25
|
-
import { coalescePublicationTarget, createPublicationQueue, type PublicationQueueState } from "./publication.ts";
|
|
26
|
-
import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePath, removePublicationQueue, savePublicationQueue } from "./publication.ts";
|
|
27
|
-
import { runPublicationWorker } from "./publication.ts";
|
|
24
|
+
import { loadPublicationQueue, publicationQueuePath, PublicationWorkerController, resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument, type PublicationQueueState } from "./publication.ts";
|
|
28
25
|
import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.ts";
|
|
29
26
|
import { canonicalJson, isObject, sameJson } from "./json.ts";
|
|
30
27
|
import { readProjectedState, readStatePath } from "./query.ts";
|
|
@@ -35,7 +32,7 @@ import {
|
|
|
35
32
|
type SessionAddress,
|
|
36
33
|
} from "./durable.ts";
|
|
37
34
|
import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.ts";
|
|
38
|
-
import {
|
|
35
|
+
import { StateFlowDiagnosticWriter, stateFlowLogPath, type DiagnosticExtras, type StateFlowDiagnosticCategory } from "./logging.ts";
|
|
39
36
|
import { isGitCommitAncestor, pushGitCommit, pushGitTarget, resolveGitPushDestination } from "./git.ts";
|
|
40
37
|
import {
|
|
41
38
|
ORDINARY_ARTIFACT_COMPILER,
|
|
@@ -54,64 +51,13 @@ export interface StateFlowExtensionOptions {
|
|
|
54
51
|
passive?: { bootstrap?: boolean; tools?: boolean };
|
|
55
52
|
}
|
|
56
53
|
|
|
54
|
+
export { formatPatchStateArguments, normalizePatchStateArguments };
|
|
55
|
+
|
|
57
56
|
export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
58
57
|
export const READ_STATE_TOOL_NAME = "read_state";
|
|
59
58
|
export const MAX_FALLBACK_ATTEMPTS: number = 2;
|
|
60
59
|
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
61
60
|
const PUBLICATION_SHUTDOWN_WAIT_MS = 2_000;
|
|
62
|
-
const PATCH_DISPLAY_SECTION_KEYS = new Set([
|
|
63
|
-
"global",
|
|
64
|
-
"cwd",
|
|
65
|
-
"session",
|
|
66
|
-
"artifacts",
|
|
67
|
-
"contract",
|
|
68
|
-
"working",
|
|
69
|
-
"response",
|
|
70
|
-
"final",
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
/** Keep successful patch JSON valid while separating adjacent scopes and memory sections visually. */
|
|
74
|
-
export function formatPatchStateArguments(args: unknown): string {
|
|
75
|
-
const seenAtIndent = new Set<number>();
|
|
76
|
-
return JSON.stringify(args, null, 2).split("\n").flatMap((line) => {
|
|
77
|
-
const indent = line.length - line.trimStart().length;
|
|
78
|
-
const match = /^(\s+)"([^"]+)":/.exec(line);
|
|
79
|
-
if (match === null || !PATCH_DISPLAY_SECTION_KEYS.has(match[2])) {
|
|
80
|
-
for (const seenIndent of seenAtIndent) {
|
|
81
|
-
if (seenIndent > indent) seenAtIndent.delete(seenIndent);
|
|
82
|
-
}
|
|
83
|
-
return [line];
|
|
84
|
-
}
|
|
85
|
-
const separator = seenAtIndent.has(indent) ? [""] : [];
|
|
86
|
-
seenAtIndent.add(indent);
|
|
87
|
-
return [...separator, line];
|
|
88
|
-
}).join("\n");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Normalize a bounded compatibility superset without advertising aliases in the model-facing contract. */
|
|
92
|
-
export function normalizePatchStateArguments(args: unknown): any {
|
|
93
|
-
if (!isObject(args) || !Object.hasOwn(args, "final")) return args;
|
|
94
|
-
const value = args.final;
|
|
95
|
-
let final: boolean;
|
|
96
|
-
if (typeof value === "boolean") final = value;
|
|
97
|
-
else if (value === 1) final = true;
|
|
98
|
-
else if (value === 0) final = false;
|
|
99
|
-
else if (typeof value === "string" && value.trim().toLowerCase() === "true") final = true;
|
|
100
|
-
else if (typeof value === "string" && value.trim().toLowerCase() === "false") final = false;
|
|
101
|
-
else return args;
|
|
102
|
-
return { ...args, final };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Keep tool output visually separated from its heading with exactly one leading newline. */
|
|
106
|
-
function separatedOutput(text: string): string {
|
|
107
|
-
return `\n${text.replace(/^\n+/, "")}`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Keep a failed tool invocation visually separated from its rendered error without changing error semantics. */
|
|
111
|
-
function separatedFailure(error: unknown): Error {
|
|
112
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
-
return new Error(separatedOutput(message), error instanceof Error ? { cause: error } : undefined);
|
|
114
|
-
}
|
|
115
61
|
|
|
116
62
|
export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
|
|
117
63
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
@@ -145,15 +91,22 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
145
91
|
let pendingPublication: PendingPublicationDiagnostic | undefined;
|
|
146
92
|
let rehydrationPhase: RehydrationPhase | undefined;
|
|
147
93
|
let turnPublicationTarget: string | undefined;
|
|
148
|
-
const activePublicationWorkers = new Map<string, { controller: AbortController; done: Promise<void> }>();
|
|
149
|
-
let publicationStopped = false;
|
|
150
94
|
let publicationShutdown: Promise<void> | undefined;
|
|
151
95
|
const repositoryRoot = resolve(options.repositoryRoot ?? config.directory);
|
|
96
|
+
const diagnosticWriter = new StateFlowDiagnosticWriter(config.logging, stateFlowLogPath(agentDir), repositoryRoot, (message) => activeContext?.ui.notify(message, "warning"));
|
|
97
|
+
const publicationWorker = new PublicationWorkerController({
|
|
98
|
+
resolveDestination: () => resolveGitPushDestination(repositoryRoot),
|
|
99
|
+
isAncestor: (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant),
|
|
100
|
+
push: (destination, target, signal) => pushGitTarget(repositoryRoot, destination, target, signal),
|
|
101
|
+
onDiverged: (dropped, target) => activeContext && recordDiagnostic(
|
|
102
|
+
`Retired publication queue target ${dropped.target} after a journal lineage rewrite; retargeting to ${target}`,
|
|
103
|
+
"publication-conflict", activeContext,
|
|
104
|
+
),
|
|
105
|
+
});
|
|
152
106
|
const skillReads = new SkillReadTracker();
|
|
153
107
|
const artifactReads = new ArtifactReadTracker();
|
|
154
108
|
const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(agentDir));
|
|
155
109
|
let artifactInvalidations: ArtifactInvalidationRequest[] = [];
|
|
156
|
-
let loggingWarningReported = false;
|
|
157
110
|
let telegramStartPending = false;
|
|
158
111
|
|
|
159
112
|
function sessionAddress(ctx: ExtensionContext): SessionAddress {
|
|
@@ -206,27 +159,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
206
159
|
artifactReads.clear();
|
|
207
160
|
}
|
|
208
161
|
|
|
209
|
-
function passiveStopBoundary(ctx: ExtensionContext): { at: number; from?: number } | undefined {
|
|
210
|
-
for (const entry of [...ctx.sessionManager.getBranch()].reverse()) {
|
|
211
|
-
try {
|
|
212
|
-
if (entry?.type !== "custom" || entry.customType !== PASSIVE_STOP_ENTRY_TYPE) continue;
|
|
213
|
-
const { at, from, reset, owner } = (entry.data as { at?: unknown; from?: unknown; reset?: unknown; owner?: unknown } | undefined) ?? {};
|
|
214
|
-
if (reset === true && owner === ctx.sessionManager.getSessionId()) return undefined;
|
|
215
|
-
if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0) return {
|
|
216
|
-
at,
|
|
217
|
-
...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
|
|
218
|
-
};
|
|
219
|
-
} catch {
|
|
220
|
-
// A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return undefined;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function retainsPhysicalSessionProjection(reason: unknown): boolean {
|
|
227
|
-
return reason === undefined || reason === "startup" || reason === "reload" || reason === "resume";
|
|
228
|
-
}
|
|
229
|
-
|
|
230
162
|
function deferArtifactRefresh(): void {
|
|
231
163
|
artifactInvalidations = [];
|
|
232
164
|
artifactReads.setCandidates([]);
|
|
@@ -289,21 +221,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
289
221
|
: active.filter((name) => !owned.includes(name)));
|
|
290
222
|
}
|
|
291
223
|
|
|
292
|
-
function assistantToolBatch(ctx: ExtensionContext, toolCallId: string): string[] | undefined {
|
|
293
|
-
for (let cursor = ctx.sessionManager.getLeafEntry(); cursor; cursor = cursor.parentId ? ctx.sessionManager.getEntry(cursor.parentId) : undefined) {
|
|
294
|
-
const entry = cursor as { type?: unknown; message?: { role?: unknown; content?: unknown } };
|
|
295
|
-
if (entry.type !== "message" || entry.message?.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
|
|
296
|
-
const calls = entry.message.content.filter((block): block is { type: "toolCall"; id: string; name: string } => {
|
|
297
|
-
return typeof block === "object" && block !== null
|
|
298
|
-
&& (block as { type?: unknown }).type === "toolCall"
|
|
299
|
-
&& typeof (block as { id?: unknown }).id === "string"
|
|
300
|
-
&& typeof (block as { name?: unknown }).name === "string";
|
|
301
|
-
});
|
|
302
|
-
if (calls.some(({ id }) => id === toolCallId)) return calls.map(({ name }) => name);
|
|
303
|
-
}
|
|
304
|
-
return undefined;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
224
|
function recordPublication(publication: RuntimePublication, ctx: ExtensionContext): void {
|
|
308
225
|
const revision = publication.revision ?? publication.commit;
|
|
309
226
|
if (revision !== undefined) snapshot.meta.durableBase = revision;
|
|
@@ -330,8 +247,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
330
247
|
if (mode !== "turn-end" || target === undefined || !/^[0-9a-f]{40,64}$/.test(target)) return;
|
|
331
248
|
turnPublicationTarget = target;
|
|
332
249
|
try {
|
|
333
|
-
enqueueTurnPublication(
|
|
334
|
-
|
|
250
|
+
enqueueTurnPublication();
|
|
251
|
+
publicationWorker.launch();
|
|
335
252
|
} catch (error) {
|
|
336
253
|
ctx.ui.notify(
|
|
337
254
|
`State Flow accepted the local commit; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -372,95 +289,16 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
372
289
|
return true;
|
|
373
290
|
}
|
|
374
291
|
|
|
375
|
-
function enqueueTurnPublication(
|
|
292
|
+
function enqueueTurnPublication(): void {
|
|
376
293
|
const target = turnPublicationTarget;
|
|
377
294
|
turnPublicationTarget = undefined;
|
|
378
|
-
if (
|
|
379
|
-
const destination = resolveGitPushDestination(repositoryRoot);
|
|
380
|
-
if (!destination) return;
|
|
381
|
-
const path = publicationQueuePath(destination);
|
|
382
|
-
const previous = loadPublicationQueue(path);
|
|
383
|
-
const next = previous
|
|
384
|
-
? coalescePublicationTarget(previous, destination, target, (ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant), {
|
|
385
|
-
onDivergedLineage: (dropped) => recordDiagnostic(
|
|
386
|
-
`Retired publication queue target ${dropped.target} after a journal lineage rewrite; retargeting to ${target}`,
|
|
387
|
-
"publication-conflict", ctx,
|
|
388
|
-
),
|
|
389
|
-
})
|
|
390
|
-
: createPublicationQueue(destination, target);
|
|
391
|
-
savePublicationQueue(path, next, previous);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function launchPublicationWorker(): void {
|
|
395
|
-
if (publicationStopped) return;
|
|
396
|
-
let destination: ReturnType<typeof resolveGitPushDestination>;
|
|
397
|
-
try {
|
|
398
|
-
destination = resolveGitPushDestination(repositoryRoot);
|
|
399
|
-
} catch (error) {
|
|
400
|
-
if (error instanceof Error && /ENOENT/.test(error.message)) return;
|
|
401
|
-
throw error;
|
|
402
|
-
}
|
|
403
|
-
if (!destination) return;
|
|
404
|
-
const path = publicationQueuePath(destination);
|
|
405
|
-
if (activePublicationWorkers.has(path)) return;
|
|
406
|
-
let queued: PublicationQueueState | undefined;
|
|
407
|
-
let lease: ReturnType<typeof acquirePublicationWorkerLease>;
|
|
408
|
-
try {
|
|
409
|
-
queued = loadPublicationQueue(path);
|
|
410
|
-
if (!queued) return;
|
|
411
|
-
lease = acquirePublicationWorkerLease(path);
|
|
412
|
-
} catch {
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
if (!lease) return;
|
|
416
|
-
const controller = new AbortController();
|
|
417
|
-
const done = runPublicationWorker(
|
|
418
|
-
queued,
|
|
419
|
-
({ target }) => pushGitTarget(repositoryRoot, destination, target, controller.signal),
|
|
420
|
-
() => loadPublicationQueue(path) ?? queued,
|
|
421
|
-
(ancestor, descendant) => isGitCommitAncestor(repositoryRoot, ancestor, descendant),
|
|
422
|
-
).then((result) => {
|
|
423
|
-
if (publicationStopped) return; // Late outcomes belong to an unconfirmed queue, not the replacement generation.
|
|
424
|
-
const current = loadPublicationQueue(path);
|
|
425
|
-
if (!current) return;
|
|
426
|
-
if (result.next === undefined) {
|
|
427
|
-
if (current.target === result.attempted.target) removePublicationQueue(path, current);
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
if (current.target === result.attempted.target || result.next.target === current.target) {
|
|
431
|
-
savePublicationQueue(path, result.next, current);
|
|
432
|
-
}
|
|
433
|
-
}).catch(() => {
|
|
434
|
-
// Queue/CAS truth remains durable; status and a later activation expose retry.
|
|
435
|
-
}).finally(() => {
|
|
436
|
-
activePublicationWorkers.delete(path);
|
|
437
|
-
try {
|
|
438
|
-
lease.release();
|
|
439
|
-
if (!publicationStopped && loadPublicationQueue(path)?.status === "pending") launchPublicationWorker();
|
|
440
|
-
} catch {
|
|
441
|
-
// Failed lease cleanup or malformed persistence stays inert until retry or repair.
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
activePublicationWorkers.set(path, { controller, done });
|
|
295
|
+
if (target) publicationWorker.enqueue(target);
|
|
445
296
|
}
|
|
446
297
|
|
|
447
298
|
function shutdownPublicationWorkers(ctx: ExtensionContext): Promise<void> {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
for (const { controller } of workers) controller.abort();
|
|
452
|
-
if (workers.length === 0) return;
|
|
453
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
454
|
-
try {
|
|
455
|
-
const completed = await Promise.race([
|
|
456
|
-
Promise.all(workers.map(({ done }) => done)).then(() => true),
|
|
457
|
-
new Promise<false>((resolve) => { timeout = setTimeout(() => resolve(false), PUBLICATION_SHUTDOWN_WAIT_MS); }),
|
|
458
|
-
]);
|
|
459
|
-
if (!completed) ctx.ui.notify(`State Flow push cleanup is unconfirmed after ${PUBLICATION_SHUTDOWN_WAIT_MS}ms; worker leases remain held until child exit.`, "warning");
|
|
460
|
-
} finally {
|
|
461
|
-
clearTimeout(timeout);
|
|
462
|
-
}
|
|
463
|
-
})();
|
|
299
|
+
return publicationShutdown ??= publicationWorker.shutdown(PUBLICATION_SHUTDOWN_WAIT_MS).then((completed) => {
|
|
300
|
+
if (!completed) ctx.ui.notify(`State Flow push cleanup is unconfirmed after ${PUBLICATION_SHUTDOWN_WAIT_MS}ms; worker leases remain held until child exit.`, "warning");
|
|
301
|
+
});
|
|
464
302
|
}
|
|
465
303
|
|
|
466
304
|
function retryPendingPush(ctx: ExtensionContext): void {
|
|
@@ -472,8 +310,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
472
310
|
if (mode === "turn-end") {
|
|
473
311
|
turnPublicationTarget = target;
|
|
474
312
|
try {
|
|
475
|
-
enqueueTurnPublication(
|
|
476
|
-
|
|
313
|
+
enqueueTurnPublication();
|
|
314
|
+
publicationWorker.launch();
|
|
477
315
|
} catch (error) {
|
|
478
316
|
ctx.ui.notify(`State Flow retained local state; asynchronous publication recovery is deferred: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
479
317
|
}
|
|
@@ -681,7 +519,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
681
519
|
ctx.ui.notify(`State Flow restored disabled: ${snapshot.meta.validation.error}`, "error");
|
|
682
520
|
}
|
|
683
521
|
if (retainsPhysicalSessionProjection(sessionStartReason) && runtime?.view) {
|
|
684
|
-
const boundary =
|
|
522
|
+
const boundary = findPassiveStopBoundary(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId(), PASSIVE_STOP_ENTRY_TYPE);
|
|
685
523
|
if (boundary !== undefined) {
|
|
686
524
|
const continuation = createPassiveContinuation(
|
|
687
525
|
projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)),
|
|
@@ -705,41 +543,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
705
543
|
updateUi(ctx);
|
|
706
544
|
}
|
|
707
545
|
|
|
708
|
-
interface DiagnosticExtras {
|
|
709
|
-
content?: unknown;
|
|
710
|
-
input?: unknown;
|
|
711
|
-
tool?: string;
|
|
712
|
-
toolCallId?: string;
|
|
713
|
-
resolutionAttempt?: number;
|
|
714
|
-
terminalEligible?: boolean;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
546
|
function recordDiagnostic(error: string, category: StateFlowDiagnosticCategory, ctx: ExtensionContext, extras: DiagnosticExtras = {}): void {
|
|
718
|
-
|
|
719
|
-
try {
|
|
720
|
-
const path = stateFlowLogPath(agentDir);
|
|
721
|
-
const fromRepository = relative(repositoryRoot, path);
|
|
722
|
-
if (fromRepository === "" || (!isAbsolute(fromRepository) && fromRepository !== ".." && !fromRepository.startsWith(`..${sep}`))) {
|
|
723
|
-
throw new Error("diagnostic path overlaps the State Flow repository");
|
|
724
|
-
}
|
|
725
|
-
appendStateFlowDiagnostic(path, {
|
|
726
|
-
at: new Date().toISOString(),
|
|
727
|
-
sessionId: sessionAddress(ctx).id,
|
|
728
|
-
cwd: resolve(ctx.cwd),
|
|
729
|
-
category,
|
|
730
|
-
error,
|
|
731
|
-
...(extras.content === undefined ? {} : { content: projectDiagnosticContent(extras.content) }),
|
|
732
|
-
...(extras.input === undefined ? {} : { input: extras.input }),
|
|
733
|
-
...(extras.tool === undefined ? {} : { tool: extras.tool }),
|
|
734
|
-
...(extras.toolCallId === undefined ? {} : { toolCallId: extras.toolCallId }),
|
|
735
|
-
...(extras.resolutionAttempt === undefined ? {} : { resolutionAttempt: extras.resolutionAttempt }),
|
|
736
|
-
...(extras.terminalEligible === undefined ? {} : { terminalEligible: extras.terminalEligible }),
|
|
737
|
-
});
|
|
738
|
-
} catch (failure) {
|
|
739
|
-
if (loggingWarningReported) return;
|
|
740
|
-
loggingWarningReported = true;
|
|
741
|
-
ctx.ui.notify(`State Flow could not write diagnostics: ${failure instanceof Error ? failure.message : String(failure)}`, "warning");
|
|
742
|
-
}
|
|
547
|
+
diagnosticWriter.record(sessionAddress(ctx).id, ctx.cwd, error, category, extras);
|
|
743
548
|
}
|
|
744
549
|
|
|
745
550
|
function continueFallbackResolution(lastWarning: boolean): void {
|
|
@@ -977,8 +782,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
977
782
|
);
|
|
978
783
|
}
|
|
979
784
|
branchHasSnapshot = true;
|
|
785
|
+
// Activation reasserts the selected session stream as a complete cohort. A resumed
|
|
786
|
+
// branch may have no reusable temporal revision after an extension upgrade; a
|
|
787
|
+
// runtime-only write would then reject its selected session as an omitted change.
|
|
980
788
|
const publication = runtime.view
|
|
981
|
-
? runtime.promote(snapshot) ?? runtime.publish(snapshot)
|
|
789
|
+
? runtime.promote(snapshot) ?? runtime.publish(snapshot, true)
|
|
982
790
|
: runtime.initialize(snapshot, true, undefined, branchStartsWithoutRuntime);
|
|
983
791
|
recordPolicyPublication(publication, ctx);
|
|
984
792
|
installScopeStates();
|
|
@@ -1193,7 +1001,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1193
1001
|
|
|
1194
1002
|
pi.on("tool_call", (event, ctx) => {
|
|
1195
1003
|
if (!snapshot.config.enabled) return;
|
|
1196
|
-
const batch =
|
|
1004
|
+
const batch = findAssistantToolBatch(ctx.sessionManager, event.toolCallId);
|
|
1197
1005
|
const patchCalls = batch?.filter((name) => name === PATCH_STATE_TOOL_NAME).length ?? 0;
|
|
1198
1006
|
if (patchCalls > 0) {
|
|
1199
1007
|
if (patchCalls !== 1) {
|
|
@@ -1264,8 +1072,8 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1264
1072
|
responseCommitted = true;
|
|
1265
1073
|
bootstrapContinuation = undefined;
|
|
1266
1074
|
rehydrationPhase = "step";
|
|
1267
|
-
enqueueTurnPublication(
|
|
1268
|
-
if (snapshot.meta.remotePublication?.mode === "turn-end")
|
|
1075
|
+
enqueueTurnPublication();
|
|
1076
|
+
if (snapshot.meta.remotePublication?.mode === "turn-end") publicationWorker.launch();
|
|
1269
1077
|
completedRunAccepted = !wasBootstrap;
|
|
1270
1078
|
} catch (error) {
|
|
1271
1079
|
if (!responseCommitted && specification !== undefined) snapshot.meta.specification = specification;
|
|
@@ -1292,7 +1100,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1292
1100
|
telegramStartPending = false;
|
|
1293
1101
|
startStateFlow(ctx);
|
|
1294
1102
|
}
|
|
1295
|
-
if (snapshot.meta.remotePublication?.mode === "turn-end")
|
|
1103
|
+
if (snapshot.meta.remotePublication?.mode === "turn-end") publicationWorker.launch();
|
|
1296
1104
|
if (!completedRunAccepted || compactionStopped || !snapshot.config.enabled || snapshot.meta.bootstrap || resolutionPending
|
|
1297
1105
|
|| compactionInFlight || !ctx.isIdle() || ctx.hasPendingMessages() || !snapshot.meta.durableBase
|
|
1298
1106
|
|| !shouldRequestStateFlowCompaction(ctx.getContextUsage())) return;
|
|
@@ -1314,7 +1122,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1314
1122
|
rehydrationPhase = event.reason === "resume" ? "resume-bootstrap" : "new-bootstrap";
|
|
1315
1123
|
restoreActiveBranch(ctx, event.reason);
|
|
1316
1124
|
retryPendingPush(ctx);
|
|
1317
|
-
if (snapshot.meta.remotePublication?.mode === "turn-end")
|
|
1125
|
+
if (snapshot.meta.remotePublication?.mode === "turn-end") publicationWorker.launch();
|
|
1318
1126
|
updateUi(ctx);
|
|
1319
1127
|
void telegram.ensure();
|
|
1320
1128
|
});
|
|
@@ -22,7 +22,7 @@ export interface RecentTransition extends AcceptedTransition {
|
|
|
22
22
|
|
|
23
23
|
export type RecentTransitionWindow = RecentTransition[];
|
|
24
24
|
const SCOPES = new Set<StateScope>(["global", "cwd", "session"]);
|
|
25
|
-
const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response", "lazy"]);
|
|
25
|
+
const PATCH_KEYS = new Set(["artifacts", "contract", "working", "intents", "response", "lazy"]);
|
|
26
26
|
|
|
27
27
|
/** Normalize accepted replacements into recursive-merge replay, including removals. */
|
|
28
28
|
function replayPatch(before: JsonObject, after: JsonObject): JsonObject {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Domain: opt-in diagnostic capture for rejected State Flow resolutions.
|
|
2
2
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { isObject } from "./json.ts";
|
|
5
5
|
|
|
6
6
|
export type StateFlowDiagnosticCategory = "invalid-patch" | "publication-conflict" | "terminal-pending" | "finalization";
|
|
@@ -45,3 +45,55 @@ export function appendStateFlowDiagnostic(path: string, record: StateFlowDiagnos
|
|
|
45
45
|
mkdirSync(dirname(path), { recursive: true });
|
|
46
46
|
appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
export interface DiagnosticExtras {
|
|
50
|
+
content?: unknown;
|
|
51
|
+
input?: unknown;
|
|
52
|
+
tool?: string;
|
|
53
|
+
toolCallId?: string;
|
|
54
|
+
resolutionAttempt?: number;
|
|
55
|
+
terminalEligible?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Own diagnostic path safety, projection, persistence, and one-shot failure reporting. */
|
|
59
|
+
export class StateFlowDiagnosticWriter {
|
|
60
|
+
private warningReported = false;
|
|
61
|
+
private readonly enabled: boolean;
|
|
62
|
+
private readonly path: string;
|
|
63
|
+
private readonly repositoryRoot: string;
|
|
64
|
+
private readonly notify: (message: string) => void;
|
|
65
|
+
|
|
66
|
+
constructor(enabled: boolean, path: string, repositoryRoot: string, notify: (message: string) => void) {
|
|
67
|
+
this.enabled = enabled;
|
|
68
|
+
this.path = path;
|
|
69
|
+
this.repositoryRoot = repositoryRoot;
|
|
70
|
+
this.notify = notify;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
record(sessionId: string, cwd: string, error: string, category: StateFlowDiagnosticCategory, extras: DiagnosticExtras = {}): void {
|
|
74
|
+
if (!this.enabled) return;
|
|
75
|
+
try {
|
|
76
|
+
const fromRepository = relative(this.repositoryRoot, this.path);
|
|
77
|
+
if (fromRepository === "" || (!isAbsolute(fromRepository) && fromRepository !== ".." && !fromRepository.startsWith(`..${sep}`))) {
|
|
78
|
+
throw new Error("diagnostic path overlaps the State Flow repository");
|
|
79
|
+
}
|
|
80
|
+
appendStateFlowDiagnostic(this.path, {
|
|
81
|
+
at: new Date().toISOString(),
|
|
82
|
+
sessionId,
|
|
83
|
+
cwd: resolve(cwd),
|
|
84
|
+
category,
|
|
85
|
+
error,
|
|
86
|
+
...(extras.content === undefined ? {} : { content: projectDiagnosticContent(extras.content) }),
|
|
87
|
+
...(extras.input === undefined ? {} : { input: extras.input }),
|
|
88
|
+
...(extras.tool === undefined ? {} : { tool: extras.tool }),
|
|
89
|
+
...(extras.toolCallId === undefined ? {} : { toolCallId: extras.toolCallId }),
|
|
90
|
+
...(extras.resolutionAttempt === undefined ? {} : { resolutionAttempt: extras.resolutionAttempt }),
|
|
91
|
+
...(extras.terminalEligible === undefined ? {} : { terminalEligible: extras.terminalEligible }),
|
|
92
|
+
});
|
|
93
|
+
} catch (failure) {
|
|
94
|
+
if (this.warningReported) return;
|
|
95
|
+
this.warningReported = true;
|
|
96
|
+
this.notify(`State Flow could not write diagnostics: ${failure instanceof Error ? failure.message : String(failure)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type OwnedFileUpdate,
|
|
14
14
|
} from "./durable.ts";
|
|
15
15
|
import { parseSessionRuntime, serializeSessionRuntime } from "./snapshot.ts";
|
|
16
|
-
import type
|
|
16
|
+
import { migratePreIntentState, type StateScope } from "./state.ts";
|
|
17
17
|
|
|
18
18
|
export interface LegacyStorageMigration {
|
|
19
19
|
bases: DurableFileBase[];
|
|
@@ -57,9 +57,14 @@ function migrationDirectories(cwd: string, sessionId: string, root: string, sess
|
|
|
57
57
|
try { sessionScopeKey(entry.name); } catch { continue; }
|
|
58
58
|
const directory = join(cwdDirectory, entry.name);
|
|
59
59
|
let meta: unknown;
|
|
60
|
+
let runtime: unknown;
|
|
60
61
|
try { meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8")); }
|
|
61
62
|
catch { continue; }
|
|
62
|
-
|
|
63
|
+
try { runtime = JSON.parse(readFileSync(join(directory, "runtime.json"), "utf8")); }
|
|
64
|
+
catch { /* predecessor sessions keep identity in meta.json */ }
|
|
65
|
+
const identity = meta && typeof meta === "object" && !Array.isArray(meta) && (meta as { identity?: unknown }).identity !== undefined
|
|
66
|
+
? (meta as { identity?: unknown }).identity
|
|
67
|
+
: runtime && typeof runtime === "object" && !Array.isArray(runtime) ? (runtime as { identity?: unknown }).identity : undefined;
|
|
63
68
|
if (!identity || typeof identity !== "object" || Array.isArray(identity)) continue;
|
|
64
69
|
const owner = identity as { cwd?: unknown; sessionId?: unknown };
|
|
65
70
|
if (owner.cwd === cwdOwner && typeof owner.sessionId === "string" && owner.sessionId.length > 0) {
|
|
@@ -85,7 +90,14 @@ export function hasCwdMaterialization(cwd: string, repositoryRoot: string): bool
|
|
|
85
90
|
return false;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
function checkpointNeedsIntentMigration(source: string): boolean {
|
|
94
|
+
const checkpoint = JSON.parse(source) as unknown;
|
|
95
|
+
if (migratePreIntentState(checkpoint) !== undefined) return true;
|
|
96
|
+
return checkpoint !== null && typeof checkpoint === "object" && !Array.isArray(checkpoint)
|
|
97
|
+
&& migratePreIntentState((checkpoint as { state?: unknown }).state) !== undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Detect predecessor snapshots, temporal envelopes, or pre-intents semantic states without mutating the store. */
|
|
89
101
|
export function hasLegacyStateSources(
|
|
90
102
|
cwd: string,
|
|
91
103
|
sessionId: string,
|
|
@@ -94,8 +106,10 @@ export function hasLegacyStateSources(
|
|
|
94
106
|
): boolean {
|
|
95
107
|
const root = resolve(repositoryRoot);
|
|
96
108
|
return migrationDirectories(cwd, sessionId, root, sessionKey).some(({ directory, scope }) => {
|
|
97
|
-
|
|
109
|
+
const checkpointPath = join(directory, "checkpoint.json");
|
|
110
|
+
if (lstatSync(checkpointPath, { throwIfNoEntry: false }) === undefined) return false;
|
|
98
111
|
try {
|
|
112
|
+
if (checkpointNeedsIntentMigration(readFileSync(checkpointPath, "utf8"))) return true;
|
|
99
113
|
const meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8"));
|
|
100
114
|
if (meta === null || typeof meta !== "object" || !("temporal" in meta)) return true;
|
|
101
115
|
return scope === "session"
|