praxis-agent 0.20.22 → 0.21.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/README.md +11 -8
- package/dist/application/background-agent-manager.js +15 -10
- package/dist/application/session-memory.d.ts +96 -0
- package/dist/application/session-memory.js +383 -0
- package/dist/application/session-service.d.ts +18 -0
- package/dist/application/session-service.js +325 -65
- package/dist/application/subagent-service.d.ts +2 -1
- package/dist/application/subagent-service.js +147 -51
- package/dist/cli/tui/claude-style.js +5 -2
- package/dist/cli-runtime.js +22 -18
- package/dist/compatibility/claude/schema.d.ts +7 -0
- package/dist/compatibility/claude/schema.js +22 -10
- package/dist/compatibility/claude/sidechain.d.ts +5 -1
- package/dist/compatibility/claude/sidechain.js +15 -2
- package/dist/core/context-budget.d.ts +53 -2
- package/dist/core/context-budget.js +117 -5
- package/dist/tools/claude-capabilities.d.ts +79 -0
- package/dist/tools/claude-capabilities.js +187 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -170,14 +170,17 @@ Praxis ──────┘
|
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
Praxis can resume Claude Code sessions, and Claude Code can resume compatible
|
|
173
|
-
sessions written by Praxis.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
173
|
+
sessions written by Praxis. Every semver-like Claude Code producer version is
|
|
174
|
+
structurally validated and is read/write compatible when its entry shape is
|
|
175
|
+
supported; schema adapters are selected from transcript entry structure rather
|
|
176
|
+
than an installed or fixed producer version, and malformed or unsupported
|
|
177
|
+
shapes fail closed before any write. Each transcript record keeps its original
|
|
178
|
+
producer version, so supported shapes may be mixed across versions in one
|
|
179
|
+
session. Native fork creation is a separate restricted lossless copy path that
|
|
180
|
+
preserves each existing source record's producer version, so it can copy
|
|
181
|
+
specific black-box-verified foreign shapes such as the observed Claude Code
|
|
182
|
+
2.1.233 records; unsupported record shapes and unverified versions still fail
|
|
183
|
+
closed and remain read-only. Maintainers can prove mixed-version Claude JSONL interoperability with
|
|
181
184
|
`npm run test:cross-version-session-compat`, `test:cross-version-fork-compat`,
|
|
182
185
|
`test:cross-version-sidechain-compat`, `test:cross-version-compaction-compat`,
|
|
183
186
|
and `test:cross-version-resume-at-compat`, covering linear resume, native fork,
|
|
@@ -233,15 +233,21 @@ export class BackgroundAgentManager {
|
|
|
233
233
|
const task = this.tasks.get(agentId);
|
|
234
234
|
if (!task)
|
|
235
235
|
throw new Error(`No task found with ID: ${agentId}`);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
await waitBounded(task.promise, options.timeout);
|
|
242
|
-
}
|
|
236
|
+
// A positive block wait settles a pending task chain — a stopped task's
|
|
237
|
+
// cleanup chain included — or lets the timeout elapse. Zero-timeout and
|
|
238
|
+
// non-blocking retrievals never wait.
|
|
239
|
+
if (options.block && options.timeout > 0 && task.promise) {
|
|
240
|
+
await waitBounded(task.promise, options.timeout);
|
|
243
241
|
}
|
|
244
|
-
|
|
242
|
+
// The retrieval outcome reflects the task's state after any wait: success
|
|
243
|
+
// once terminal, not_ready for non-blocking retrievals, and timeout for a
|
|
244
|
+
// blocking retrieval whose window (including zero) closed while live.
|
|
245
|
+
const retrieval = task.status !== 'running'
|
|
246
|
+
? 'success'
|
|
247
|
+
: options.block
|
|
248
|
+
? 'timeout'
|
|
249
|
+
: 'not_ready';
|
|
250
|
+
return this.formatOutput(task, retrieval);
|
|
245
251
|
}
|
|
246
252
|
stop(agentId) {
|
|
247
253
|
agentId = this.resolveRequired(agentId);
|
|
@@ -469,9 +475,8 @@ export class BackgroundAgentManager {
|
|
|
469
475
|
durationMs: task.durationMs,
|
|
470
476
|
};
|
|
471
477
|
}
|
|
472
|
-
formatOutput(task) {
|
|
478
|
+
formatOutput(task, retrieval) {
|
|
473
479
|
const output = task.result?.text ?? task.error ?? '';
|
|
474
|
-
const retrieval = task.status === 'running' ? 'not_ready' : 'success';
|
|
475
480
|
return [
|
|
476
481
|
`<retrieval_status>${retrieval}</retrieval_status>`,
|
|
477
482
|
`<task_id>${task.spec.agentId}</task_id>`,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ModelMessage } from '../core/runtime.js';
|
|
2
|
+
/** Versioned, explicit progress state for one session's extracted memory. */
|
|
3
|
+
export interface SessionMemoryState {
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
initialized: boolean;
|
|
6
|
+
lastObservedTokens: number;
|
|
7
|
+
lastObservedToolCalls: number;
|
|
8
|
+
lastSummarizedMessageId: string | null;
|
|
9
|
+
extractionStartedAt: number | null;
|
|
10
|
+
extractionCompletedAt: number | null;
|
|
11
|
+
extractionError: string | null;
|
|
12
|
+
}
|
|
13
|
+
export declare class SessionMemoryStateError extends Error {
|
|
14
|
+
readonly name = "SessionMemoryStateError";
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class SessionMemoryTimeoutError extends Error {
|
|
18
|
+
readonly name = "SessionMemoryTimeoutError";
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
export declare function createFreshSessionMemoryState(): SessionMemoryState;
|
|
22
|
+
export interface SessionMemoryStoreOptions {
|
|
23
|
+
configRoot: string;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Durable sidecar for one session's extracted memory under
|
|
28
|
+
* `<configRoot>/praxis/session-memory/<sessionId>/`. All writes are atomic
|
|
29
|
+
* (same-directory temp file, fsync, then rename) and version-checked. Never
|
|
30
|
+
* touches shared Claude transcript entries.
|
|
31
|
+
*/
|
|
32
|
+
export declare class SessionMemoryStore {
|
|
33
|
+
private readonly directory;
|
|
34
|
+
private readonly stateFile;
|
|
35
|
+
private readonly summaryFile;
|
|
36
|
+
constructor(options: SessionMemoryStoreOptions);
|
|
37
|
+
load(): Promise<SessionMemoryState>;
|
|
38
|
+
loadSummary(): Promise<string>;
|
|
39
|
+
writeSummary(summary: string): Promise<void>;
|
|
40
|
+
writeState(state: SessionMemoryState): Promise<void>;
|
|
41
|
+
clear(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
export interface SessionMemoryExtractorInput {
|
|
44
|
+
summary: string;
|
|
45
|
+
tokens: number;
|
|
46
|
+
toolCalls: number;
|
|
47
|
+
messages?: readonly ModelMessage[];
|
|
48
|
+
}
|
|
49
|
+
export type SessionMemoryExtractor = (input: SessionMemoryExtractorInput) => Promise<string> | string;
|
|
50
|
+
export interface SessionMemoryControllerOptions {
|
|
51
|
+
store: SessionMemoryStore;
|
|
52
|
+
extractor: SessionMemoryExtractor;
|
|
53
|
+
initTokens?: number;
|
|
54
|
+
updateTokens?: number;
|
|
55
|
+
updateToolCalls?: number;
|
|
56
|
+
waitTimeoutMs?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Serialized extraction lifecycle for one session. Concurrent callers of
|
|
60
|
+
* `observe` share one in-flight extraction promise, so two extractors never
|
|
61
|
+
* run for the same session. A persisted extraction that never completed is
|
|
62
|
+
* marked failed on reopen and can be retried.
|
|
63
|
+
*/
|
|
64
|
+
export declare class SessionMemoryController {
|
|
65
|
+
private readonly options;
|
|
66
|
+
private readonly initTokens;
|
|
67
|
+
private readonly updateTokens;
|
|
68
|
+
private readonly updateToolCalls;
|
|
69
|
+
private readonly waitTimeoutMs;
|
|
70
|
+
private stateValue;
|
|
71
|
+
private summaryValue;
|
|
72
|
+
private inFlight;
|
|
73
|
+
private loading;
|
|
74
|
+
private observedTokens;
|
|
75
|
+
private observedToolCalls;
|
|
76
|
+
constructor(options: SessionMemoryControllerOptions);
|
|
77
|
+
observe(tokens: number, toolCalls: number, messageId: string, messages?: readonly ModelMessage[]): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Adds non-negative deltas to the current cumulative observed totals and
|
|
80
|
+
* delegates to the serialized extraction path with those cumulative totals.
|
|
81
|
+
* The persisted counters only advance once an extraction succeeds.
|
|
82
|
+
*/
|
|
83
|
+
observeDelta(inputTokens: number, toolCalls: number, messageId: string, messages?: readonly ModelMessage[]): Promise<boolean>;
|
|
84
|
+
/** Safe snapshot of the loaded durable summary; empty when none exists. */
|
|
85
|
+
summary(): Promise<string>;
|
|
86
|
+
/** Safe snapshot of the loaded session memory state. */
|
|
87
|
+
state(): Promise<SessionMemoryState>;
|
|
88
|
+
/** Resolves when no extraction is running; rejects on failure or timeout. */
|
|
89
|
+
waitForIdle(): Promise<void>;
|
|
90
|
+
clear(): Promise<void>;
|
|
91
|
+
private ensureLoaded;
|
|
92
|
+
private loadState;
|
|
93
|
+
private isExtractionDue;
|
|
94
|
+
private runExtraction;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=session-memory.d.ts.map
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { isClaudeSessionId } from '../compatibility/claude/paths.js';
|
|
4
|
+
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
5
|
+
export class SessionMemoryStateError extends Error {
|
|
6
|
+
name = 'SessionMemoryStateError';
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class SessionMemoryTimeoutError extends Error {
|
|
12
|
+
name = 'SessionMemoryTimeoutError';
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function createFreshSessionMemoryState() {
|
|
18
|
+
return {
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
initialized: false,
|
|
21
|
+
lastObservedTokens: 0,
|
|
22
|
+
lastObservedToolCalls: 0,
|
|
23
|
+
lastSummarizedMessageId: null,
|
|
24
|
+
extractionStartedAt: null,
|
|
25
|
+
extractionCompletedAt: null,
|
|
26
|
+
extractionError: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function isNonNegativeSafeInteger(value) {
|
|
30
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
31
|
+
}
|
|
32
|
+
function isNullableTimestamp(value) {
|
|
33
|
+
return (value === null ||
|
|
34
|
+
(typeof value === 'number' && Number.isFinite(value) && value >= 0));
|
|
35
|
+
}
|
|
36
|
+
function assertValidSessionMemoryState(value) {
|
|
37
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
38
|
+
throw new SessionMemoryStateError('Session memory state must be an object');
|
|
39
|
+
}
|
|
40
|
+
const record = value;
|
|
41
|
+
if (record.schemaVersion !== 1) {
|
|
42
|
+
throw new SessionMemoryStateError(`Unsupported session memory schema version: ${String(record.schemaVersion)}`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof record.initialized !== 'boolean') {
|
|
45
|
+
throw new SessionMemoryStateError('Session memory state initialized must be a boolean');
|
|
46
|
+
}
|
|
47
|
+
if (!isNonNegativeSafeInteger(record.lastObservedTokens)) {
|
|
48
|
+
throw new SessionMemoryStateError('Session memory state lastObservedTokens must be a non-negative safe integer');
|
|
49
|
+
}
|
|
50
|
+
if (!isNonNegativeSafeInteger(record.lastObservedToolCalls)) {
|
|
51
|
+
throw new SessionMemoryStateError('Session memory state lastObservedToolCalls must be a non-negative safe integer');
|
|
52
|
+
}
|
|
53
|
+
if (record.lastSummarizedMessageId !== null &&
|
|
54
|
+
typeof record.lastSummarizedMessageId !== 'string') {
|
|
55
|
+
throw new SessionMemoryStateError('Session memory state lastSummarizedMessageId must be a string or null');
|
|
56
|
+
}
|
|
57
|
+
if (!isNullableTimestamp(record.extractionStartedAt)) {
|
|
58
|
+
throw new SessionMemoryStateError('Session memory state extractionStartedAt must be a non-negative timestamp or null');
|
|
59
|
+
}
|
|
60
|
+
if (!isNullableTimestamp(record.extractionCompletedAt)) {
|
|
61
|
+
throw new SessionMemoryStateError('Session memory state extractionCompletedAt must be a non-negative timestamp or null');
|
|
62
|
+
}
|
|
63
|
+
if (record.extractionError !== null &&
|
|
64
|
+
typeof record.extractionError !== 'string') {
|
|
65
|
+
throw new SessionMemoryStateError('Session memory state extractionError must be a string or null');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function parseSessionMemoryState(source) {
|
|
69
|
+
let value;
|
|
70
|
+
try {
|
|
71
|
+
value = JSON.parse(source);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new SessionMemoryStateError('Session memory state is not valid JSON');
|
|
75
|
+
}
|
|
76
|
+
assertValidSessionMemoryState(value);
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Durable sidecar for one session's extracted memory under
|
|
81
|
+
* `<configRoot>/praxis/session-memory/<sessionId>/`. All writes are atomic
|
|
82
|
+
* (same-directory temp file, fsync, then rename) and version-checked. Never
|
|
83
|
+
* touches shared Claude transcript entries.
|
|
84
|
+
*/
|
|
85
|
+
export class SessionMemoryStore {
|
|
86
|
+
directory;
|
|
87
|
+
stateFile;
|
|
88
|
+
summaryFile;
|
|
89
|
+
constructor(options) {
|
|
90
|
+
if (typeof options.configRoot !== 'string' ||
|
|
91
|
+
options.configRoot.length === 0) {
|
|
92
|
+
throw new SessionMemoryStateError('Session memory configRoot must be a non-empty string');
|
|
93
|
+
}
|
|
94
|
+
if (!isClaudeSessionId(options.sessionId)) {
|
|
95
|
+
throw new SessionMemoryStateError(`Invalid session memory session ID: ${options.sessionId}`);
|
|
96
|
+
}
|
|
97
|
+
this.directory = resolve(options.configRoot, 'praxis', 'session-memory', options.sessionId);
|
|
98
|
+
this.stateFile = join(this.directory, 'state.json');
|
|
99
|
+
this.summaryFile = join(this.directory, 'summary.md');
|
|
100
|
+
}
|
|
101
|
+
async load() {
|
|
102
|
+
let source;
|
|
103
|
+
try {
|
|
104
|
+
source = await readFile(this.stateFile, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error.code === 'ENOENT') {
|
|
108
|
+
return createFreshSessionMemoryState();
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
return parseSessionMemoryState(source);
|
|
113
|
+
}
|
|
114
|
+
async loadSummary() {
|
|
115
|
+
let source;
|
|
116
|
+
try {
|
|
117
|
+
source = await readFile(this.summaryFile, 'utf8');
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error.code === 'ENOENT')
|
|
121
|
+
return '';
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
return source;
|
|
125
|
+
}
|
|
126
|
+
async writeSummary(summary) {
|
|
127
|
+
if (typeof summary !== 'string' || summary.trim().length === 0) {
|
|
128
|
+
throw new SessionMemoryStateError('Session memory summary must be a non-empty string');
|
|
129
|
+
}
|
|
130
|
+
await writeFileAtomically(this.summaryFile, summary);
|
|
131
|
+
}
|
|
132
|
+
async writeState(state) {
|
|
133
|
+
assertValidSessionMemoryState(state);
|
|
134
|
+
const existing = await this.load();
|
|
135
|
+
if (state.lastObservedTokens < existing.lastObservedTokens ||
|
|
136
|
+
state.lastObservedToolCalls < existing.lastObservedToolCalls) {
|
|
137
|
+
throw new SessionMemoryStateError('Session memory observed counters must be monotonic');
|
|
138
|
+
}
|
|
139
|
+
await writeFileAtomically(this.stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
140
|
+
}
|
|
141
|
+
async clear() {
|
|
142
|
+
await Promise.all([
|
|
143
|
+
rm(this.stateFile, { force: true }),
|
|
144
|
+
rm(this.summaryFile, { force: true }),
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Serialized extraction lifecycle for one session. Concurrent callers of
|
|
150
|
+
* `observe` share one in-flight extraction promise, so two extractors never
|
|
151
|
+
* run for the same session. A persisted extraction that never completed is
|
|
152
|
+
* marked failed on reopen and can be retried.
|
|
153
|
+
*/
|
|
154
|
+
export class SessionMemoryController {
|
|
155
|
+
options;
|
|
156
|
+
initTokens;
|
|
157
|
+
updateTokens;
|
|
158
|
+
updateToolCalls;
|
|
159
|
+
waitTimeoutMs;
|
|
160
|
+
stateValue = null;
|
|
161
|
+
summaryValue = '';
|
|
162
|
+
inFlight = null;
|
|
163
|
+
loading = null;
|
|
164
|
+
observedTokens = 0;
|
|
165
|
+
observedToolCalls = 0;
|
|
166
|
+
constructor(options) {
|
|
167
|
+
this.options = options;
|
|
168
|
+
this.initTokens = options.initTokens ?? 10_000;
|
|
169
|
+
this.updateTokens = options.updateTokens ?? 5_000;
|
|
170
|
+
this.updateToolCalls = options.updateToolCalls ?? 20;
|
|
171
|
+
this.waitTimeoutMs = options.waitTimeoutMs ?? 30_000;
|
|
172
|
+
for (const [name, value] of [
|
|
173
|
+
['initTokens', this.initTokens],
|
|
174
|
+
['updateTokens', this.updateTokens],
|
|
175
|
+
['updateToolCalls', this.updateToolCalls],
|
|
176
|
+
]) {
|
|
177
|
+
if (!isNonNegativeSafeInteger(value) || value === 0) {
|
|
178
|
+
throw new SessionMemoryStateError(`Session memory ${name} must be a positive safe integer`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (typeof this.waitTimeoutMs !== 'number' ||
|
|
182
|
+
!Number.isFinite(this.waitTimeoutMs) ||
|
|
183
|
+
this.waitTimeoutMs <= 0) {
|
|
184
|
+
throw new SessionMemoryStateError('Session memory waitTimeoutMs must be a positive number');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async observe(tokens, toolCalls, messageId, messages) {
|
|
188
|
+
await this.ensureLoaded();
|
|
189
|
+
const state = this.stateValue;
|
|
190
|
+
if (state === null) {
|
|
191
|
+
throw new SessionMemoryStateError('Session memory state is unavailable');
|
|
192
|
+
}
|
|
193
|
+
if (!isNonNegativeSafeInteger(tokens) ||
|
|
194
|
+
!isNonNegativeSafeInteger(toolCalls)) {
|
|
195
|
+
throw new SessionMemoryStateError('Session memory observed counters must be non-negative safe integers');
|
|
196
|
+
}
|
|
197
|
+
if (typeof messageId !== 'string' || messageId.length === 0) {
|
|
198
|
+
throw new SessionMemoryStateError('Session memory message ID must be a non-empty string');
|
|
199
|
+
}
|
|
200
|
+
if (tokens < state.lastObservedTokens ||
|
|
201
|
+
toolCalls < state.lastObservedToolCalls) {
|
|
202
|
+
throw new SessionMemoryStateError(`Session memory observed counters regressed (tokens ${tokens} < ${state.lastObservedTokens}, toolCalls ${toolCalls} < ${state.lastObservedToolCalls})`);
|
|
203
|
+
}
|
|
204
|
+
this.observedTokens = Math.max(this.observedTokens, tokens);
|
|
205
|
+
this.observedToolCalls = Math.max(this.observedToolCalls, toolCalls);
|
|
206
|
+
if (!this.isExtractionDue(this.observedTokens, this.observedToolCalls)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (this.inFlight === null) {
|
|
210
|
+
const extraction = this.runExtraction(this.observedTokens, this.observedToolCalls, messageId, messages);
|
|
211
|
+
this.inFlight = extraction;
|
|
212
|
+
extraction
|
|
213
|
+
.catch(() => undefined)
|
|
214
|
+
.finally(() => {
|
|
215
|
+
if (this.inFlight === extraction)
|
|
216
|
+
this.inFlight = null;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Adds non-negative deltas to the current cumulative observed totals and
|
|
223
|
+
* delegates to the serialized extraction path with those cumulative totals.
|
|
224
|
+
* The persisted counters only advance once an extraction succeeds.
|
|
225
|
+
*/
|
|
226
|
+
async observeDelta(inputTokens, toolCalls, messageId, messages) {
|
|
227
|
+
await this.ensureLoaded();
|
|
228
|
+
const state = this.stateValue;
|
|
229
|
+
if (state === null) {
|
|
230
|
+
throw new SessionMemoryStateError('Session memory state is unavailable');
|
|
231
|
+
}
|
|
232
|
+
if (!isNonNegativeSafeInteger(inputTokens) ||
|
|
233
|
+
!isNonNegativeSafeInteger(toolCalls)) {
|
|
234
|
+
throw new SessionMemoryStateError('Session memory observed deltas must be non-negative safe integers');
|
|
235
|
+
}
|
|
236
|
+
if (typeof messageId !== 'string' || messageId.length === 0) {
|
|
237
|
+
throw new SessionMemoryStateError('Session memory message ID must be a non-empty string');
|
|
238
|
+
}
|
|
239
|
+
this.observedTokens += inputTokens;
|
|
240
|
+
this.observedToolCalls += toolCalls;
|
|
241
|
+
return this.observe(this.observedTokens, this.observedToolCalls, messageId, messages);
|
|
242
|
+
}
|
|
243
|
+
/** Safe snapshot of the loaded durable summary; empty when none exists. */
|
|
244
|
+
async summary() {
|
|
245
|
+
await this.ensureLoaded();
|
|
246
|
+
return this.summaryValue;
|
|
247
|
+
}
|
|
248
|
+
/** Safe snapshot of the loaded session memory state. */
|
|
249
|
+
async state() {
|
|
250
|
+
await this.ensureLoaded();
|
|
251
|
+
if (this.stateValue === null) {
|
|
252
|
+
throw new SessionMemoryStateError('Session memory state is unavailable');
|
|
253
|
+
}
|
|
254
|
+
return { ...this.stateValue };
|
|
255
|
+
}
|
|
256
|
+
/** Resolves when no extraction is running; rejects on failure or timeout. */
|
|
257
|
+
async waitForIdle() {
|
|
258
|
+
await this.ensureLoaded();
|
|
259
|
+
const extraction = this.inFlight;
|
|
260
|
+
if (extraction === null)
|
|
261
|
+
return;
|
|
262
|
+
let timer;
|
|
263
|
+
const timeout = new Promise((_, reject) => {
|
|
264
|
+
timer = setTimeout(() => reject(new SessionMemoryTimeoutError(`Session memory extraction did not complete within ${this.waitTimeoutMs}ms`)), this.waitTimeoutMs);
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
await Promise.race([extraction, timeout]);
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
if (timer !== undefined)
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async clear() {
|
|
275
|
+
await this.ensureLoaded();
|
|
276
|
+
if (this.inFlight !== null) {
|
|
277
|
+
throw new SessionMemoryStateError('Session memory cannot be cleared while extraction is in progress');
|
|
278
|
+
}
|
|
279
|
+
await this.options.store.clear();
|
|
280
|
+
const fresh = createFreshSessionMemoryState();
|
|
281
|
+
this.stateValue = fresh;
|
|
282
|
+
this.observedTokens = fresh.lastObservedTokens;
|
|
283
|
+
this.observedToolCalls = fresh.lastObservedToolCalls;
|
|
284
|
+
this.summaryValue = '';
|
|
285
|
+
}
|
|
286
|
+
ensureLoaded() {
|
|
287
|
+
if (this.loading !== null)
|
|
288
|
+
return this.loading;
|
|
289
|
+
this.loading = this.loadState().catch((error) => {
|
|
290
|
+
this.loading = null;
|
|
291
|
+
throw error;
|
|
292
|
+
});
|
|
293
|
+
return this.loading;
|
|
294
|
+
}
|
|
295
|
+
async loadState() {
|
|
296
|
+
const [state, summary] = await Promise.all([
|
|
297
|
+
this.options.store.load(),
|
|
298
|
+
this.options.store.loadSummary(),
|
|
299
|
+
]);
|
|
300
|
+
if (state.extractionStartedAt !== null &&
|
|
301
|
+
state.extractionCompletedAt === null) {
|
|
302
|
+
const elapsed = Date.now() - state.extractionStartedAt;
|
|
303
|
+
const recovered = {
|
|
304
|
+
...state,
|
|
305
|
+
extractionStartedAt: null,
|
|
306
|
+
extractionCompletedAt: null,
|
|
307
|
+
extractionError: elapsed >= this.waitTimeoutMs
|
|
308
|
+
? `Session memory extraction is stale after ${elapsed}ms`
|
|
309
|
+
: 'Session memory extraction was interrupted',
|
|
310
|
+
};
|
|
311
|
+
await this.options.store.writeState(recovered);
|
|
312
|
+
this.stateValue = recovered;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
this.stateValue = state;
|
|
316
|
+
}
|
|
317
|
+
this.observedTokens = this.stateValue.lastObservedTokens;
|
|
318
|
+
this.observedToolCalls = this.stateValue.lastObservedToolCalls;
|
|
319
|
+
this.summaryValue = summary;
|
|
320
|
+
}
|
|
321
|
+
isExtractionDue(tokens, toolCalls) {
|
|
322
|
+
const state = this.stateValue;
|
|
323
|
+
if (state === null)
|
|
324
|
+
return false;
|
|
325
|
+
if (!state.initialized)
|
|
326
|
+
return tokens >= this.initTokens;
|
|
327
|
+
return (tokens - state.lastObservedTokens >= this.updateTokens ||
|
|
328
|
+
toolCalls - state.lastObservedToolCalls >= this.updateToolCalls);
|
|
329
|
+
}
|
|
330
|
+
async runExtraction(tokens, toolCalls, messageId, messages) {
|
|
331
|
+
const state = this.stateValue;
|
|
332
|
+
if (state === null) {
|
|
333
|
+
throw new SessionMemoryStateError('Session memory state is unavailable');
|
|
334
|
+
}
|
|
335
|
+
this.stateValue = {
|
|
336
|
+
...state,
|
|
337
|
+
extractionStartedAt: Date.now(),
|
|
338
|
+
extractionCompletedAt: null,
|
|
339
|
+
extractionError: null,
|
|
340
|
+
};
|
|
341
|
+
await this.options.store.writeState(this.stateValue);
|
|
342
|
+
try {
|
|
343
|
+
const summary = await this.options.extractor({
|
|
344
|
+
summary: this.summaryValue,
|
|
345
|
+
tokens,
|
|
346
|
+
toolCalls,
|
|
347
|
+
...(messages?.length ? { messages } : {}),
|
|
348
|
+
});
|
|
349
|
+
if (typeof summary !== 'string' || summary.trim().length === 0) {
|
|
350
|
+
throw new SessionMemoryStateError('Session memory extractor returned an empty summary');
|
|
351
|
+
}
|
|
352
|
+
// Persist the summary before the completed state so a crash in between
|
|
353
|
+
// is recovered as a stale extraction and safely re-extracted.
|
|
354
|
+
await this.options.store.writeSummary(summary);
|
|
355
|
+
this.stateValue = {
|
|
356
|
+
...this.stateValue,
|
|
357
|
+
initialized: true,
|
|
358
|
+
lastObservedTokens: tokens,
|
|
359
|
+
lastObservedToolCalls: toolCalls,
|
|
360
|
+
lastSummarizedMessageId: messageId,
|
|
361
|
+
extractionStartedAt: null,
|
|
362
|
+
extractionCompletedAt: Date.now(),
|
|
363
|
+
extractionError: null,
|
|
364
|
+
};
|
|
365
|
+
await this.options.store.writeState(this.stateValue);
|
|
366
|
+
this.summaryValue = summary;
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
const failure = error instanceof Error ? error.message : String(error);
|
|
370
|
+
this.stateValue = {
|
|
371
|
+
...this.stateValue,
|
|
372
|
+
extractionStartedAt: null,
|
|
373
|
+
extractionCompletedAt: null,
|
|
374
|
+
extractionError: failure,
|
|
375
|
+
};
|
|
376
|
+
await this.options.store
|
|
377
|
+
.writeState(this.stateValue)
|
|
378
|
+
.catch(() => undefined);
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
//# sourceMappingURL=session-memory.js.map
|
|
@@ -17,6 +17,7 @@ import { type ScheduledPrompt } from './scheduled-prompt-manager.js';
|
|
|
17
17
|
import { type WorkflowTaskSnapshot } from './workflow-manager.js';
|
|
18
18
|
import type { WorkspaceContext } from './session-worktree.js';
|
|
19
19
|
import { type ClaudeSessionCostSnapshot } from './session-cost-tracker.js';
|
|
20
|
+
import { type ClaudeToolRole } from '../tools/claude-capabilities.js';
|
|
20
21
|
import type { ClaudeInteractiveToolManager } from '../tools/claude-interactive-tools.js';
|
|
21
22
|
import type { ClaudePermissionMode } from '../permissions/claude-permission-resolver.js';
|
|
22
23
|
import type { ClaudeMcpRuntime, ClaudeMcpServerStatus, ClaudeMcpToolInspection } from '../mcp/claude-mcp-tools.js';
|
|
@@ -45,6 +46,10 @@ export interface ClaudeSessionServiceOptions {
|
|
|
45
46
|
subagentToolNames?: readonly string[];
|
|
46
47
|
taskToolNames?: readonly string[];
|
|
47
48
|
scheduledToolNames?: readonly string[];
|
|
49
|
+
/** Runtime gates for Claude capability-driven tool exposure. */
|
|
50
|
+
toolRole?: ClaudeToolRole;
|
|
51
|
+
toolCapabilityEnvironment?: Readonly<Record<string, string | undefined>>;
|
|
52
|
+
simpleMode?: boolean;
|
|
48
53
|
enableDynamicWakeups?: boolean;
|
|
49
54
|
enableWorkflows?: boolean;
|
|
50
55
|
providerForModel?: (model: string) => ModelProvider;
|
|
@@ -63,6 +68,10 @@ export interface ClaudeSessionServiceOptions {
|
|
|
63
68
|
brief?: boolean;
|
|
64
69
|
collectMetrics?: boolean;
|
|
65
70
|
sessionPersistence?: boolean;
|
|
71
|
+
/** Enable durable per-session memory extraction and injection. Defaults to
|
|
72
|
+
* enabled whenever session persistence is enabled; ignored when
|
|
73
|
+
* `sessionPersistence === false`. */
|
|
74
|
+
enableSessionMemory?: boolean;
|
|
66
75
|
sessionKind?: 'bg';
|
|
67
76
|
workspace?: WorkspaceContext;
|
|
68
77
|
initialWorktree?: boolean;
|
|
@@ -169,6 +178,7 @@ export declare class ClaudeSessionService {
|
|
|
169
178
|
private readonly sessionCostTrackers;
|
|
170
179
|
private activeCostSessionId;
|
|
171
180
|
private closeCostSavePromise;
|
|
181
|
+
private readonly sessionMemoryControllers;
|
|
172
182
|
private runtimeCwd;
|
|
173
183
|
constructor(options: ClaudeSessionServiceOptions);
|
|
174
184
|
nextScheduledPrompt(signal?: AbortSignal): Promise<ScheduledPrompt | null>;
|
|
@@ -258,6 +268,14 @@ export declare class ClaudeSessionService {
|
|
|
258
268
|
private mainAgentSystemPrompt;
|
|
259
269
|
private assembledSystemMessages;
|
|
260
270
|
private contextBudget;
|
|
271
|
+
private sessionMemoryEnabled;
|
|
272
|
+
private sessionMemoryController;
|
|
273
|
+
private sessionMemoryMessage;
|
|
274
|
+
private boundSessionMemorySummary;
|
|
275
|
+
private formatSessionMemoryConversation;
|
|
276
|
+
private extractSessionMemory;
|
|
277
|
+
private toolCapabilities;
|
|
278
|
+
private capabilityToolNames;
|
|
261
279
|
private append;
|
|
262
280
|
private logicalTailUuid;
|
|
263
281
|
}
|