@aexol/spectral 0.9.220 → 0.9.222
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/dist/agent/index.d.ts +7 -3
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +35 -8
- package/dist/extensions/aexol-backend-client.d.ts +85 -0
- package/dist/extensions/aexol-backend-client.d.ts.map +1 -0
- package/dist/extensions/aexol-backend-client.js +159 -0
- package/dist/extensions/aexol-mcp.d.ts +10 -6
- package/dist/extensions/aexol-mcp.d.ts.map +1 -1
- package/dist/extensions/aexol-mcp.js +21 -106
- package/dist/extensions/logic-trace-backend-draft.d.ts +68 -0
- package/dist/extensions/logic-trace-backend-draft.d.ts.map +1 -0
- package/dist/extensions/logic-trace-backend-draft.js +171 -0
- package/dist/extensions/logic-trace-validate.d.ts +33 -0
- package/dist/extensions/logic-trace-validate.d.ts.map +1 -0
- package/dist/extensions/logic-trace-validate.js +73 -0
- package/dist/extensions/logic-trace.d.ts +213 -0
- package/dist/extensions/logic-trace.d.ts.map +1 -0
- package/dist/extensions/logic-trace.js +1098 -0
- package/dist/generated/parser/lexer.d.ts +148 -0
- package/dist/generated/parser/lexer.d.ts.map +1 -0
- package/dist/generated/parser/lexer.js +877 -0
- package/dist/generated/parser/mod.d.ts +26 -0
- package/dist/generated/parser/mod.d.ts.map +1 -0
- package/dist/generated/parser/mod.js +30 -0
- package/dist/generated/parser/parser.d.ts +161 -0
- package/dist/generated/parser/parser.d.ts.map +1 -0
- package/dist/generated/parser/parser.js +754 -0
- package/dist/generated/parser/tokenizer.d.ts +107 -0
- package/dist/generated/parser/tokenizer.d.ts.map +1 -0
- package/dist/generated/parser/tokenizer.js +202 -0
- package/dist/generated/parser/validator.d.ts +45 -0
- package/dist/generated/parser/validator.d.ts.map +1 -0
- package/dist/generated/parser/validator.js +202 -0
- package/dist/generated/zeus/const.d.ts.map +1 -1
- package/dist/generated/zeus/const.js +4 -170
- package/dist/generated/zeus/index.d.ts +8 -873
- package/dist/generated/zeus/index.d.ts.map +1 -1
- package/dist/generated/zeus/index.js +0 -7
- package/dist/mcp/ui-stream-types.d.ts +2 -2
- package/dist/memory/tools/share-project-observation.js +1 -1
- package/dist/memory/tools/write-project-observation.d.ts.map +1 -1
- package/dist/memory/tools/write-project-observation.js +4 -3
- package/dist/sdk/agent-core/harness/types.d.ts +0 -1
- package/dist/sdk/agent-core/harness/types.d.ts.map +1 -1
- package/dist/sdk/agent-core/index.d.ts +0 -8
- package/dist/sdk/agent-core/index.d.ts.map +1 -1
- package/dist/sdk/agent-core/index.js +0 -9
- package/dist/sdk/coding-agent/core/extensions/index.d.ts +1 -1
- package/dist/sdk/coding-agent/core/extensions/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +2 -2
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +20 -1
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/tool-definition-wrapper.d.ts +9 -4
- package/dist/sdk/coding-agent/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/tool-definition-wrapper.js +15 -2
- package/dist/sdk/coding-agent/index.d.ts +1 -1
- package/dist/sdk/coding-agent/index.d.ts.map +1 -1
- package/dist/server/agent-bridge/headless-ui.d.ts +20 -0
- package/dist/server/agent-bridge/headless-ui.d.ts.map +1 -0
- package/dist/server/agent-bridge/headless-ui.js +85 -0
- package/dist/server/agent-bridge/pricing.d.ts +37 -0
- package/dist/server/agent-bridge/pricing.d.ts.map +1 -0
- package/dist/server/agent-bridge/pricing.js +202 -0
- package/dist/server/agent-bridge/rehydrate.d.ts +35 -0
- package/dist/server/agent-bridge/rehydrate.d.ts.map +1 -0
- package/dist/server/agent-bridge/rehydrate.js +122 -0
- package/dist/server/agent-bridge/synthetic-model.d.ts +27 -0
- package/dist/server/agent-bridge/synthetic-model.d.ts.map +1 -0
- package/dist/server/agent-bridge/synthetic-model.js +92 -0
- package/dist/server/agent-bridge.d.ts +11 -1
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +42 -481
- package/dist/server/session-stream/compaction-events.d.ts +13 -0
- package/dist/server/session-stream/compaction-events.d.ts.map +1 -0
- package/dist/server/session-stream/compaction-events.js +32 -0
- package/dist/server/session-stream/history-prune.d.ts +35 -0
- package/dist/server/session-stream/history-prune.d.ts.map +1 -0
- package/dist/server/session-stream/history-prune.js +182 -0
- package/dist/server/session-stream/memory.d.ts +48 -0
- package/dist/server/session-stream/memory.d.ts.map +1 -0
- package/dist/server/session-stream/memory.js +136 -0
- package/dist/server/session-stream/replay-tail.d.ts +4 -0
- package/dist/server/session-stream/replay-tail.d.ts.map +1 -0
- package/dist/server/session-stream/replay-tail.js +39 -0
- package/dist/server/session-stream/turn-events.d.ts +33 -0
- package/dist/server/session-stream/turn-events.d.ts.map +1 -0
- package/dist/server/session-stream/turn-events.js +65 -0
- package/dist/server/session-stream/turn-silence.d.ts +8 -0
- package/dist/server/session-stream/turn-silence.d.ts.map +1 -0
- package/dist/server/session-stream/turn-silence.js +72 -0
- package/dist/server/session-stream.d.ts +137 -36
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +18 -509
- package/dist/server/storage/observation-ranking.d.ts +14 -0
- package/dist/server/storage/observation-ranking.d.ts.map +1 -0
- package/dist/server/storage/observation-ranking.js +37 -0
- package/dist/server/storage/preflight.d.ts +20 -0
- package/dist/server/storage/preflight.d.ts.map +1 -0
- package/dist/server/storage/preflight.js +22 -0
- package/dist/server/storage/project-binding.d.ts +5 -0
- package/dist/server/storage/project-binding.d.ts.map +1 -0
- package/dist/server/storage/project-binding.js +32 -0
- package/dist/server/storage/rows.d.ts +104 -0
- package/dist/server/storage/rows.d.ts.map +1 -0
- package/dist/server/storage/rows.js +2 -0
- package/dist/server/storage/schema.d.ts +20 -0
- package/dist/server/storage/schema.d.ts.map +1 -0
- package/dist/server/storage/schema.js +134 -0
- package/dist/server/storage/types.d.ts +66 -0
- package/dist/server/storage/types.d.ts.map +1 -0
- package/dist/server/storage/types.js +1 -0
- package/dist/server/storage.d.ts +7 -94
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +6 -224
- package/dist/server/wire.d.ts +6 -0
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +2 -1
- package/dist/sdk/agent-core/harness/agent-harness.d.ts +0 -92
- package/dist/sdk/agent-core/harness/agent-harness.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/agent-harness.js +0 -899
- package/dist/sdk/agent-core/harness/env/nodejs.d.ts +0 -51
- package/dist/sdk/agent-core/harness/env/nodejs.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/env/nodejs.js +0 -485
- package/dist/sdk/agent-core/harness/prompt-templates.d.ts +0 -48
- package/dist/sdk/agent-core/harness/prompt-templates.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/prompt-templates.js +0 -229
- package/dist/sdk/agent-core/harness/session/jsonl-repo.d.ts +0 -26
- package/dist/sdk/agent-core/harness/session/jsonl-repo.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/session/jsonl-repo.js +0 -100
- package/dist/sdk/agent-core/harness/session/jsonl-storage.d.ts +0 -33
- package/dist/sdk/agent-core/harness/session/jsonl-storage.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/session/jsonl-storage.js +0 -230
- package/dist/sdk/agent-core/harness/session/memory-repo.d.ts +0 -18
- package/dist/sdk/agent-core/harness/session/memory-repo.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/session/memory-repo.js +0 -41
- package/dist/sdk/agent-core/harness/session/memory-storage.d.ts +0 -25
- package/dist/sdk/agent-core/harness/session/memory-storage.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/session/memory-storage.js +0 -113
- package/dist/sdk/agent-core/harness/session/repo-utils.d.ts +0 -11
- package/dist/sdk/agent-core/harness/session/repo-utils.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/session/repo-utils.js +0 -38
- package/dist/sdk/agent-core/harness/skills.d.ts +0 -44
- package/dist/sdk/agent-core/harness/skills.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/skills.js +0 -310
- package/dist/sdk/agent-core/harness/utils/shell-output.d.ts +0 -14
- package/dist/sdk/agent-core/harness/utils/shell-output.d.ts.map +0 -1
- package/dist/sdk/agent-core/harness/utils/shell-output.js +0 -125
- package/dist/sdk/agent-core/node.d.ts +0 -3
- package/dist/sdk/agent-core/node.d.ts.map +0 -1
- package/dist/sdk/agent-core/node.js +0 -2
- package/dist/sdk/agent-core/proxy.d.ts +0 -69
- package/dist/sdk/agent-core/proxy.d.ts.map +0 -1
- package/dist/sdk/agent-core/proxy.js +0 -297
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted-history pruning after compaction: derive the kept branch
|
|
3
|
+
* message signatures, match them against SQLite history, and delete the
|
|
4
|
+
* pruned remainder after a successful compaction_end. Bodies moved
|
|
5
|
+
* verbatim from `session-stream.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import type { BridgeLike } from "../session-stream.js";
|
|
8
|
+
import type { SessionStore } from "../storage.js";
|
|
9
|
+
import type { ServerEvent, WireMessage } from "../wire.js";
|
|
10
|
+
export type CompactionEndPruneSkipReason = "aborted" | "skipped" | "failed" | "missing_token_reduction";
|
|
11
|
+
export declare function getCompactionEndPruneSkipReason(event: Extract<ServerEvent, {
|
|
12
|
+
type: "compaction_end";
|
|
13
|
+
}>): CompactionEndPruneSkipReason | null;
|
|
14
|
+
export type BranchEntryLike = NonNullable<ReturnType<NonNullable<BridgeLike["getSessionBranch"]>>>[number];
|
|
15
|
+
export type PersistedConversationRole = "user" | "assistant";
|
|
16
|
+
export interface KeptBranchMessageSignature {
|
|
17
|
+
role: PersistedConversationRole;
|
|
18
|
+
content: string;
|
|
19
|
+
timestampMs: number | null;
|
|
20
|
+
entryId: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ExistingConversationMessage {
|
|
23
|
+
message: WireMessage;
|
|
24
|
+
conversationIndex: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
27
|
+
export declare function contentAsPersistedText(content: unknown): string | null;
|
|
28
|
+
export declare function branchMessageTimestampMs(entry: BranchEntryLike, message: Record<string, unknown>): number | null;
|
|
29
|
+
export declare function branchMessageSignature(entry: BranchEntryLike): KeptBranchMessageSignature | null;
|
|
30
|
+
export declare function branchMessageRole(entry: BranchEntryLike): unknown;
|
|
31
|
+
export declare function getKeptBranchMessageSignatures(entries: BranchEntryLike[], keptEntryIndex: number): KeptBranchMessageSignature[] | null;
|
|
32
|
+
export declare function selectUniqueCandidate(candidates: ExistingConversationMessage[], kept: KeptBranchMessageSignature): ExistingConversationMessage | null;
|
|
33
|
+
export declare function matchKeptPersistedMessageIds(existing: WireMessage[], keptBranchMessages: KeptBranchMessageSignature[]): Set<string> | null;
|
|
34
|
+
export declare function prunePersistedHistoryAfterCompaction(store: SessionStore, sessionId: string, bridge: BridgeLike): void;
|
|
35
|
+
//# sourceMappingURL=history-prune.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-prune.d.ts","sourceRoot":"","sources":["../../../src/server/session-stream/history-prune.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,MAAM,4BAA4B,GACtC,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,yBAAyB,CAAC;AAM/D,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAC,GACtD,4BAA4B,GAAG,IAAI,CAOrC;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,CACvC,UAAU,CAAC,WAAW,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CACxD,CAAC,MAAM,CAAC,CAAC;AACV,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,WAAW,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAID,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmBtE;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,MAAM,GAAG,IAAI,CASf;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,eAAe,GACrB,0BAA0B,GAAG,IAAI,CAYnC;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAIjE;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,eAAe,EAAE,EAC1B,cAAc,EAAE,MAAM,GACrB,0BAA0B,EAAE,GAAG,IAAI,CAkBrC;AAED,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,2BAA2B,EAAE,EACzC,IAAI,EAAE,0BAA0B,GAC/B,2BAA2B,GAAG,IAAI,CAUpC;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,WAAW,EAAE,EACvB,kBAAkB,EAAE,0BAA0B,EAAE,GAC/C,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CA+BpB;AAED,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,UAAU,GACjB,IAAI,CA4FN"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Persisted-history prune is deliberately stricter than timeline status
|
|
2
|
+
// rendering: only a successful compaction_end proves Pi appended a usable
|
|
3
|
+
// compaction entry. Aborted, skipped, or failed endings may leave the branch
|
|
4
|
+
// unchanged or partially settled, so pruning must skip instead of guessing.
|
|
5
|
+
export function getCompactionEndPruneSkipReason(event) {
|
|
6
|
+
if (event.aborted)
|
|
7
|
+
return "aborted";
|
|
8
|
+
if (event.skipped)
|
|
9
|
+
return "skipped";
|
|
10
|
+
if (event.errorMessage)
|
|
11
|
+
return "failed";
|
|
12
|
+
if (typeof event.tokensRemoved !== "number" || event.tokensRemoved <= 0)
|
|
13
|
+
return "missing_token_reduction";
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const BRANCH_MESSAGE_TIMESTAMP_TOLERANCE_MS = 5_000;
|
|
17
|
+
export function isRecord(value) {
|
|
18
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
export function contentAsPersistedText(content) {
|
|
21
|
+
if (typeof content === "string")
|
|
22
|
+
return content;
|
|
23
|
+
if (!Array.isArray(content))
|
|
24
|
+
return null;
|
|
25
|
+
const textParts = [];
|
|
26
|
+
for (const part of content) {
|
|
27
|
+
if (typeof part === "string") {
|
|
28
|
+
textParts.push(part);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (isRecord(part) &&
|
|
32
|
+
part.type === "text" &&
|
|
33
|
+
typeof part.text === "string") {
|
|
34
|
+
textParts.push(part.text);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return textParts.length > 0 ? textParts.join("") : null;
|
|
38
|
+
}
|
|
39
|
+
export function branchMessageTimestampMs(entry, message) {
|
|
40
|
+
if (typeof message.timestamp === "number" &&
|
|
41
|
+
Number.isFinite(message.timestamp))
|
|
42
|
+
return message.timestamp;
|
|
43
|
+
if (typeof entry.timestamp !== "string")
|
|
44
|
+
return null;
|
|
45
|
+
const parsed = Date.parse(entry.timestamp);
|
|
46
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
47
|
+
}
|
|
48
|
+
export function branchMessageSignature(entry) {
|
|
49
|
+
if (entry.type !== "message" || !isRecord(entry.message))
|
|
50
|
+
return null;
|
|
51
|
+
const role = entry.message.role;
|
|
52
|
+
if (role !== "user" && role !== "assistant")
|
|
53
|
+
return null;
|
|
54
|
+
const content = contentAsPersistedText(entry.message.content);
|
|
55
|
+
if (content === null)
|
|
56
|
+
return null;
|
|
57
|
+
return {
|
|
58
|
+
role,
|
|
59
|
+
content,
|
|
60
|
+
timestampMs: branchMessageTimestampMs(entry, entry.message),
|
|
61
|
+
entryId: entry.id,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function branchMessageRole(entry) {
|
|
65
|
+
return entry.type === "message" && isRecord(entry.message)
|
|
66
|
+
? entry.message.role
|
|
67
|
+
: undefined;
|
|
68
|
+
}
|
|
69
|
+
export function getKeptBranchMessageSignatures(entries, keptEntryIndex) {
|
|
70
|
+
const signatures = [];
|
|
71
|
+
for (let i = keptEntryIndex; i < entries.length; i++) {
|
|
72
|
+
const entry = entries[i];
|
|
73
|
+
if (entry.type !== "message")
|
|
74
|
+
continue;
|
|
75
|
+
const role = branchMessageRole(entry);
|
|
76
|
+
if (role !== "user" && role !== "assistant")
|
|
77
|
+
continue;
|
|
78
|
+
const signature = branchMessageSignature(entry);
|
|
79
|
+
if (!signature) {
|
|
80
|
+
console.log(`[spectral] prune: skipped – reason=unsafe_kept_message, ` +
|
|
81
|
+
`kept branch message ${entry.id} cannot be matched safely`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
signatures.push(signature);
|
|
85
|
+
}
|
|
86
|
+
return signatures;
|
|
87
|
+
}
|
|
88
|
+
export function selectUniqueCandidate(candidates, kept) {
|
|
89
|
+
if (candidates.length === 1)
|
|
90
|
+
return candidates[0];
|
|
91
|
+
if (kept.timestampMs === null)
|
|
92
|
+
return null;
|
|
93
|
+
const keptTimestampMs = kept.timestampMs;
|
|
94
|
+
const timestampMatches = candidates.filter((candidate) => Math.abs(candidate.message.createdAt - keptTimestampMs) <=
|
|
95
|
+
BRANCH_MESSAGE_TIMESTAMP_TOLERANCE_MS);
|
|
96
|
+
return timestampMatches.length === 1 ? timestampMatches[0] : null;
|
|
97
|
+
}
|
|
98
|
+
export function matchKeptPersistedMessageIds(existing, keptBranchMessages) {
|
|
99
|
+
const existingConversationMessages = existing
|
|
100
|
+
.filter((message) => message.role === "user" || message.role === "assistant")
|
|
101
|
+
.map((message, conversationIndex) => ({ message, conversationIndex }));
|
|
102
|
+
const matchedIds = new Set();
|
|
103
|
+
let searchStart = 0;
|
|
104
|
+
for (const kept of keptBranchMessages) {
|
|
105
|
+
const candidates = existingConversationMessages.filter((candidate) => candidate.conversationIndex >= searchStart &&
|
|
106
|
+
!matchedIds.has(candidate.message.id) &&
|
|
107
|
+
candidate.message.role === kept.role &&
|
|
108
|
+
candidate.message.content === kept.content);
|
|
109
|
+
const selected = selectUniqueCandidate(candidates, kept);
|
|
110
|
+
if (!selected) {
|
|
111
|
+
console.log(`[spectral] prune: skipped – reason=ambiguous_kept_mapping, ` +
|
|
112
|
+
`could not stably match kept branch entry ${kept.entryId} ` +
|
|
113
|
+
`(${kept.role}, ${kept.content.length.toLocaleString()} chars) to SQLite history`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
matchedIds.add(selected.message.id);
|
|
117
|
+
searchStart = selected.conversationIndex + 1;
|
|
118
|
+
}
|
|
119
|
+
return matchedIds;
|
|
120
|
+
}
|
|
121
|
+
export function prunePersistedHistoryAfterCompaction(store, sessionId, bridge) {
|
|
122
|
+
if (!bridge.getSessionBranch)
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
const entries = bridge.getSessionBranch();
|
|
126
|
+
// Pi appends a CompactionEntry (type "compaction") at the end whose
|
|
127
|
+
// firstKeptEntryId marks the first branch entry that survives in LLM context.
|
|
128
|
+
// We scan backwards so we find the most recent compaction (there should be
|
|
129
|
+
// exactly one per compaction pass).
|
|
130
|
+
const lastCompaction = [...entries]
|
|
131
|
+
.reverse()
|
|
132
|
+
.find((entry) => entry.type === "compaction");
|
|
133
|
+
const firstKeptEntryId = lastCompaction?.firstKeptEntryId;
|
|
134
|
+
if (!firstKeptEntryId) {
|
|
135
|
+
console.log(`[spectral] prune: skipped – reason=missing_compaction_entry, ` +
|
|
136
|
+
`no compaction entry (or no firstKeptEntryId) ` +
|
|
137
|
+
`in branch for ${sessionId} (${entries.length} entries)`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const keptEntryIndex = entries.findIndex((entry) => entry.id === firstKeptEntryId);
|
|
141
|
+
if (keptEntryIndex === -1) {
|
|
142
|
+
console.log(`[spectral] prune: skipped – reason=missing_kept_entry, ` +
|
|
143
|
+
`firstKeptEntryId "${firstKeptEntryId}" ` +
|
|
144
|
+
`not found in ${entries.length} branch entries for ${sessionId}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const keptBranchMessages = getKeptBranchMessageSignatures(entries, keptEntryIndex);
|
|
148
|
+
if (!keptBranchMessages)
|
|
149
|
+
return;
|
|
150
|
+
const existing = store.getMessages(sessionId);
|
|
151
|
+
if (existing.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
if (keptBranchMessages.length === 0) {
|
|
154
|
+
console.log(`[spectral] prune: skipped – reason=no_kept_conversation_messages, ` +
|
|
155
|
+
`no kept persisted user/assistant messages for ${sessionId}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log(`[spectral] prune: ${sessionId} – source=compaction_end, branch has ${entries.length} entries, ` +
|
|
159
|
+
`kept portion starts at index ${keptEntryIndex} (id=${firstKeptEntryId}), ` +
|
|
160
|
+
`${keptBranchMessages.length} user+assistant entries kept, ` +
|
|
161
|
+
`${existing.length} SQLite messages`);
|
|
162
|
+
const matchedKeptIds = matchKeptPersistedMessageIds(existing, keptBranchMessages);
|
|
163
|
+
if (!matchedKeptIds)
|
|
164
|
+
return;
|
|
165
|
+
const deleteIds = existing
|
|
166
|
+
.filter((message) => (message.role === "user" || message.role === "assistant") &&
|
|
167
|
+
!matchedKeptIds.has(message.id))
|
|
168
|
+
.map((message) => message.id);
|
|
169
|
+
if (deleteIds.length === 0) {
|
|
170
|
+
console.log(`[spectral] prune: skipped – reason=already_pruned, ` +
|
|
171
|
+
`SQLite history already matches compacted branch for ${sessionId}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const pruned = store.deleteMessagesByIdForSession(sessionId, deleteIds);
|
|
175
|
+
console.log(`[spectral] prune: finished – reason=successful_compaction_end, ` +
|
|
176
|
+
`dropped ${pruned} pre-compaction message(s) ` +
|
|
177
|
+
`for ${sessionId}, ${matchedKeptIds.size} user/assistant message(s) kept`);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
console.warn(`[spectral] warn: failed to prune persisted history after compaction for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type CompactionPolicy } from "../../sdk/coding-agent/core/compaction/policy.js";
|
|
2
|
+
import { type Config } from "../../memory/config.js";
|
|
3
|
+
import type { BridgeLike, SessionStream } from "../session-stream.js";
|
|
4
|
+
import type { SessionStore } from "../storage.js";
|
|
5
|
+
import type { ServerEvent, WireDcpLiteStatus } from "../wire.js";
|
|
6
|
+
export declare function persistObservationalMemorySnapshot(store: SessionStore, sessionId: string, bridge: BridgeLike): void;
|
|
7
|
+
export declare function loadCompactionPolicy(cwd: string): CompactionPolicy;
|
|
8
|
+
export type SessionDcpLiteThresholds = WireDcpLiteStatus["thresholds"];
|
|
9
|
+
export interface SessionDcpLiteStatus extends Omit<WireDcpLiteStatus, "thresholds"> {
|
|
10
|
+
thresholds: SessionDcpLiteThresholds;
|
|
11
|
+
}
|
|
12
|
+
export interface SessionMemoryStatus {
|
|
13
|
+
mode: "active" | "passive";
|
|
14
|
+
phase: "idle" | "observing" | "compacting" | "reflecting" | "pruning";
|
|
15
|
+
inFlight: {
|
|
16
|
+
observer: boolean;
|
|
17
|
+
compaction: boolean;
|
|
18
|
+
reflection: boolean;
|
|
19
|
+
pruner: boolean;
|
|
20
|
+
};
|
|
21
|
+
reflections: {
|
|
22
|
+
count: number;
|
|
23
|
+
tokens: number;
|
|
24
|
+
};
|
|
25
|
+
observations: {
|
|
26
|
+
committedCount: number;
|
|
27
|
+
committedTokens: number;
|
|
28
|
+
pendingCount: number;
|
|
29
|
+
pendingTokens: number;
|
|
30
|
+
};
|
|
31
|
+
thresholds: {
|
|
32
|
+
observation: number;
|
|
33
|
+
compaction: number;
|
|
34
|
+
reflection: number;
|
|
35
|
+
};
|
|
36
|
+
progress: {
|
|
37
|
+
sinceObservationBoundTokens: number;
|
|
38
|
+
sinceLastCompactionTokens: number;
|
|
39
|
+
observationPoolTokens: number;
|
|
40
|
+
};
|
|
41
|
+
dcpLite?: SessionDcpLiteStatus;
|
|
42
|
+
}
|
|
43
|
+
export declare function buildDcpLiteThresholds(config: Config, policy: CompactionPolicy): SessionDcpLiteThresholds;
|
|
44
|
+
export declare function contextPercent(usedTokens: number | null, maxTokens: number | null): number | null;
|
|
45
|
+
export declare function applyDcpLiteRuntimeStatus(status: SessionMemoryStatus, stream: SessionStream | undefined): void;
|
|
46
|
+
export declare function dcpLiteEventFromServerEvent(event: ServerEvent): WireDcpLiteStatus["lastEvent"] | null;
|
|
47
|
+
export declare function defaultMemoryStatus(cwd: string): SessionMemoryStatus;
|
|
48
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../src/server/session-stream/memory.ts"],"names":[],"mappings":"AAMA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,kDAAkD,CAAC;AAG1D,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEjE,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,UAAU,GACjB,IAAI,CAmCN;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAMlE;AAED,MAAM,MAAM,wBAAwB,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;AAEvE,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC;IACjF,UAAU,EAAE,wBAAwB,CAAC;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;IACtE,QAAQ,EAAE;QACR,QAAQ,EAAE,OAAO,CAAC;QAClB,UAAU,EAAE,OAAO,CAAC;QACpB,UAAU,EAAE,OAAO,CAAC;QACpB,MAAM,EAAE,OAAO,CAAC;KACjB,CAAC;IACF,WAAW,EAAE;QACX,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,YAAY,EAAE;QACZ,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;QACxB,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,UAAU,EAAE;QACV,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,QAAQ,EAAE;QACR,2BAA2B,EAAE,MAAM,CAAC;QACpC,yBAAyB,EAAE,MAAM,CAAC;QAClC,qBAAqB,EAAE,MAAM,CAAC;KAC/B,CAAC;IACF,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,gBAAgB,GACvB,wBAAwB,CAa1B;AAED,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,MAAM,GAAG,IAAI,CAQf;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,IAAI,CAYN;AAED,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,WAAW,GACjB,iBAAiB,CAAC,WAAW,CAAC,GAAG,IAAI,CAoBvC;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAiCpE"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observational-memory session status / DCP-lite status computation and
|
|
3
|
+
* snapshot persistence helpers. Bodies moved verbatim from
|
|
4
|
+
* `session-stream.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import { SettingsManager } from "../../sdk/coding-agent/index.js";
|
|
7
|
+
import { CONTEXT_LIMIT_RATIO, DEFAULT_COMPACTION_POLICY, } from "../../sdk/coding-agent/core/compaction/policy.js";
|
|
8
|
+
import { getMemoryState, isSourceEntry } from "../../memory/branch.js";
|
|
9
|
+
import { renderSummary } from "../../memory/compaction.js";
|
|
10
|
+
import { loadConfig } from "../../memory/config.js";
|
|
11
|
+
export function persistObservationalMemorySnapshot(store, sessionId, bridge) {
|
|
12
|
+
if (!bridge.getSessionBranch)
|
|
13
|
+
return;
|
|
14
|
+
try {
|
|
15
|
+
const entries = bridge.getSessionBranch();
|
|
16
|
+
const memoryState = getMemoryState(entries);
|
|
17
|
+
const allObservations = [
|
|
18
|
+
...memoryState.committedObs,
|
|
19
|
+
...memoryState.pendingObs,
|
|
20
|
+
];
|
|
21
|
+
const hasMemory = memoryState.reflections.length > 0 || allObservations.length > 0;
|
|
22
|
+
if (!hasMemory) {
|
|
23
|
+
store.deleteSessionMemorySnapshot(sessionId);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const summary = renderSummary(memoryState.reflections, allObservations);
|
|
27
|
+
const details = {
|
|
28
|
+
type: "observational-memory",
|
|
29
|
+
version: 4,
|
|
30
|
+
observations: allObservations,
|
|
31
|
+
reflections: memoryState.reflections,
|
|
32
|
+
};
|
|
33
|
+
store.upsertSessionMemorySnapshot(sessionId, {
|
|
34
|
+
summary,
|
|
35
|
+
details,
|
|
36
|
+
coveredSourceCount: entries.filter((entry) => isSourceEntry(entry))
|
|
37
|
+
.length,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.warn(`[spectral] warn: failed to persist observational memory snapshot for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function loadCompactionPolicy(cwd) {
|
|
45
|
+
try {
|
|
46
|
+
return SettingsManager.create(cwd).getCompactionPolicy();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return DEFAULT_COMPACTION_POLICY;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function buildDcpLiteThresholds(config, policy) {
|
|
53
|
+
return {
|
|
54
|
+
postRunCompactionThresholdTokens: Math.max(config.compactionThresholdTokens, policy.autoOlderHistory.minRawTokens),
|
|
55
|
+
autoContextLimitRatio: CONTEXT_LIMIT_RATIO,
|
|
56
|
+
olderHistoryKeepRecentTokens: policy.agentDriven.olderHistoryKeepRecentTokens,
|
|
57
|
+
olderHistoryMinCompactableTokens: policy.agentDriven.olderHistoryMinCompactableTokens,
|
|
58
|
+
cooldownTurns: policy.agentDriven.cooldownTurns,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function contextPercent(usedTokens, maxTokens) {
|
|
62
|
+
if (typeof usedTokens !== "number" ||
|
|
63
|
+
typeof maxTokens !== "number" ||
|
|
64
|
+
maxTokens <= 0)
|
|
65
|
+
return null;
|
|
66
|
+
return Math.max(0, Math.min(100, (usedTokens / maxTokens) * 100));
|
|
67
|
+
}
|
|
68
|
+
export function applyDcpLiteRuntimeStatus(status, stream) {
|
|
69
|
+
if (!status.dcpLite || !stream)
|
|
70
|
+
return;
|
|
71
|
+
const usage = stream.bridge.getContextUsage?.();
|
|
72
|
+
const usedTokens = usage ? usage.tokens : stream.contextWindowUsed;
|
|
73
|
+
const maxTokens = usage ? usage.contextWindow : stream.contextWindowMax;
|
|
74
|
+
const percent = usage?.percent ?? contextPercent(usedTokens, maxTokens);
|
|
75
|
+
if (usage || usedTokens !== null || maxTokens !== null) {
|
|
76
|
+
status.dcpLite.contextWindow = { usedTokens, maxTokens, percent };
|
|
77
|
+
}
|
|
78
|
+
if (stream.lastDcpEvent) {
|
|
79
|
+
status.dcpLite.lastEvent = stream.lastDcpEvent;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function dcpLiteEventFromServerEvent(event) {
|
|
83
|
+
if (event.type !== "agent_notification" || !event.dcpLiteTelemetry)
|
|
84
|
+
return null;
|
|
85
|
+
const telemetry = event.dcpLiteTelemetry;
|
|
86
|
+
return {
|
|
87
|
+
kind: telemetry.event,
|
|
88
|
+
message: event.message,
|
|
89
|
+
timestamp: telemetry.timestamp,
|
|
90
|
+
source: telemetry.source,
|
|
91
|
+
mode: telemetry.mode,
|
|
92
|
+
scope: telemetry.scope,
|
|
93
|
+
reason: telemetry.reason,
|
|
94
|
+
satisfiedSignals: telemetry.satisfiedSignals,
|
|
95
|
+
rawTokens: telemetry.rawTokens,
|
|
96
|
+
contextTokens: telemetry.contextTokens,
|
|
97
|
+
contextPercent: telemetry.contextPercent,
|
|
98
|
+
tokensBefore: telemetry.tokensBefore,
|
|
99
|
+
tokensAfter: telemetry.tokensAfter,
|
|
100
|
+
tokensRemoved: telemetry.tokensRemoved,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function defaultMemoryStatus(cwd) {
|
|
104
|
+
const config = loadConfig(cwd);
|
|
105
|
+
const policy = loadCompactionPolicy(cwd);
|
|
106
|
+
return {
|
|
107
|
+
mode: config.passive ? "passive" : "active",
|
|
108
|
+
phase: "idle",
|
|
109
|
+
inFlight: {
|
|
110
|
+
observer: false,
|
|
111
|
+
compaction: false,
|
|
112
|
+
reflection: false,
|
|
113
|
+
pruner: false,
|
|
114
|
+
},
|
|
115
|
+
reflections: { count: 0, tokens: 0 },
|
|
116
|
+
observations: {
|
|
117
|
+
committedCount: 0,
|
|
118
|
+
committedTokens: 0,
|
|
119
|
+
pendingCount: 0,
|
|
120
|
+
pendingTokens: 0,
|
|
121
|
+
},
|
|
122
|
+
thresholds: {
|
|
123
|
+
observation: config.observationThresholdTokens,
|
|
124
|
+
compaction: config.compactionThresholdTokens,
|
|
125
|
+
reflection: config.reflectionThresholdTokens,
|
|
126
|
+
},
|
|
127
|
+
progress: {
|
|
128
|
+
sinceObservationBoundTokens: 0,
|
|
129
|
+
sinceLastCompactionTokens: 0,
|
|
130
|
+
observationPoolTokens: 0,
|
|
131
|
+
},
|
|
132
|
+
dcpLite: {
|
|
133
|
+
thresholds: buildDcpLiteThresholds(config, policy),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WireMessage } from "../wire.js";
|
|
2
|
+
export declare function estimateStoredMessageReplayTokens(message: WireMessage): number;
|
|
3
|
+
export declare function selectHistoryForBridge(history: WireMessage[] | undefined, hasMemorySnapshot: boolean): WireMessage[] | undefined;
|
|
4
|
+
//# sourceMappingURL=replay-tail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-tail.d.ts","sourceRoot":"","sources":["../../../src/server/session-stream/replay-tail.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAe9C,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAM9E;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,WAAW,EAAE,GAAG,SAAS,EAClC,iBAAiB,EAAE,OAAO,GACzB,WAAW,EAAE,GAAG,SAAS,CAc3B"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fresh-bridge history replay tail selection used on attach/rehydrate.
|
|
3
|
+
* Bodies moved verbatim from `session-stream.ts`.
|
|
4
|
+
*/
|
|
5
|
+
import { estimateStringTokens } from "../../memory/tokens.js";
|
|
6
|
+
import { estimateImagesTokens } from "../image-attachments.js";
|
|
7
|
+
/**
|
|
8
|
+
* Defensive cap for fresh-bridge history replay when we already have a
|
|
9
|
+
* persisted observational-memory snapshot. spectral's own compaction keeps a
|
|
10
|
+
* recent live tail verbatim and summarizes the older prefix; replaying the
|
|
11
|
+
* entire SQLite transcript after restart defeats that and can explode the
|
|
12
|
+
* next prompt. We therefore rehydrate only an approximate recent tail.
|
|
13
|
+
*
|
|
14
|
+
* Keep this conservative — the tail is rehydrated as raw user/assistant
|
|
15
|
+
* messages PLUS spectral reconstructs tool-result messages from events_jsonl,
|
|
16
|
+
* so the actual context inflation is 3-8× the stored token estimate.
|
|
17
|
+
*/
|
|
18
|
+
const REHYDRATION_TAIL_TOKEN_BUDGET = 20_000;
|
|
19
|
+
export function estimateStoredMessageReplayTokens(message) {
|
|
20
|
+
return (estimateStringTokens(message.content) +
|
|
21
|
+
estimateStringTokens(message.events) +
|
|
22
|
+
estimateImagesTokens(message.images));
|
|
23
|
+
}
|
|
24
|
+
export function selectHistoryForBridge(history, hasMemorySnapshot) {
|
|
25
|
+
if (!history || history.length === 0)
|
|
26
|
+
return history;
|
|
27
|
+
if (!hasMemorySnapshot)
|
|
28
|
+
return history;
|
|
29
|
+
let total = 0;
|
|
30
|
+
let start = history.length - 1;
|
|
31
|
+
for (; start >= 0; start--) {
|
|
32
|
+
const nextTotal = total + estimateStoredMessageReplayTokens(history[start]);
|
|
33
|
+
if (total > 0 && nextTotal > REHYDRATION_TAIL_TOKEN_BUDGET)
|
|
34
|
+
break;
|
|
35
|
+
total = nextTotal;
|
|
36
|
+
}
|
|
37
|
+
const selected = history.slice(Math.max(0, start + 1));
|
|
38
|
+
return selected.length > 0 ? selected : [history[history.length - 1]];
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-flight turn buffering primitives: the turn event model
|
|
3
|
+
* (`BufferedTurnEvent`, `InProgressTurn`), replayability gating, turn
|
|
4
|
+
* snapshots for reconnect replay, and persist-time credit extraction from
|
|
5
|
+
* the events JSONL. Bodies moved verbatim from `session-stream.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import type { InProgressTurnSnapshot, ReplayableTurnEvent, ServerEvent } from "../wire.js";
|
|
8
|
+
/**
|
|
9
|
+
* Extract the `creditsUsed` value from a `token_usage` event embedded in an
|
|
10
|
+
* events JSONL string. Returns `null` if no `token_usage` event is present
|
|
11
|
+
* (e.g. it hasn't arrived yet at persist time — the race condition this
|
|
12
|
+
* column exists to bridge). Best-effort: malformed lines are skipped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractCreditsFromEventsJsonl(eventsJsonl: string): number | null;
|
|
15
|
+
export type BufferedTurnEvent = Exclude<ReplayableTurnEvent, {
|
|
16
|
+
type: "subagent_progress";
|
|
17
|
+
}>;
|
|
18
|
+
export interface InProgressTurn {
|
|
19
|
+
turnId: string;
|
|
20
|
+
startedAt: number;
|
|
21
|
+
events: BufferedTurnEvent[];
|
|
22
|
+
/** Text accumulated from `text_delta` events in this turn. Used by the
|
|
23
|
+
* auto-titler so we don't have to re-read the persisted message. */
|
|
24
|
+
assistantText: string;
|
|
25
|
+
/** Number of leading events already persisted to SQLite. snapshotTurn uses this
|
|
26
|
+
* to avoid sending already-stored events again on reconnect/refresh. */
|
|
27
|
+
persistedEventCount: number;
|
|
28
|
+
cleanupStarted: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function isReplayable(event: ServerEvent): event is BufferedTurnEvent;
|
|
31
|
+
export declare function assistantMessageIdsForTurn(turn: InProgressTurn): Set<string>;
|
|
32
|
+
export declare function snapshotTurn(turn: InProgressTurn, includePersistedEvents?: boolean): InProgressTurnSnapshot;
|
|
33
|
+
//# sourceMappingURL=turn-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-events.d.ts","sourceRoot":"","sources":["../../../src/server/session-stream/turn-events.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EACV,sBAAsB,EACtB,mBAAmB,EACnB,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBhF;AAED,MAAM,MAAM,iBAAiB,GAAG,OAAO,CACrC,mBAAmB,EACnB;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,CAC9B,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B;yEACqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB;4EACwE;IACxE,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,KAAK,IAAI,iBAAiB,CAoB3E;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,cAAc,GAAG,GAAG,CAAC,MAAM,CAAC,CAM5E;AACD,wBAAgB,YAAY,CAC1B,IAAI,EAAE,cAAc,EACpB,sBAAsB,UAAQ,GAC7B,sBAAsB,CAWxB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the `creditsUsed` value from a `token_usage` event embedded in an
|
|
3
|
+
* events JSONL string. Returns `null` if no `token_usage` event is present
|
|
4
|
+
* (e.g. it hasn't arrived yet at persist time — the race condition this
|
|
5
|
+
* column exists to bridge). Best-effort: malformed lines are skipped.
|
|
6
|
+
*/
|
|
7
|
+
export function extractCreditsFromEventsJsonl(eventsJsonl) {
|
|
8
|
+
if (!eventsJsonl)
|
|
9
|
+
return null;
|
|
10
|
+
let creditsUsed = null;
|
|
11
|
+
for (const line of eventsJsonl.split("\n")) {
|
|
12
|
+
if (!line.includes("token_usage"))
|
|
13
|
+
continue;
|
|
14
|
+
try {
|
|
15
|
+
const ev = JSON.parse(line);
|
|
16
|
+
if (ev.type === "token_usage" &&
|
|
17
|
+
typeof ev.usage.creditsUsed === "number") {
|
|
18
|
+
creditsUsed = ev.usage.creditsUsed;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Skip malformed lines — best-effort extraction.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return creditsUsed;
|
|
26
|
+
}
|
|
27
|
+
export function isReplayable(event) {
|
|
28
|
+
return (event.type === "message_start" ||
|
|
29
|
+
event.type === "text_delta" ||
|
|
30
|
+
event.type === "thinking_start" ||
|
|
31
|
+
event.type === "thinking_delta" ||
|
|
32
|
+
event.type === "thinking_end" ||
|
|
33
|
+
event.type === "tool_call" ||
|
|
34
|
+
event.type === "tool_result" ||
|
|
35
|
+
event.type === "message_end" ||
|
|
36
|
+
event.type === "token_usage" ||
|
|
37
|
+
event.type === "error" ||
|
|
38
|
+
event.type === "subagent_start" ||
|
|
39
|
+
// `subagent_progress` carries full accumulated live snapshots (streaming
|
|
40
|
+
// text/thinking/status) and is intentionally live-only; buffering it into
|
|
41
|
+
// currentTurn or batch persistence recreates O(n²) snapshot growth while a
|
|
42
|
+
// long subagent is running. Keep lifecycle/tool/end events replayable.
|
|
43
|
+
event.type === "subagent_end" ||
|
|
44
|
+
event.type === "subagent_tool_progress");
|
|
45
|
+
}
|
|
46
|
+
export function assistantMessageIdsForTurn(turn) {
|
|
47
|
+
const ids = new Set();
|
|
48
|
+
for (const event of turn.events) {
|
|
49
|
+
if (event.type === "message_start")
|
|
50
|
+
ids.add(event.messageId);
|
|
51
|
+
}
|
|
52
|
+
return ids;
|
|
53
|
+
}
|
|
54
|
+
export function snapshotTurn(turn, includePersistedEvents = false) {
|
|
55
|
+
// Defensive copy of the events array so the snapshot can't be mutated by
|
|
56
|
+
// subsequent buffer pushes. Individual event objects are safe to share —
|
|
57
|
+
// they're never mutated after creation.
|
|
58
|
+
return {
|
|
59
|
+
turnId: turn.turnId,
|
|
60
|
+
startedAt: turn.startedAt,
|
|
61
|
+
events: includePersistedEvents
|
|
62
|
+
? [...turn.events]
|
|
63
|
+
: turn.events.slice(turn.persistedEventCount),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-silence safety-net tuning (Fix E): environment-resolved silence
|
|
3
|
+
* timeout and re-arm budget for the stale-`currentTurn` watchdog. Bodies
|
|
4
|
+
* moved verbatim from `session-stream.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveTurnSilenceTimeoutMs(): number;
|
|
7
|
+
export declare function resolveTurnSilenceMaxRearm(): number;
|
|
8
|
+
//# sourceMappingURL=turn-silence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-silence.d.ts","sourceRoot":"","sources":["../../../src/server/session-stream/turn-silence.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoCH,wBAAgB,2BAA2B,IAAI,MAAM,CAcpD;AAED,wBAAgB,0BAA0B,IAAI,MAAM,CAgBnD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-silence safety-net tuning (Fix E): environment-resolved silence
|
|
3
|
+
* timeout and re-arm budget for the stale-`currentTurn` watchdog. Bodies
|
|
4
|
+
* moved verbatim from `session-stream.ts`.
|
|
5
|
+
*/
|
|
6
|
+
// Fix E: safety-net for stale `currentTurn`. When an agent hangs, crashes,
|
|
7
|
+
// or the WS drops during sleep, `agent_end` may never arrive and
|
|
8
|
+
// `stream.currentTurn` stays non-null forever. On reconnect, `session_ready`
|
|
9
|
+
// then reports `currentTurn != null`, so landing shows a phantom "streaming"
|
|
10
|
+
// status. This timer clears the stale turn and emits a synthetic `agent_end`
|
|
11
|
+
// after 30 minutes of silence on streaming-meaningful events.
|
|
12
|
+
//
|
|
13
|
+
// Configurable via `SPECTRAL_TURN_SILENCE_TIMEOUT_MS`:
|
|
14
|
+
// * unset / empty / not-a-number / negative -> 30 minutes (default)
|
|
15
|
+
// * `0` -> DISABLED, the timer is never armed (official opt-out)
|
|
16
|
+
// * a positive fraction (e.g. `0.5`) is rounded UP to 1ms so it can never
|
|
17
|
+
// accidentally disable the safety net
|
|
18
|
+
// * anything larger than Node's max delay is clamped (see below)
|
|
19
|
+
const DEFAULT_TURN_SILENCE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
20
|
+
/**
|
|
21
|
+
* Node emits `TimeoutOverflowWarning` and fires immediately for delays above
|
|
22
|
+
* this value, so anything larger is clamped to it.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_TIMEOUT_DELAY_MS = 2_147_483_647;
|
|
25
|
+
/**
|
|
26
|
+
* Upper bound on how many times the silence timer may be re-armed instead of
|
|
27
|
+
* closing the turn while a subagent is still running. Guards against a
|
|
28
|
+
* counter stuck at >0 forever (e.g. a `subagent_end` that never arrives after
|
|
29
|
+
* a crash) turning the safety net off permanently.
|
|
30
|
+
* `SPECTRAL_TURN_SILENCE_MAX_REARM=0` means unlimited re-arms.
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_TURN_SILENCE_MAX_REARM = 8;
|
|
33
|
+
/**
|
|
34
|
+
* Sanity clamp for `SPECTRAL_TURN_SILENCE_MAX_REARM`. Even with the shortest
|
|
35
|
+
* useful timeout this is days of silence, so a typo like `1e10` cannot keep a
|
|
36
|
+
* hung turn open effectively forever.
|
|
37
|
+
*/
|
|
38
|
+
const MAX_TURN_SILENCE_MAX_REARM = 1000;
|
|
39
|
+
export function resolveTurnSilenceTimeoutMs() {
|
|
40
|
+
const raw = process.env.SPECTRAL_TURN_SILENCE_TIMEOUT_MS;
|
|
41
|
+
if (raw === undefined || raw.trim() === "") {
|
|
42
|
+
return DEFAULT_TURN_SILENCE_TIMEOUT_MS;
|
|
43
|
+
}
|
|
44
|
+
const parsed = Number(raw);
|
|
45
|
+
// `0` is the documented way to switch the safety net off entirely.
|
|
46
|
+
if (parsed === 0)
|
|
47
|
+
return 0;
|
|
48
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
49
|
+
return DEFAULT_TURN_SILENCE_TIMEOUT_MS;
|
|
50
|
+
}
|
|
51
|
+
// `Math.max(1, ...)`: a fractional value such as `0.5` must floor to 1ms,
|
|
52
|
+
// not to 0 — flooring to 0 would silently switch the safety net off.
|
|
53
|
+
return Math.min(Math.max(1, Math.floor(parsed)), MAX_TIMEOUT_DELAY_MS);
|
|
54
|
+
}
|
|
55
|
+
export function resolveTurnSilenceMaxRearm() {
|
|
56
|
+
const raw = process.env.SPECTRAL_TURN_SILENCE_MAX_REARM;
|
|
57
|
+
// Empty / whitespace-only must NOT fall through to `Number(...)`, which
|
|
58
|
+
// coerces `""` to `0` and would therefore mean "unlimited re-arms" —
|
|
59
|
+
// i.e. the original "phantom streaming forever" bug.
|
|
60
|
+
if (raw === undefined || raw.trim() === "") {
|
|
61
|
+
return DEFAULT_TURN_SILENCE_MAX_REARM;
|
|
62
|
+
}
|
|
63
|
+
const parsed = Number(raw);
|
|
64
|
+
// `0` is the documented way to allow unlimited re-arms (opt-out of the
|
|
65
|
+
// re-arm budget).
|
|
66
|
+
if (parsed === 0)
|
|
67
|
+
return 0;
|
|
68
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
69
|
+
return DEFAULT_TURN_SILENCE_MAX_REARM;
|
|
70
|
+
}
|
|
71
|
+
return Math.min(Math.max(1, Math.floor(parsed)), MAX_TURN_SILENCE_MAX_REARM);
|
|
72
|
+
}
|