@llblab/pi-kit 0.18.1 → 0.19.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/CHANGELOG.md +12 -0
- package/README.md +4 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +35 -37
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -1
- package/node_modules/@llblab/pi-state-flow/README.md +102 -48
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +6 -9
- package/node_modules/@llblab/pi-state-flow/dist/index.js +6 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +21 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +40 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +93 -63
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +6 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +56 -58
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +12 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +7 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +47 -78
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +4 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +241 -437
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -72
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +120 -499
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +43 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +1 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +1 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +5 -37
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +13 -32
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +25 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +15 -12
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +2 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +5 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +26 -88
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +159 -255
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +0 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +1 -47
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +16 -33
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +48 -137
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +16 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +13 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +15 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +5 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +13 -45
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +15 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +104 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +11 -14
- package/node_modules/@llblab/pi-state-flow/dist/package.json +9 -6
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +11 -17
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +8 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +64 -67
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +24 -6
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -15
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +22 -27
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +25 -41
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +38 -421
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +33 -46
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -61
- package/node_modules/@llblab/pi-state-flow/index.ts +7 -71
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +26 -11
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +116 -88
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +12 -10
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +52 -59
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +11 -20
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +44 -86
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +237 -460
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +110 -552
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +39 -23
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -7
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +5 -44
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -25
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +26 -22
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +17 -11
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +7 -5
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +154 -254
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +1 -49
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +58 -142
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +27 -19
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -54
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +12 -40
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +104 -22
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +16 -26
- package/node_modules/@llblab/pi-state-flow/package.json +9 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +11 -17
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +8 -8
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +13 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +1 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +72 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +27 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +17 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +8 -1
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +0 -10
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +18 -41
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +126 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +46 -0
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +8 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +0 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +0 -125
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +0 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +0 -98
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +0 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +0 -167
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +0 -86
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +0 -437
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +0 -133
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +0 -147
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +0 -171
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +0 -458
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
classifyArtifactCompilationNeed,
|
|
3
|
+
inspectRegisteredArtifactPaths,
|
|
4
|
+
sameArtifactSourceFingerprint,
|
|
5
|
+
type ArtifactInvalidationReason,
|
|
6
|
+
type ArtifactInvalidationRequest,
|
|
7
|
+
type ArtifactSourceFingerprint,
|
|
8
|
+
type ArtifactSourceIdentity,
|
|
6
9
|
} from "./artifact.ts";
|
|
7
10
|
import { isObject } from "./json.ts";
|
|
8
11
|
|
|
@@ -35,7 +38,7 @@ export interface ArtifactAcquisitionOptions {
|
|
|
35
38
|
/** Caller-assessed semantic sufficiency; only relevant to a concrete relevant gap. */
|
|
36
39
|
materializedSufficient?: boolean;
|
|
37
40
|
explicitRefresh?: boolean;
|
|
38
|
-
/** Runtime-owned
|
|
41
|
+
/** Runtime-owned compilation evidence retained beside the semantic artifact. */
|
|
39
42
|
provenance?: unknown;
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -45,6 +48,7 @@ export interface SuccessfulArtifactRead extends ArtifactInvalidationRequest {}
|
|
|
45
48
|
interface PendingRead {
|
|
46
49
|
toolName: string;
|
|
47
50
|
args: unknown;
|
|
51
|
+
fingerprint?: ArtifactSourceFingerprint;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
function readPath(toolName: unknown, args: unknown): string | undefined {
|
|
@@ -84,7 +88,12 @@ export class ArtifactReadTracker {
|
|
|
84
88
|
const path = readPath(pending.toolName, pending.args);
|
|
85
89
|
if (path === undefined) return;
|
|
86
90
|
const candidate = this.#candidates.get(path);
|
|
87
|
-
if (candidate
|
|
91
|
+
if (candidate === undefined || pending.fingerprint === undefined) return;
|
|
92
|
+
const observation = inspectRegisteredArtifactPaths([path])[0];
|
|
93
|
+
if (observation?.kind !== "present" || !sameArtifactSourceFingerprint(pending.fingerprint, observation.fingerprint)) return;
|
|
94
|
+
const finalObservation = inspectRegisteredArtifactPaths([path])[0];
|
|
95
|
+
if (finalObservation?.kind !== "present" || !sameArtifactSourceFingerprint(observation.fingerprint, finalObservation.fingerprint)) return;
|
|
96
|
+
this.successful.set(path, structuredClone({ ...candidate, sourceFingerprint: finalObservation.fingerprint }));
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
#record(toolCallId: string, toolName: string, args: unknown): void {
|
|
@@ -92,14 +101,20 @@ export class ArtifactReadTracker {
|
|
|
92
101
|
this.#pending.delete(toolCallId);
|
|
93
102
|
return;
|
|
94
103
|
}
|
|
95
|
-
|
|
104
|
+
const path = readPath(toolName, args);
|
|
105
|
+
const observation = path !== undefined && this.#candidates.has(path) ? inspectRegisteredArtifactPaths([path])[0] : undefined;
|
|
106
|
+
this.#pending.set(toolCallId, {
|
|
107
|
+
toolName,
|
|
108
|
+
args,
|
|
109
|
+
...(observation?.kind === "present" ? { fingerprint: observation.fingerprint } : {}),
|
|
110
|
+
});
|
|
96
111
|
}
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
/**
|
|
100
115
|
* Apply one materialized-first source acquisition policy.
|
|
101
116
|
*
|
|
102
|
-
*
|
|
117
|
+
* Required recompilation always wins. Otherwise routine use and a new session
|
|
103
118
|
* stay on materialized state; only a concrete source need permits rereading.
|
|
104
119
|
*/
|
|
105
120
|
export function decideArtifactAcquisition(
|
|
@@ -108,15 +123,15 @@ export function decideArtifactAcquisition(
|
|
|
108
123
|
compiler: string,
|
|
109
124
|
options: ArtifactAcquisitionOptions,
|
|
110
125
|
): ArtifactAcquisitionDecision {
|
|
111
|
-
const
|
|
126
|
+
const need = classifyArtifactCompilationNeed(
|
|
112
127
|
source,
|
|
113
128
|
metadata,
|
|
114
129
|
compiler,
|
|
115
130
|
options.explicitRefresh ?? false,
|
|
116
131
|
options.provenance,
|
|
117
132
|
);
|
|
118
|
-
if (
|
|
119
|
-
return { kind: "read-source", reason:
|
|
133
|
+
if (need.kind === "requires-compilation") {
|
|
134
|
+
return { kind: "read-source", reason: need.reason };
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
switch (options.intent) {
|
|
@@ -1,16 +1,76 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
4
|
import { containsNull, isJsonValue, isObject, type JsonObject, type JsonValue } from "./json.ts";
|
|
3
5
|
|
|
4
6
|
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
5
|
-
const MODEL_FORBIDDEN_PROVENANCE_FIELDS = ["hash", "compiler", "sourceHash", "compilerRevision", "compiledAt", "source_hash_verified"] as const;
|
|
7
|
+
const MODEL_FORBIDDEN_PROVENANCE_FIELDS = ["hash", "compiler", "sourceHash", "sourceFingerprint", "compilerRevision", "compiledAt", "source_hash_verified"] as const;
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
export interface ArtifactSourceFingerprint {
|
|
10
|
+
size: number;
|
|
11
|
+
mtimeNs: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseArtifactSourceFingerprint(value: unknown): ArtifactSourceFingerprint | undefined {
|
|
15
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
16
|
+
const candidate = value as Record<string, unknown>;
|
|
17
|
+
if (Object.keys(candidate).sort().join(",") !== "mtimeNs,size"
|
|
18
|
+
|| !Number.isSafeInteger(candidate.size) || (candidate.size as number) < 0
|
|
19
|
+
|| typeof candidate.mtimeNs !== "string" || !/^-?\d+$/.test(candidate.mtimeNs)) return undefined;
|
|
20
|
+
return { size: candidate.size as number, mtimeNs: candidate.mtimeNs };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sameArtifactSourceFingerprint(left: ArtifactSourceFingerprint, right: ArtifactSourceFingerprint): boolean {
|
|
24
|
+
return left.size === right.size && left.mtimeNs === right.mtimeNs;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ArtifactSourceObservation =
|
|
28
|
+
| { path: string; kind: "present"; fingerprint: ArtifactSourceFingerprint }
|
|
29
|
+
| { path: string; kind: "missing" }
|
|
30
|
+
| { path: string; kind: "unavailable"; reason: string };
|
|
31
|
+
|
|
32
|
+
function artifactSourceErrorCode(error: unknown): string | undefined {
|
|
33
|
+
return error instanceof Error && "code" in error
|
|
34
|
+
? (error as NodeJS.ErrnoException).code
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Inspect only exact registered artifact paths without reading source bodies or traversing directories. */
|
|
39
|
+
export function inspectRegisteredArtifactPaths(paths: Iterable<string>): ArtifactSourceObservation[] {
|
|
40
|
+
return [...new Set(paths)].sort().map((path) => {
|
|
41
|
+
if (!isAbsolute(path) || resolve(path) !== path) {
|
|
42
|
+
return { path, kind: "unavailable", reason: "artifact path is not canonical and absolute" };
|
|
43
|
+
}
|
|
44
|
+
let metadata;
|
|
45
|
+
try {
|
|
46
|
+
metadata = lstatSync(path, { bigint: true });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const code = artifactSourceErrorCode(error);
|
|
49
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { path, kind: "missing" };
|
|
50
|
+
return { path, kind: "unavailable", reason: error instanceof Error ? error.message : String(error) };
|
|
51
|
+
}
|
|
52
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
53
|
+
return { path, kind: "unavailable", reason: "artifact source is not a regular non-symlink file" };
|
|
54
|
+
}
|
|
55
|
+
if (metadata.size > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
56
|
+
return { path, kind: "unavailable", reason: "artifact source size exceeds the supported range" };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
path,
|
|
60
|
+
kind: "present",
|
|
61
|
+
fingerprint: { size: Number(metadata.size), mtimeNs: metadata.mtimeNs.toString() },
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Current compiler protocol for ordinary registered source artifacts. */
|
|
8
67
|
export const ORDINARY_ARTIFACT_COMPILER = "artifact-v1";
|
|
9
68
|
|
|
10
|
-
/**
|
|
69
|
+
/** Compilation evidence that runtime-owned scope metadata may retain per artifact path. */
|
|
11
70
|
export interface ArtifactProvenance {
|
|
12
71
|
/** Retained value; absent means unavailable, malformed means fail closed. */
|
|
13
72
|
sourceHash?: unknown;
|
|
73
|
+
sourceFingerprint?: unknown;
|
|
14
74
|
compilerRevision?: unknown;
|
|
15
75
|
compiledAt?: unknown;
|
|
16
76
|
/** Internal marker for an uninterpretable retained entry; every dependent capability fails closed. */
|
|
@@ -25,7 +85,7 @@ export type ArtifactProvenanceRegistry = Record<string, ArtifactProvenance>;
|
|
|
25
85
|
* `hash`, `compiler`, and `compiled_at` remain accepted only as retired embedded
|
|
26
86
|
* provenance input from pre-0.7 state; they are consumed as compatibility evidence
|
|
27
87
|
* and stripped from model projection. New compilations keep semantic fields here and
|
|
28
|
-
* runtime-owned
|
|
88
|
+
* runtime-owned compilation evidence in the scope `meta.json` provenance registry.
|
|
29
89
|
*/
|
|
30
90
|
export type ArtifactMetadata = JsonObject & {
|
|
31
91
|
description: string;
|
|
@@ -40,10 +100,17 @@ export type ArtifactMetadata = JsonObject & {
|
|
|
40
100
|
/** Artifact source paths are the canonical registry keys. */
|
|
41
101
|
export type ArtifactRegistry = JsonObject & Record<string, ArtifactMetadata>;
|
|
42
102
|
|
|
43
|
-
/** Source identity is sufficient for
|
|
103
|
+
/** Source identity is sufficient for compilation decisions; it never contains the source body. */
|
|
104
|
+
export type ArtifactScope = "global" | "cwd" | "session";
|
|
105
|
+
|
|
44
106
|
export interface ArtifactSourceIdentity {
|
|
45
107
|
path: string;
|
|
46
|
-
|
|
108
|
+
/** Exact semantic owner selected from the effective global → CWD → session overlay. */
|
|
109
|
+
scope?: ArtifactScope;
|
|
110
|
+
/** Transitional legacy content identity; generic maintenance uses the filesystem fingerprint instead. */
|
|
111
|
+
hash?: string;
|
|
112
|
+
/** Stable filesystem evidence observed across the successful source read. */
|
|
113
|
+
sourceFingerprint?: ArtifactSourceFingerprint;
|
|
47
114
|
}
|
|
48
115
|
|
|
49
116
|
export type ArtifactInvalidationReason =
|
|
@@ -53,8 +120,8 @@ export type ArtifactInvalidationReason =
|
|
|
53
120
|
| "invalid-metadata"
|
|
54
121
|
| "explicit-refresh";
|
|
55
122
|
|
|
56
|
-
export type
|
|
57
|
-
| { kind: "
|
|
123
|
+
export type ArtifactCompilationNeed =
|
|
124
|
+
| { kind: "current" }
|
|
58
125
|
| { kind: "requires-compilation"; reason: ArtifactInvalidationReason };
|
|
59
126
|
|
|
60
127
|
export interface ArtifactInvalidationRequest extends ArtifactSourceIdentity {
|
|
@@ -64,22 +131,10 @@ export interface ArtifactInvalidationRequest extends ArtifactSourceIdentity {
|
|
|
64
131
|
/** Model-visible invalidation projection: paths and reasons only, never source identity. */
|
|
65
132
|
export interface ArtifactInvalidationNotice {
|
|
66
133
|
path: string;
|
|
134
|
+
scope?: ArtifactScope;
|
|
67
135
|
reason: ArtifactInvalidationReason;
|
|
68
136
|
}
|
|
69
137
|
|
|
70
|
-
export interface ArtifactInvalidationPlan {
|
|
71
|
-
fresh: ArtifactSourceIdentity[];
|
|
72
|
-
requiresCompilation: ArtifactInvalidationRequest[];
|
|
73
|
-
removed: string[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface ArtifactInvalidationOptions {
|
|
77
|
-
/** Refresh every source, or only paths in the supplied set. */
|
|
78
|
-
explicitRefresh?: boolean | ReadonlySet<string>;
|
|
79
|
-
/** Explicit absence evidence supplied by the source owner, not inferred from a partial candidate set. */
|
|
80
|
-
removed?: readonly string[];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
138
|
/** Trusted compilation input; embedded timestamps are accepted here, but never in model patches. */
|
|
84
139
|
export type ArtifactCompilerOutput = JsonObject & {
|
|
85
140
|
description: string;
|
|
@@ -191,9 +246,12 @@ function validateSourceIdentity(source: ArtifactSourceIdentity): void {
|
|
|
191
246
|
if (typeof source.path !== "string" || source.path.trim().length === 0) {
|
|
192
247
|
throw new Error("Artifact source path must be non-empty");
|
|
193
248
|
}
|
|
194
|
-
if (!isArtifactHash(source.hash)) {
|
|
249
|
+
if (source.hash !== undefined && !isArtifactHash(source.hash)) {
|
|
195
250
|
throw new Error(`Artifact source at ${source.path} must have a sha256:<64 lowercase hex characters> hash`);
|
|
196
251
|
}
|
|
252
|
+
if (source.sourceFingerprint !== undefined && parseArtifactSourceFingerprint(source.sourceFingerprint) === undefined) {
|
|
253
|
+
throw new Error(`Artifact source fingerprint at ${source.path} is invalid`);
|
|
254
|
+
}
|
|
197
255
|
}
|
|
198
256
|
|
|
199
257
|
function validateCompilerRevision(compiler: string, path?: string): void {
|
|
@@ -204,12 +262,6 @@ function validateCompilerRevision(compiler: string, path?: string): void {
|
|
|
204
262
|
}
|
|
205
263
|
}
|
|
206
264
|
|
|
207
|
-
function refreshRequested(path: string, explicitRefresh: boolean | ReadonlySet<string> | undefined): boolean {
|
|
208
|
-
if (explicitRefresh === true) return true;
|
|
209
|
-
if (!explicitRefresh || typeof explicitRefresh !== "object" || typeof explicitRefresh.has !== "function") return false;
|
|
210
|
-
return explicitRefresh.has(path);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
265
|
function isProvenanceEntry(value: unknown): value is ArtifactProvenance {
|
|
214
266
|
return isObject(value) && (value.malformed === undefined || value.malformed === true);
|
|
215
267
|
}
|
|
@@ -225,13 +277,14 @@ export function parseArtifactProvenanceRegistry(value: unknown, context = "Artif
|
|
|
225
277
|
registry[path] = { malformed: true };
|
|
226
278
|
continue;
|
|
227
279
|
}
|
|
228
|
-
const known = new Set(["sourceHash", "compilerRevision", "compiledAt"]);
|
|
280
|
+
const known = new Set(["sourceHash", "sourceFingerprint", "compilerRevision", "compiledAt"]);
|
|
229
281
|
if (Object.keys(entry).some((key) => !known.has(key))) {
|
|
230
282
|
registry[path] = { malformed: true };
|
|
231
283
|
continue;
|
|
232
284
|
}
|
|
233
285
|
registry[path] = {
|
|
234
286
|
...(Object.hasOwn(entry, "sourceHash") ? { sourceHash: entry.sourceHash } : {}),
|
|
287
|
+
...(Object.hasOwn(entry, "sourceFingerprint") ? { sourceFingerprint: entry.sourceFingerprint } : {}),
|
|
235
288
|
...(Object.hasOwn(entry, "compilerRevision") ? { compilerRevision: entry.compilerRevision } : {}),
|
|
236
289
|
...(Object.hasOwn(entry, "compiledAt") ? { compiledAt: entry.compiledAt } : {}),
|
|
237
290
|
};
|
|
@@ -246,6 +299,7 @@ export function serializeArtifactProvenanceRegistry(registry: Readonly<ArtifactP
|
|
|
246
299
|
if (entry.malformed === true) continue;
|
|
247
300
|
const fields: JsonObject = {};
|
|
248
301
|
if (entry.sourceHash !== undefined) fields.sourceHash = entry.sourceHash as JsonValue;
|
|
302
|
+
if (entry.sourceFingerprint !== undefined) fields.sourceFingerprint = entry.sourceFingerprint as JsonValue;
|
|
249
303
|
if (entry.compilerRevision !== undefined) fields.compilerRevision = entry.compilerRevision as JsonValue;
|
|
250
304
|
if (entry.compiledAt !== undefined) fields.compiledAt = entry.compiledAt as JsonValue;
|
|
251
305
|
if (Object.keys(fields).length === 0) continue;
|
|
@@ -258,84 +312,50 @@ function invalidField(value: unknown, validate: (candidate: unknown) => boolean)
|
|
|
258
312
|
return value !== undefined && !validate(value);
|
|
259
313
|
}
|
|
260
314
|
|
|
261
|
-
const INVALID_EVIDENCE = Symbol("invalid-evidence");
|
|
262
|
-
|
|
263
315
|
/** Later authority wins per field; absent runtime evidence falls back to retired embedded values. */
|
|
264
316
|
function fieldEvidence(entry: ArtifactProvenance | undefined, field: "sourceHash" | "compilerRevision" | "compiledAt", legacy: unknown): unknown {
|
|
265
|
-
if (entry?.malformed === true) return INVALID_EVIDENCE;
|
|
266
317
|
if (entry !== undefined && Object.hasOwn(entry, field)) return entry[field];
|
|
267
318
|
return legacy;
|
|
268
319
|
}
|
|
269
320
|
|
|
270
|
-
/**
|
|
271
|
-
export function
|
|
321
|
+
/** Decide whether retained compilation evidence requires source reacquisition. */
|
|
322
|
+
export function classifyArtifactCompilationNeed(
|
|
272
323
|
source: ArtifactSourceIdentity,
|
|
273
324
|
metadata: unknown,
|
|
274
325
|
compiler: string,
|
|
275
326
|
explicitRefresh = false,
|
|
276
327
|
provenance?: unknown,
|
|
277
|
-
):
|
|
328
|
+
): ArtifactCompilationNeed {
|
|
278
329
|
validateSourceIdentity(source);
|
|
279
330
|
validateCompilerRevision(compiler);
|
|
280
331
|
if (metadata === undefined) return { kind: "requires-compilation", reason: "new" };
|
|
281
332
|
if (!isArtifactMetadata(metadata)) return { kind: "requires-compilation", reason: "invalid-metadata" };
|
|
282
333
|
const entry = isProvenanceEntry(provenance) ? provenance : undefined;
|
|
283
|
-
|
|
284
|
-
const sourceHash = runtime ? INVALID_EVIDENCE : fieldEvidence(entry, "sourceHash", metadata.hash);
|
|
285
|
-
if (sourceHash === INVALID_EVIDENCE || invalidField(sourceHash, (value) => isArtifactHash(value))) {
|
|
334
|
+
if ((provenance !== undefined && entry === undefined) || entry?.malformed === true) {
|
|
286
335
|
return { kind: "requires-compilation", reason: "invalid-metadata" };
|
|
287
336
|
}
|
|
288
|
-
if (
|
|
289
|
-
|
|
337
|
+
if (source.sourceFingerprint !== undefined) {
|
|
338
|
+
const retained = parseArtifactSourceFingerprint(entry?.sourceFingerprint);
|
|
339
|
+
if (retained === undefined) return { kind: "requires-compilation", reason: "invalid-metadata" };
|
|
340
|
+
if (!sameArtifactSourceFingerprint(source.sourceFingerprint, retained)) return { kind: "requires-compilation", reason: "source-changed" };
|
|
341
|
+
}
|
|
342
|
+
if (source.sourceFingerprint === undefined || source.hash !== undefined) {
|
|
343
|
+
// Explicit current-hash observations, including Skills, retain their identity contract.
|
|
344
|
+
const sourceHash = fieldEvidence(entry, "sourceHash", metadata.hash);
|
|
345
|
+
if (invalidField(sourceHash, isArtifactHash)) return { kind: "requires-compilation", reason: "invalid-metadata" };
|
|
346
|
+
if (source.hash !== undefined && typeof sourceHash === "string" && sourceHash !== source.hash) {
|
|
347
|
+
return { kind: "requires-compilation", reason: "source-changed" };
|
|
348
|
+
}
|
|
290
349
|
}
|
|
291
|
-
const compilerRevision =
|
|
292
|
-
if (
|
|
350
|
+
const compilerRevision = fieldEvidence(entry, "compilerRevision", metadata.compiler);
|
|
351
|
+
if (invalidField(compilerRevision, (value) => typeof value === "string" && value.trim().length > 0)) {
|
|
293
352
|
return { kind: "requires-compilation", reason: "invalid-metadata" };
|
|
294
353
|
}
|
|
295
354
|
if (typeof compilerRevision === "string" && compilerRevision !== compiler) {
|
|
296
355
|
return { kind: "requires-compilation", reason: "compiler-changed" };
|
|
297
356
|
}
|
|
298
357
|
if (explicitRefresh) return { kind: "requires-compilation", reason: "explicit-refresh" };
|
|
299
|
-
return { kind: "
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Produce a deterministic acquisition plan from path/hash candidates and retained evidence. */
|
|
303
|
-
export function planArtifactInvalidation(
|
|
304
|
-
sources: readonly ArtifactSourceIdentity[],
|
|
305
|
-
registry: Readonly<Record<string, unknown>>,
|
|
306
|
-
compiler: string,
|
|
307
|
-
options: ArtifactInvalidationOptions = {},
|
|
308
|
-
provenance: Readonly<ArtifactProvenanceRegistry> = {},
|
|
309
|
-
): ArtifactInvalidationPlan {
|
|
310
|
-
if (!isObject(registry)) throw new Error("Artifacts must be a path-keyed JSON object");
|
|
311
|
-
validateCompilerRevision(compiler);
|
|
312
|
-
const ordered = [...sources].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
313
|
-
const seen = new Set<string>();
|
|
314
|
-
const fresh: ArtifactSourceIdentity[] = [];
|
|
315
|
-
const requiresCompilation: ArtifactInvalidationRequest[] = [];
|
|
316
|
-
for (const source of ordered) {
|
|
317
|
-
if (seen.has(source.path)) throw new Error(`Duplicate artifact source path: ${source.path}`);
|
|
318
|
-
seen.add(source.path);
|
|
319
|
-
const metadata = Object.hasOwn(registry, source.path) ? registry[source.path] : undefined;
|
|
320
|
-
const entry = Object.hasOwn(provenance, source.path) ? provenance[source.path] : undefined;
|
|
321
|
-
const freshness = classifyArtifactFreshness(
|
|
322
|
-
source,
|
|
323
|
-
metadata,
|
|
324
|
-
compiler,
|
|
325
|
-
refreshRequested(source.path, options.explicitRefresh),
|
|
326
|
-
entry,
|
|
327
|
-
);
|
|
328
|
-
const identity = { path: source.path, hash: source.hash };
|
|
329
|
-
if (freshness.kind === "fresh") fresh.push(identity);
|
|
330
|
-
else requiresCompilation.push({ ...identity, reason: freshness.reason });
|
|
331
|
-
}
|
|
332
|
-
if (options.removed !== undefined && !Array.isArray(options.removed)) throw new Error("Removed artifact paths must be an array");
|
|
333
|
-
const removed = [...new Set(options.removed ?? [])];
|
|
334
|
-
for (const path of removed) {
|
|
335
|
-
if (typeof path !== "string" || path.trim().length === 0) throw new Error("Removed artifact paths must be non-empty");
|
|
336
|
-
if (seen.has(path)) throw new Error(`Artifact source cannot be both present and removed: ${path}`);
|
|
337
|
-
}
|
|
338
|
-
return { fresh, requiresCompilation, removed: removed.filter((path) => Object.hasOwn(registry, path)).sort() };
|
|
358
|
+
return { kind: "current" };
|
|
339
359
|
}
|
|
340
360
|
|
|
341
361
|
/** Split one compiler output into model-visible semantics and runtime-owned provenance. */
|
|
@@ -352,7 +372,8 @@ export function compileArtifact(update: ArtifactCompilationUpdate): CompiledArti
|
|
|
352
372
|
return {
|
|
353
373
|
semantic,
|
|
354
374
|
provenance: {
|
|
355
|
-
sourceHash: update.source.hash,
|
|
375
|
+
...(update.source.hash === undefined ? {} : { sourceHash: update.source.hash }),
|
|
376
|
+
...(update.source.sourceFingerprint === undefined ? {} : { sourceFingerprint: structuredClone(update.source.sourceFingerprint) }),
|
|
356
377
|
compilerRevision: update.compiler,
|
|
357
378
|
...(typeof update.output.compiled_at === "string" ? { compiledAt: update.output.compiled_at } : {}),
|
|
358
379
|
},
|
|
@@ -417,14 +438,14 @@ export function updateArtifactRegistry(
|
|
|
417
438
|
}
|
|
418
439
|
|
|
419
440
|
/** Runtime-owned artifact fields that never belong in ordinary model context. */
|
|
420
|
-
const RUNTIME_ARTIFACT_FIELDS = [...MODEL_FORBIDDEN_PROVENANCE_FIELDS, "compiled_at"] as const;
|
|
441
|
+
const RUNTIME_ARTIFACT_FIELDS = [...MODEL_FORBIDDEN_PROVENANCE_FIELDS, "compiled_at", "hint"] as const;
|
|
421
442
|
|
|
422
443
|
/** Validate authored fields only: legacy retained evidence stays readable but cannot be model-edited. */
|
|
423
444
|
export function validateModelArtifactPatch(patch: JsonObject): void {
|
|
424
445
|
for (const [path, entry] of Object.entries(patch)) {
|
|
425
446
|
if (!isObject(entry)) continue; // Whole-artifact deletion and materialized shape belong to the transition owner.
|
|
426
447
|
const field = RUNTIME_ARTIFACT_FIELDS.find((field) => Object.hasOwn(entry, field));
|
|
427
|
-
if (field !== undefined) throw new Error(`Artifact patch at ${path} cannot set runtime-owned
|
|
448
|
+
if (field !== undefined) throw new Error(`Artifact patch at ${path} cannot set runtime-owned field ${field}`);
|
|
428
449
|
}
|
|
429
450
|
}
|
|
430
451
|
|
|
@@ -436,12 +457,19 @@ export function projectArtifactForModel(entry: unknown): unknown {
|
|
|
436
457
|
return projected;
|
|
437
458
|
}
|
|
438
459
|
|
|
439
|
-
|
|
440
|
-
|
|
460
|
+
export type ArtifactModelHints = Readonly<Record<string, string | readonly string[]>>;
|
|
461
|
+
|
|
462
|
+
/** Strip retained runtime bookkeeping and add deterministic runtime-only guidance. */
|
|
463
|
+
export function projectArtifactsForModel(registry: Readonly<ArtifactRegistry>, hints: ArtifactModelHints = {}): ArtifactRegistry {
|
|
441
464
|
const projected: ArtifactRegistry = {};
|
|
442
465
|
for (const [path, entry] of Object.entries(registry)) {
|
|
466
|
+
const modelEntry = projectArtifactForModel(entry);
|
|
467
|
+
const values = (Array.isArray(hints[path]) ? hints[path] : [hints[path]])
|
|
468
|
+
.filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
|
469
|
+
const hint = [...new Set(values)].sort().join("\n");
|
|
470
|
+
if (hint.length > 0 && isObject(modelEntry)) modelEntry.hint = hint;
|
|
443
471
|
Object.defineProperty(projected, path, {
|
|
444
|
-
value:
|
|
472
|
+
value: modelEntry,
|
|
445
473
|
enumerable: true,
|
|
446
474
|
configurable: true,
|
|
447
475
|
writable: true,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { estimateTokens, type AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { CompactionResult } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
|
-
export const STATE_FLOW_COMPACTION_SUMMARY = "State Flow accepted the completed work before this boundary. Current memory is restored from its
|
|
4
|
+
export const STATE_FLOW_COMPACTION_SUMMARY = "State Flow accepted the completed work before this boundary. Current memory is restored from its retained semantic boundary and projected separately; use the retained native entries for subsequent work.";
|
|
5
5
|
/** A modest margin above Pi's default 20k retained suffix absorbs estimation drift. */
|
|
6
6
|
export const STATE_FLOW_COMPACTION_MIN_CONTEXT_TOKENS = 24_000;
|
|
7
7
|
|
|
8
8
|
export interface StateFlowCompactionDetails {
|
|
9
9
|
version: 1;
|
|
10
10
|
owner: "state-flow";
|
|
11
|
-
|
|
11
|
+
boundary: string;
|
|
12
12
|
step: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -22,7 +22,7 @@ type ActiveEntry = {
|
|
|
22
22
|
id?: unknown;
|
|
23
23
|
type?: unknown;
|
|
24
24
|
customType?: unknown;
|
|
25
|
-
message?: { role?: unknown; stopReason?: unknown; content?: unknown };
|
|
25
|
+
message?: { role?: unknown; stopReason?: unknown; content?: unknown; timestamp?: unknown };
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
function stateFlowEntry(entry: ActiveEntry): boolean {
|
|
@@ -55,23 +55,25 @@ export function hasCompactionSizedTranscript(entries: readonly ActiveEntry[]): b
|
|
|
55
55
|
/** Retain the complete latest accepted user iteration without hiding foreign extension context. */
|
|
56
56
|
export function planStateFlowCompaction(
|
|
57
57
|
entries: readonly ActiveEntry[],
|
|
58
|
-
|
|
58
|
+
boundary: string,
|
|
59
59
|
step: number,
|
|
60
|
+
runAnchorTimestamp: number | undefined,
|
|
60
61
|
): StateFlowCompactionPlan | undefined {
|
|
61
|
-
if (
|
|
62
|
+
if (boundary.trim().length === 0 || typeof runAnchorTimestamp !== "number" || !Number.isFinite(runAnchorTimestamp)) return undefined;
|
|
62
63
|
if (!Number.isSafeInteger(step) || step < 0 || entries.length === 0) return undefined;
|
|
63
64
|
const terminal = entries.findLastIndex((entry) => entry.type === "message" && entry.message?.role === "assistant");
|
|
64
65
|
if (terminal < 0 || entries[terminal]?.message?.stopReason === "aborted"
|
|
65
66
|
|| entries[terminal]?.message?.stopReason === "error"
|
|
66
67
|
|| entries[terminal]?.message?.stopReason === "length") return undefined;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
68
|
+
// Steering is part of the same run; an absent or colliding timestamp cannot prove its first entry.
|
|
69
|
+
const isRunAnchor = (entry: ActiveEntry) => entry.type === "message" && entry.message?.role === "user" && entry.message.timestamp === runAnchorTimestamp;
|
|
70
|
+
const keep = entries.findIndex(isRunAnchor);
|
|
71
|
+
if (keep < 0 || keep > terminal || entries.findLastIndex(isRunAnchor) !== keep) return undefined;
|
|
72
|
+
if (entries.slice(0, keep).some((entry) => entry.type === "custom_message" || (entry.type === "custom" && !stateFlowEntry(entry)))) return undefined;
|
|
71
73
|
const firstKeptEntryId = entries[keep]?.id;
|
|
72
74
|
const leafId = entries.at(-1)?.id;
|
|
73
75
|
if (typeof firstKeptEntryId !== "string" || typeof leafId !== "string") return undefined;
|
|
74
|
-
return { leafId, firstKeptEntryId, details: { version: 1, owner: "state-flow",
|
|
76
|
+
return { leafId, firstKeptEntryId, details: { version: 1, owner: "state-flow", boundary, step } };
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** Customize only the extension-owned manual request and only while its planned leaf remains selected. */
|
|
@@ -3,6 +3,7 @@ import { lstatSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { getDurableRepositoryRoot } from "./durable.ts";
|
|
6
|
+
import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT } from "./history.ts";
|
|
6
7
|
import { isObject } from "./json.ts";
|
|
7
8
|
|
|
8
9
|
export interface StateFlowConfig {
|
|
@@ -15,7 +16,7 @@ export interface StateFlowConfig {
|
|
|
15
16
|
logging: boolean;
|
|
16
17
|
/** Show successful patch_state arguments in the interactive tool row. */
|
|
17
18
|
showSuccessfulPatches: boolean;
|
|
18
|
-
|
|
19
|
+
historyLimit: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/** Read the repository-global config once at extension load/reload. Missing config uses defaults; invalid config never falls back. */
|
|
@@ -29,6 +30,7 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
|
|
|
29
30
|
passiveTools: true,
|
|
30
31
|
logging: false,
|
|
31
32
|
showSuccessfulPatches: true,
|
|
33
|
+
historyLimit: DEFAULT_HISTORY_LIMIT,
|
|
32
34
|
};
|
|
33
35
|
let value: unknown;
|
|
34
36
|
try {
|
|
@@ -37,7 +39,7 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
|
|
|
37
39
|
} catch (error) {
|
|
38
40
|
throw new Error(`Cannot read State Flow configuration: ${path}`, { cause: error });
|
|
39
41
|
}
|
|
40
|
-
const allowed = new Set(["autoStart", "passiveBootstrap", "passiveTools", "logging", "showSuccessfulPatches", "
|
|
42
|
+
const allowed = new Set(["autoStart", "passiveBootstrap", "passiveTools", "logging", "showSuccessfulPatches", "historyLimit"]);
|
|
41
43
|
if (!isObject(value) || Object.keys(value).some((key) => !allowed.has(key))) {
|
|
42
44
|
throw new Error(`State Flow configuration contains unknown settings: ${path}`);
|
|
43
45
|
}
|
|
@@ -46,9 +48,7 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
|
|
|
46
48
|
if (Object.hasOwn(value, "passiveTools") && typeof value.passiveTools !== "boolean") throw new Error(`State Flow passiveTools must be a boolean: ${path}`);
|
|
47
49
|
if (Object.hasOwn(value, "logging") && typeof value.logging !== "boolean") throw new Error(`State Flow logging must be a boolean: ${path}`);
|
|
48
50
|
if (Object.hasOwn(value, "showSuccessfulPatches") && typeof value.showSuccessfulPatches !== "boolean") throw new Error(`State Flow showSuccessfulPatches must be a boolean: ${path}`);
|
|
49
|
-
if (Object.hasOwn(value, "
|
|
50
|
-
throw new Error(`State Flow remotePublication must be off, turn-end, or transition: ${path}`);
|
|
51
|
-
}
|
|
51
|
+
if (Object.hasOwn(value, "historyLimit") && (!Number.isSafeInteger(value.historyLimit) || (value.historyLimit as number) < 0 || (value.historyLimit as number) > MAX_HISTORY_LIMIT)) throw new Error(`State Flow historyLimit must be an integer from 0 to ${MAX_HISTORY_LIMIT}: ${path}`);
|
|
52
52
|
return {
|
|
53
53
|
directory,
|
|
54
54
|
autoStart: value.autoStart === true,
|
|
@@ -56,6 +56,6 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
|
|
|
56
56
|
passiveTools: value.passiveTools !== false,
|
|
57
57
|
logging: value.logging === true,
|
|
58
58
|
showSuccessfulPatches: value.showSuccessfulPatches !== false,
|
|
59
|
-
|
|
59
|
+
historyLimit: typeof value.historyLimit === "number" ? value.historyLimit : DEFAULT_HISTORY_LIMIT,
|
|
60
60
|
};
|
|
61
61
|
}
|