@oh-my-pi/pi-coding-agent 12.2.1 → 12.3.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 +15 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +46 -0
- package/src/config/settings.ts +1 -0
- package/src/extensibility/slash-commands.ts +11 -0
- package/src/memories/index.ts +1102 -0
- package/src/memories/storage.ts +563 -0
- package/src/modes/controllers/command-controller.ts +46 -0
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/memories/consolidation.md +30 -0
- package/src/prompts/memories/read_path.md +11 -0
- package/src/prompts/memories/stage_one_input.md +7 -0
- package/src/prompts/memories/stage_one_system.md +21 -0
- package/src/sdk.ts +12 -0
- package/src/session/agent-session.ts +8 -0
- package/src/system-prompt.ts +8 -9
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
export interface MemoryThread {
|
|
4
|
+
id: string;
|
|
5
|
+
updatedAt: number;
|
|
6
|
+
rolloutPath: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
sourceKind: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Stage1OutputRow {
|
|
12
|
+
threadId: string;
|
|
13
|
+
sourceUpdatedAt: number;
|
|
14
|
+
rawMemory: string;
|
|
15
|
+
rolloutSummary: string;
|
|
16
|
+
rolloutSlug: string | null;
|
|
17
|
+
generatedAt: number;
|
|
18
|
+
cwd: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Stage1Claim {
|
|
22
|
+
threadId: string;
|
|
23
|
+
ownershipToken: string;
|
|
24
|
+
inputWatermark: number;
|
|
25
|
+
sourceUpdatedAt: number;
|
|
26
|
+
rolloutPath: string;
|
|
27
|
+
cwd: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GlobalClaim {
|
|
31
|
+
ownershipToken: string;
|
|
32
|
+
inputWatermark: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const STAGE1_KIND = "memory_stage1";
|
|
36
|
+
const GLOBAL_KIND = "memory_consolidate_global";
|
|
37
|
+
const GLOBAL_KEY = "global";
|
|
38
|
+
const DEFAULT_RETRY_REMAINING = 3;
|
|
39
|
+
|
|
40
|
+
export function openMemoryDb(dbPath: string): Database {
|
|
41
|
+
const db = new Database(dbPath);
|
|
42
|
+
db.exec(`
|
|
43
|
+
PRAGMA journal_mode=WAL;
|
|
44
|
+
PRAGMA synchronous=NORMAL;
|
|
45
|
+
PRAGMA busy_timeout=5000;
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
48
|
+
id TEXT PRIMARY KEY,
|
|
49
|
+
updated_at INTEGER NOT NULL,
|
|
50
|
+
rollout_path TEXT NOT NULL,
|
|
51
|
+
cwd TEXT NOT NULL,
|
|
52
|
+
source_kind TEXT NOT NULL
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS stage1_outputs (
|
|
56
|
+
thread_id TEXT PRIMARY KEY,
|
|
57
|
+
source_updated_at INTEGER NOT NULL,
|
|
58
|
+
raw_memory TEXT NOT NULL,
|
|
59
|
+
rollout_summary TEXT NOT NULL,
|
|
60
|
+
rollout_slug TEXT,
|
|
61
|
+
generated_at INTEGER NOT NULL
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
65
|
+
kind TEXT NOT NULL,
|
|
66
|
+
job_key TEXT NOT NULL,
|
|
67
|
+
status TEXT NOT NULL,
|
|
68
|
+
worker_id TEXT,
|
|
69
|
+
ownership_token TEXT,
|
|
70
|
+
started_at INTEGER,
|
|
71
|
+
finished_at INTEGER,
|
|
72
|
+
lease_until INTEGER,
|
|
73
|
+
retry_at INTEGER,
|
|
74
|
+
retry_remaining INTEGER NOT NULL,
|
|
75
|
+
last_error TEXT,
|
|
76
|
+
input_watermark INTEGER,
|
|
77
|
+
last_success_watermark INTEGER,
|
|
78
|
+
PRIMARY KEY (kind, job_key)
|
|
79
|
+
);
|
|
80
|
+
`);
|
|
81
|
+
return db;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function closeMemoryDb(db: Database): void {
|
|
85
|
+
db.close();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function clearMemoryData(db: Database): void {
|
|
89
|
+
db.exec(`
|
|
90
|
+
DELETE FROM stage1_outputs;
|
|
91
|
+
DELETE FROM threads;
|
|
92
|
+
DELETE FROM jobs WHERE kind IN ('memory_stage1', 'memory_consolidate_global');
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function upsertThreads(db: Database, threads: MemoryThread[]): void {
|
|
97
|
+
if (threads.length === 0) return;
|
|
98
|
+
const stmt = db.prepare(`
|
|
99
|
+
INSERT INTO threads (id, updated_at, rollout_path, cwd, source_kind)
|
|
100
|
+
VALUES (?, ?, ?, ?, ?)
|
|
101
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
102
|
+
updated_at = excluded.updated_at,
|
|
103
|
+
rollout_path = excluded.rollout_path,
|
|
104
|
+
cwd = excluded.cwd,
|
|
105
|
+
source_kind = excluded.source_kind
|
|
106
|
+
`);
|
|
107
|
+
const tx = db.transaction((rows: MemoryThread[]) => {
|
|
108
|
+
for (const row of rows) {
|
|
109
|
+
stmt.run(row.id, row.updatedAt, row.rolloutPath, row.cwd, row.sourceKind);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
tx(threads);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ensureStage1Job(db: Database, threadId: string): void {
|
|
116
|
+
db.prepare(`
|
|
117
|
+
INSERT OR IGNORE INTO jobs (kind, job_key, status, retry_remaining, input_watermark, last_success_watermark)
|
|
118
|
+
VALUES (?, ?, 'pending', ?, 0, 0)
|
|
119
|
+
`).run(STAGE1_KIND, threadId, DEFAULT_RETRY_REMAINING);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ensureGlobalJob(db: Database): void {
|
|
123
|
+
db.prepare(`
|
|
124
|
+
INSERT OR IGNORE INTO jobs (kind, job_key, status, retry_remaining, input_watermark, last_success_watermark)
|
|
125
|
+
VALUES (?, ?, 'pending', ?, 0, 0)
|
|
126
|
+
`).run(GLOBAL_KIND, GLOBAL_KEY, DEFAULT_RETRY_REMAINING);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function claimStage1Jobs(
|
|
130
|
+
db: Database,
|
|
131
|
+
params: {
|
|
132
|
+
nowSec: number;
|
|
133
|
+
threadScanLimit: number;
|
|
134
|
+
maxRolloutsPerStartup: number;
|
|
135
|
+
maxRolloutAgeDays: number;
|
|
136
|
+
minRolloutIdleHours: number;
|
|
137
|
+
leaseSeconds: number;
|
|
138
|
+
runningConcurrencyCap: number;
|
|
139
|
+
workerId: string;
|
|
140
|
+
excludeThreadIds?: string[];
|
|
141
|
+
},
|
|
142
|
+
): Stage1Claim[] {
|
|
143
|
+
const {
|
|
144
|
+
nowSec,
|
|
145
|
+
threadScanLimit,
|
|
146
|
+
maxRolloutsPerStartup,
|
|
147
|
+
maxRolloutAgeDays,
|
|
148
|
+
minRolloutIdleHours,
|
|
149
|
+
leaseSeconds,
|
|
150
|
+
runningConcurrencyCap,
|
|
151
|
+
workerId,
|
|
152
|
+
excludeThreadIds = [],
|
|
153
|
+
} = params;
|
|
154
|
+
const maxAgeSec = maxRolloutAgeDays * 24 * 60 * 60;
|
|
155
|
+
const minIdleSec = minRolloutIdleHours * 60 * 60;
|
|
156
|
+
const runningCountRow = db
|
|
157
|
+
.prepare(
|
|
158
|
+
"SELECT COUNT(*) AS count FROM jobs WHERE kind = ? AND status = 'running' AND lease_until IS NOT NULL AND lease_until > ?",
|
|
159
|
+
)
|
|
160
|
+
.get(STAGE1_KIND, nowSec) as { count?: number } | undefined;
|
|
161
|
+
let runningCount = runningCountRow?.count ?? 0;
|
|
162
|
+
if (runningCount >= runningConcurrencyCap) return [];
|
|
163
|
+
const candidateRows = db
|
|
164
|
+
.prepare("SELECT id, updated_at, rollout_path, cwd, source_kind FROM threads ORDER BY updated_at DESC LIMIT ?")
|
|
165
|
+
.all(threadScanLimit) as Array<{
|
|
166
|
+
id: string;
|
|
167
|
+
updated_at: number;
|
|
168
|
+
rollout_path: string;
|
|
169
|
+
cwd: string;
|
|
170
|
+
source_kind: string;
|
|
171
|
+
}>;
|
|
172
|
+
const claims: Stage1Claim[] = [];
|
|
173
|
+
const excluded = new Set(excludeThreadIds);
|
|
174
|
+
for (const row of candidateRows) {
|
|
175
|
+
if (claims.length >= maxRolloutsPerStartup) break;
|
|
176
|
+
if (excluded.has(row.id)) continue;
|
|
177
|
+
if (row.source_kind !== "cli" && row.source_kind !== "app") continue;
|
|
178
|
+
if (nowSec - row.updated_at > maxAgeSec) continue;
|
|
179
|
+
if (nowSec - row.updated_at < minIdleSec) continue;
|
|
180
|
+
if (runningCount >= runningConcurrencyCap) break;
|
|
181
|
+
const stage1 = db.prepare("SELECT source_updated_at FROM stage1_outputs WHERE thread_id = ?").get(row.id) as
|
|
182
|
+
| { source_updated_at?: number }
|
|
183
|
+
| undefined;
|
|
184
|
+
if ((stage1?.source_updated_at ?? 0) >= row.updated_at) continue;
|
|
185
|
+
ensureStage1Job(db, row.id);
|
|
186
|
+
const ownershipToken = crypto.randomUUID();
|
|
187
|
+
const leaseUntil = nowSec + leaseSeconds;
|
|
188
|
+
const claimed = db
|
|
189
|
+
.prepare(`
|
|
190
|
+
UPDATE jobs
|
|
191
|
+
SET status = 'running', worker_id = ?, ownership_token = ?, started_at = ?, finished_at = NULL,
|
|
192
|
+
lease_until = ?, retry_at = NULL, last_error = NULL, input_watermark = ?,
|
|
193
|
+
retry_remaining = CASE
|
|
194
|
+
WHEN input_watermark IS NULL THEN ?
|
|
195
|
+
WHEN input_watermark < ? THEN ?
|
|
196
|
+
ELSE retry_remaining
|
|
197
|
+
END
|
|
198
|
+
WHERE kind = ? AND job_key = ?
|
|
199
|
+
AND (last_success_watermark IS NULL OR last_success_watermark < ?)
|
|
200
|
+
AND NOT (status = 'running' AND lease_until IS NOT NULL AND lease_until > ?)
|
|
201
|
+
AND (
|
|
202
|
+
input_watermark IS NULL
|
|
203
|
+
OR input_watermark < ?
|
|
204
|
+
OR (
|
|
205
|
+
retry_remaining > 0
|
|
206
|
+
AND (retry_at IS NULL OR retry_at <= ?)
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
`)
|
|
210
|
+
.run(
|
|
211
|
+
workerId,
|
|
212
|
+
ownershipToken,
|
|
213
|
+
nowSec,
|
|
214
|
+
leaseUntil,
|
|
215
|
+
row.updated_at,
|
|
216
|
+
DEFAULT_RETRY_REMAINING,
|
|
217
|
+
row.updated_at,
|
|
218
|
+
DEFAULT_RETRY_REMAINING,
|
|
219
|
+
STAGE1_KIND,
|
|
220
|
+
row.id,
|
|
221
|
+
row.updated_at,
|
|
222
|
+
nowSec,
|
|
223
|
+
row.updated_at,
|
|
224
|
+
nowSec,
|
|
225
|
+
);
|
|
226
|
+
if (Number(claimed.changes ?? 0) <= 0) continue;
|
|
227
|
+
claims.push({
|
|
228
|
+
threadId: row.id,
|
|
229
|
+
ownershipToken,
|
|
230
|
+
inputWatermark: row.updated_at,
|
|
231
|
+
sourceUpdatedAt: row.updated_at,
|
|
232
|
+
rolloutPath: row.rollout_path,
|
|
233
|
+
cwd: row.cwd,
|
|
234
|
+
});
|
|
235
|
+
runningCount += 1;
|
|
236
|
+
}
|
|
237
|
+
return claims;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function enqueueGlobalWatermark(
|
|
241
|
+
db: Database,
|
|
242
|
+
sourceUpdatedAt: number,
|
|
243
|
+
params?: { forceDirtyWhenNotAdvanced?: boolean },
|
|
244
|
+
): void {
|
|
245
|
+
const forceDirtyWhenNotAdvanced = params?.forceDirtyWhenNotAdvanced ?? false;
|
|
246
|
+
ensureGlobalJob(db);
|
|
247
|
+
db.prepare(`
|
|
248
|
+
UPDATE jobs
|
|
249
|
+
SET
|
|
250
|
+
input_watermark = CASE
|
|
251
|
+
WHEN input_watermark IS NULL THEN ?
|
|
252
|
+
WHEN input_watermark < ? THEN ?
|
|
253
|
+
WHEN ? = 1 AND (last_success_watermark IS NULL OR input_watermark <= last_success_watermark) THEN
|
|
254
|
+
CASE
|
|
255
|
+
WHEN last_success_watermark IS NULL THEN input_watermark + 1
|
|
256
|
+
ELSE last_success_watermark + 1
|
|
257
|
+
END
|
|
258
|
+
ELSE input_watermark
|
|
259
|
+
END,
|
|
260
|
+
retry_remaining = CASE
|
|
261
|
+
WHEN input_watermark IS NULL THEN ?
|
|
262
|
+
WHEN input_watermark < ? THEN ?
|
|
263
|
+
WHEN ? = 1 AND (last_success_watermark IS NULL OR input_watermark <= last_success_watermark) THEN ?
|
|
264
|
+
ELSE retry_remaining
|
|
265
|
+
END,
|
|
266
|
+
retry_at = CASE
|
|
267
|
+
WHEN input_watermark IS NULL THEN NULL
|
|
268
|
+
WHEN input_watermark < ? THEN NULL
|
|
269
|
+
WHEN ? = 1 AND (last_success_watermark IS NULL OR input_watermark <= last_success_watermark) THEN NULL
|
|
270
|
+
ELSE retry_at
|
|
271
|
+
END
|
|
272
|
+
WHERE kind = ? AND job_key = ?
|
|
273
|
+
`).run(
|
|
274
|
+
sourceUpdatedAt,
|
|
275
|
+
sourceUpdatedAt,
|
|
276
|
+
sourceUpdatedAt,
|
|
277
|
+
forceDirtyWhenNotAdvanced ? 1 : 0,
|
|
278
|
+
DEFAULT_RETRY_REMAINING,
|
|
279
|
+
sourceUpdatedAt,
|
|
280
|
+
DEFAULT_RETRY_REMAINING,
|
|
281
|
+
forceDirtyWhenNotAdvanced ? 1 : 0,
|
|
282
|
+
DEFAULT_RETRY_REMAINING,
|
|
283
|
+
sourceUpdatedAt,
|
|
284
|
+
forceDirtyWhenNotAdvanced ? 1 : 0,
|
|
285
|
+
GLOBAL_KIND,
|
|
286
|
+
GLOBAL_KEY,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function markStage1SucceededWithOutput(
|
|
291
|
+
db: Database,
|
|
292
|
+
params: {
|
|
293
|
+
threadId: string;
|
|
294
|
+
ownershipToken: string;
|
|
295
|
+
sourceUpdatedAt: number;
|
|
296
|
+
rawMemory: string;
|
|
297
|
+
rolloutSummary: string;
|
|
298
|
+
rolloutSlug: string | null;
|
|
299
|
+
nowSec: number;
|
|
300
|
+
},
|
|
301
|
+
): boolean {
|
|
302
|
+
const { threadId, ownershipToken, sourceUpdatedAt, rawMemory, rolloutSummary, rolloutSlug, nowSec } = params;
|
|
303
|
+
const tx = db.transaction(() => {
|
|
304
|
+
const matched = db
|
|
305
|
+
.prepare(
|
|
306
|
+
"SELECT 1 AS ok FROM jobs WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?",
|
|
307
|
+
)
|
|
308
|
+
.get(STAGE1_KIND, threadId, ownershipToken) as { ok?: number } | undefined;
|
|
309
|
+
if (!matched?.ok) return false;
|
|
310
|
+
|
|
311
|
+
db.prepare(`
|
|
312
|
+
UPDATE jobs
|
|
313
|
+
SET status = 'done', finished_at = ?, lease_until = NULL, retry_at = NULL,
|
|
314
|
+
last_error = NULL, last_success_watermark = input_watermark
|
|
315
|
+
WHERE kind = ? AND job_key = ? AND ownership_token = ?
|
|
316
|
+
`).run(nowSec, STAGE1_KIND, threadId, ownershipToken);
|
|
317
|
+
|
|
318
|
+
db.prepare(`
|
|
319
|
+
INSERT INTO stage1_outputs (thread_id, source_updated_at, raw_memory, rollout_summary, rollout_slug, generated_at)
|
|
320
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
321
|
+
ON CONFLICT(thread_id) DO UPDATE SET
|
|
322
|
+
source_updated_at = excluded.source_updated_at,
|
|
323
|
+
raw_memory = excluded.raw_memory,
|
|
324
|
+
rollout_summary = excluded.rollout_summary,
|
|
325
|
+
rollout_slug = excluded.rollout_slug,
|
|
326
|
+
generated_at = excluded.generated_at
|
|
327
|
+
WHERE excluded.source_updated_at >= stage1_outputs.source_updated_at
|
|
328
|
+
`).run(threadId, sourceUpdatedAt, rawMemory, rolloutSummary, rolloutSlug, nowSec);
|
|
329
|
+
|
|
330
|
+
enqueueGlobalWatermark(db, sourceUpdatedAt, { forceDirtyWhenNotAdvanced: true });
|
|
331
|
+
return true;
|
|
332
|
+
});
|
|
333
|
+
return tx() as boolean;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function markStage1SucceededNoOutput(
|
|
337
|
+
db: Database,
|
|
338
|
+
params: { threadId: string; ownershipToken: string; sourceUpdatedAt: number; nowSec: number },
|
|
339
|
+
): boolean {
|
|
340
|
+
const { threadId, ownershipToken, sourceUpdatedAt, nowSec } = params;
|
|
341
|
+
const tx = db.transaction(() => {
|
|
342
|
+
const matched = db
|
|
343
|
+
.prepare(
|
|
344
|
+
"SELECT 1 AS ok FROM jobs WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?",
|
|
345
|
+
)
|
|
346
|
+
.get(STAGE1_KIND, threadId, ownershipToken) as { ok?: number } | undefined;
|
|
347
|
+
if (!matched?.ok) return false;
|
|
348
|
+
|
|
349
|
+
db.prepare(`
|
|
350
|
+
UPDATE jobs
|
|
351
|
+
SET status = 'done', finished_at = ?, lease_until = NULL, retry_at = NULL,
|
|
352
|
+
last_error = NULL, last_success_watermark = input_watermark
|
|
353
|
+
WHERE kind = ? AND job_key = ? AND ownership_token = ?
|
|
354
|
+
`).run(nowSec, STAGE1_KIND, threadId, ownershipToken);
|
|
355
|
+
|
|
356
|
+
db.prepare("DELETE FROM stage1_outputs WHERE thread_id = ?").run(threadId);
|
|
357
|
+
enqueueGlobalWatermark(db, sourceUpdatedAt, { forceDirtyWhenNotAdvanced: true });
|
|
358
|
+
return true;
|
|
359
|
+
});
|
|
360
|
+
return tx() as boolean;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function markStage1Failed(
|
|
364
|
+
db: Database,
|
|
365
|
+
params: { threadId: string; ownershipToken: string; retryDelaySeconds: number; reason: string; nowSec: number },
|
|
366
|
+
): boolean {
|
|
367
|
+
const { threadId, ownershipToken, retryDelaySeconds, reason, nowSec } = params;
|
|
368
|
+
const result = db
|
|
369
|
+
.prepare(`
|
|
370
|
+
UPDATE jobs
|
|
371
|
+
SET status = 'error', finished_at = ?, lease_until = NULL, retry_at = ?,
|
|
372
|
+
retry_remaining = CASE WHEN retry_remaining > 0 THEN retry_remaining - 1 ELSE 0 END,
|
|
373
|
+
last_error = ?
|
|
374
|
+
WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?
|
|
375
|
+
`)
|
|
376
|
+
.run(nowSec, nowSec + retryDelaySeconds, reason, STAGE1_KIND, threadId, ownershipToken);
|
|
377
|
+
return Number(result.changes ?? 0) > 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function tryClaimGlobalPhase2Job(
|
|
381
|
+
db: Database,
|
|
382
|
+
params: { workerId: string; leaseSeconds: number; nowSec: number },
|
|
383
|
+
): { kind: "claimed"; claim: GlobalClaim } | { kind: "skipped_not_dirty" } | { kind: "skipped_running" } {
|
|
384
|
+
const { workerId, leaseSeconds, nowSec } = params;
|
|
385
|
+
ensureGlobalJob(db);
|
|
386
|
+
const pre = db
|
|
387
|
+
.prepare(
|
|
388
|
+
"SELECT status, lease_until, input_watermark, last_success_watermark, retry_at, retry_remaining FROM jobs WHERE kind = ? AND job_key = ?",
|
|
389
|
+
)
|
|
390
|
+
.get(GLOBAL_KIND, GLOBAL_KEY) as
|
|
391
|
+
| {
|
|
392
|
+
status: string;
|
|
393
|
+
lease_until: number | null;
|
|
394
|
+
input_watermark: number | null;
|
|
395
|
+
last_success_watermark: number | null;
|
|
396
|
+
retry_at: number | null;
|
|
397
|
+
retry_remaining: number;
|
|
398
|
+
}
|
|
399
|
+
| undefined;
|
|
400
|
+
if (!pre) return { kind: "skipped_not_dirty" };
|
|
401
|
+
const ownershipToken = crypto.randomUUID();
|
|
402
|
+
const claimed = db
|
|
403
|
+
.prepare(`
|
|
404
|
+
UPDATE jobs
|
|
405
|
+
SET status = 'running', worker_id = ?, ownership_token = ?, started_at = ?, finished_at = NULL,
|
|
406
|
+
lease_until = ?, retry_at = NULL, last_error = NULL
|
|
407
|
+
WHERE kind = ? AND job_key = ?
|
|
408
|
+
AND NOT (status = 'running' AND lease_until IS NOT NULL AND lease_until > ?)
|
|
409
|
+
AND (input_watermark IS NOT NULL AND (last_success_watermark IS NULL OR input_watermark > last_success_watermark))
|
|
410
|
+
AND retry_remaining > 0
|
|
411
|
+
AND (retry_at IS NULL OR retry_at <= ?)
|
|
412
|
+
`)
|
|
413
|
+
.run(workerId, ownershipToken, nowSec, nowSec + leaseSeconds, GLOBAL_KIND, GLOBAL_KEY, nowSec, nowSec);
|
|
414
|
+
if (Number(claimed.changes ?? 0) > 0) {
|
|
415
|
+
const row = db
|
|
416
|
+
.prepare("SELECT input_watermark FROM jobs WHERE kind = ? AND job_key = ? AND ownership_token = ?")
|
|
417
|
+
.get(GLOBAL_KIND, GLOBAL_KEY, ownershipToken) as { input_watermark: number | null } | undefined;
|
|
418
|
+
return {
|
|
419
|
+
kind: "claimed",
|
|
420
|
+
claim: {
|
|
421
|
+
ownershipToken,
|
|
422
|
+
inputWatermark: row?.input_watermark ?? 0,
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (pre.status === "running" && pre.lease_until !== null && pre.lease_until > nowSec) {
|
|
428
|
+
return { kind: "skipped_running" };
|
|
429
|
+
}
|
|
430
|
+
const preInput = pre.input_watermark ?? 0;
|
|
431
|
+
const preSuccess = pre.last_success_watermark ?? 0;
|
|
432
|
+
if (preInput <= preSuccess) {
|
|
433
|
+
return { kind: "skipped_not_dirty" };
|
|
434
|
+
}
|
|
435
|
+
if (pre.retry_remaining <= 0) {
|
|
436
|
+
return { kind: "skipped_not_dirty" };
|
|
437
|
+
}
|
|
438
|
+
if (pre.retry_at !== null && pre.retry_at > nowSec) {
|
|
439
|
+
return { kind: "skipped_not_dirty" };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const post = db
|
|
443
|
+
.prepare(
|
|
444
|
+
"SELECT status, lease_until, input_watermark, last_success_watermark, retry_at, retry_remaining FROM jobs WHERE kind = ? AND job_key = ?",
|
|
445
|
+
)
|
|
446
|
+
.get(GLOBAL_KIND, GLOBAL_KEY) as
|
|
447
|
+
| {
|
|
448
|
+
status: string;
|
|
449
|
+
lease_until: number | null;
|
|
450
|
+
input_watermark: number | null;
|
|
451
|
+
last_success_watermark: number | null;
|
|
452
|
+
retry_at: number | null;
|
|
453
|
+
retry_remaining: number;
|
|
454
|
+
}
|
|
455
|
+
| undefined;
|
|
456
|
+
if (!post) return { kind: "skipped_not_dirty" };
|
|
457
|
+
if (post.status === "running" && post.lease_until !== null && post.lease_until > nowSec) {
|
|
458
|
+
return { kind: "skipped_running" };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return { kind: "skipped_not_dirty" };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function heartbeatGlobalJob(
|
|
465
|
+
db: Database,
|
|
466
|
+
params: { ownershipToken: string; leaseSeconds: number; nowSec: number },
|
|
467
|
+
): boolean {
|
|
468
|
+
const { ownershipToken, leaseSeconds, nowSec } = params;
|
|
469
|
+
const result = db
|
|
470
|
+
.prepare(`
|
|
471
|
+
UPDATE jobs
|
|
472
|
+
SET lease_until = ?
|
|
473
|
+
WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?
|
|
474
|
+
`)
|
|
475
|
+
.run(nowSec + leaseSeconds, GLOBAL_KIND, GLOBAL_KEY, ownershipToken);
|
|
476
|
+
return Number(result.changes ?? 0) > 0;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function listStage1OutputsForGlobal(db: Database, limit: number): Stage1OutputRow[] {
|
|
480
|
+
const rows = db
|
|
481
|
+
.prepare(`
|
|
482
|
+
SELECT o.thread_id, o.source_updated_at, o.raw_memory, o.rollout_summary, o.rollout_slug, o.generated_at, t.cwd
|
|
483
|
+
FROM stage1_outputs o
|
|
484
|
+
LEFT JOIN threads t ON t.id = o.thread_id
|
|
485
|
+
WHERE TRIM(COALESCE(o.raw_memory, '')) != '' OR TRIM(COALESCE(o.rollout_summary, '')) != ''
|
|
486
|
+
ORDER BY o.source_updated_at DESC
|
|
487
|
+
LIMIT ?
|
|
488
|
+
`)
|
|
489
|
+
.all(limit) as Array<{
|
|
490
|
+
thread_id: string;
|
|
491
|
+
source_updated_at: number;
|
|
492
|
+
raw_memory: string;
|
|
493
|
+
rollout_summary: string;
|
|
494
|
+
rollout_slug: string | null;
|
|
495
|
+
generated_at: number;
|
|
496
|
+
cwd: string | null;
|
|
497
|
+
}>;
|
|
498
|
+
return rows.map(row => ({
|
|
499
|
+
threadId: row.thread_id,
|
|
500
|
+
sourceUpdatedAt: row.source_updated_at,
|
|
501
|
+
rawMemory: row.raw_memory,
|
|
502
|
+
rolloutSummary: row.rollout_summary,
|
|
503
|
+
rolloutSlug: row.rollout_slug,
|
|
504
|
+
generatedAt: row.generated_at,
|
|
505
|
+
cwd: row.cwd ?? "",
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function markGlobalPhase2Succeeded(
|
|
510
|
+
db: Database,
|
|
511
|
+
params: { ownershipToken: string; newWatermark: number; nowSec: number },
|
|
512
|
+
): boolean {
|
|
513
|
+
const { ownershipToken, newWatermark, nowSec } = params;
|
|
514
|
+
const result = db
|
|
515
|
+
.prepare(`
|
|
516
|
+
UPDATE jobs
|
|
517
|
+
SET status = 'done', finished_at = ?, lease_until = NULL, retry_at = NULL,
|
|
518
|
+
last_error = NULL,
|
|
519
|
+
last_success_watermark = CASE
|
|
520
|
+
WHEN last_success_watermark IS NULL THEN ?
|
|
521
|
+
WHEN last_success_watermark < ? THEN ?
|
|
522
|
+
ELSE last_success_watermark
|
|
523
|
+
END
|
|
524
|
+
WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?
|
|
525
|
+
`)
|
|
526
|
+
.run(nowSec, newWatermark, newWatermark, newWatermark, GLOBAL_KIND, GLOBAL_KEY, ownershipToken);
|
|
527
|
+
return Number(result.changes ?? 0) > 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function markGlobalPhase2Failed(
|
|
531
|
+
db: Database,
|
|
532
|
+
params: { ownershipToken: string; retryDelaySeconds: number; reason: string; nowSec: number },
|
|
533
|
+
): boolean {
|
|
534
|
+
const { ownershipToken, retryDelaySeconds, reason, nowSec } = params;
|
|
535
|
+
const result = db
|
|
536
|
+
.prepare(`
|
|
537
|
+
UPDATE jobs
|
|
538
|
+
SET status = 'error', finished_at = ?, lease_until = NULL, retry_at = ?,
|
|
539
|
+
retry_remaining = CASE WHEN retry_remaining > 0 THEN retry_remaining - 1 ELSE 0 END,
|
|
540
|
+
last_error = ?
|
|
541
|
+
WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?
|
|
542
|
+
`)
|
|
543
|
+
.run(nowSec, nowSec + retryDelaySeconds, reason, GLOBAL_KIND, GLOBAL_KEY, ownershipToken);
|
|
544
|
+
return Number(result.changes ?? 0) > 0;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function markGlobalPhase2FailedUnowned(
|
|
548
|
+
db: Database,
|
|
549
|
+
params: { retryDelaySeconds: number; reason: string; nowSec: number },
|
|
550
|
+
): boolean {
|
|
551
|
+
const { retryDelaySeconds, reason, nowSec } = params;
|
|
552
|
+
const result = db
|
|
553
|
+
.prepare(`
|
|
554
|
+
UPDATE jobs
|
|
555
|
+
SET status = 'error', finished_at = ?, lease_until = NULL, retry_at = ?,
|
|
556
|
+
retry_remaining = CASE WHEN retry_remaining > 0 THEN retry_remaining - 1 ELSE 0 END,
|
|
557
|
+
last_error = ?
|
|
558
|
+
WHERE kind = ? AND job_key = ? AND status = 'running'
|
|
559
|
+
AND (ownership_token IS NULL OR lease_until IS NULL OR lease_until <= ?)
|
|
560
|
+
`)
|
|
561
|
+
.run(nowSec, nowSec + retryDelaySeconds, reason, GLOBAL_KIND, GLOBAL_KEY, nowSec);
|
|
562
|
+
return Number(result.changes ?? 0) > 0;
|
|
563
|
+
}
|
|
@@ -17,6 +17,7 @@ import { reset as resetCapabilities } from "../../capability";
|
|
|
17
17
|
import { loadCustomShare } from "../../export/custom-share";
|
|
18
18
|
import type { CompactOptions } from "../../extensibility/extensions/types";
|
|
19
19
|
import { getGatewayStatus } from "../../ipy/gateway-coordinator";
|
|
20
|
+
import { buildMemoryToolDeveloperInstructions, clearMemoryData, enqueueMemoryConsolidation } from "../../memories";
|
|
20
21
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
21
22
|
import { BorderedLoader } from "../../modes/components/bordered-loader";
|
|
22
23
|
import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
@@ -408,6 +409,51 @@ export class CommandController {
|
|
|
408
409
|
this.ctx.ui.requestRender();
|
|
409
410
|
}
|
|
410
411
|
|
|
412
|
+
async handleMemoryCommand(text: string): Promise<void> {
|
|
413
|
+
const argumentText = text.slice(7).trim();
|
|
414
|
+
const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
|
|
415
|
+
const agentDir = this.ctx.settings.getAgentDir();
|
|
416
|
+
|
|
417
|
+
if (action === "view") {
|
|
418
|
+
const payload = await buildMemoryToolDeveloperInstructions(agentDir, this.ctx.settings);
|
|
419
|
+
if (!payload) {
|
|
420
|
+
this.ctx.showWarning("Memory payload is empty (memories disabled or no memory summary found).");
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
424
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
425
|
+
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Memory Injection Payload")), 1, 0));
|
|
426
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
427
|
+
this.ctx.chatContainer.addChild(new Markdown(payload, 1, 1, getMarkdownTheme()));
|
|
428
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
429
|
+
this.ctx.ui.requestRender();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (action === "reset" || action === "clear") {
|
|
434
|
+
try {
|
|
435
|
+
await clearMemoryData(agentDir);
|
|
436
|
+
await this.ctx.session.refreshBaseSystemPrompt();
|
|
437
|
+
this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
|
|
438
|
+
} catch (error) {
|
|
439
|
+
this.ctx.showError(`Memory clear failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (action === "enqueue" || action === "rebuild") {
|
|
445
|
+
try {
|
|
446
|
+
enqueueMemoryConsolidation(agentDir);
|
|
447
|
+
this.ctx.showStatus("Memory consolidation enqueued.");
|
|
448
|
+
} catch (error) {
|
|
449
|
+
this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild>");
|
|
455
|
+
}
|
|
456
|
+
|
|
411
457
|
async handleClearCommand(): Promise<void> {
|
|
412
458
|
if (this.ctx.loadingAnimation) {
|
|
413
459
|
this.ctx.loadingAnimation.stop();
|
|
@@ -333,6 +333,11 @@ export class InputController {
|
|
|
333
333
|
this.ctx.editor.setText("");
|
|
334
334
|
return;
|
|
335
335
|
}
|
|
336
|
+
if (text === "/memory" || text.startsWith("/memory ")) {
|
|
337
|
+
this.ctx.editor.setText("");
|
|
338
|
+
await this.ctx.handleMemoryCommand(text);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
336
341
|
if (text === "/resume") {
|
|
337
342
|
this.ctx.showSessionSelector();
|
|
338
343
|
this.ctx.editor.setText("");
|
|
@@ -919,6 +919,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
919
919
|
return this.#commandController.handleMoveCommand(targetPath);
|
|
920
920
|
}
|
|
921
921
|
|
|
922
|
+
handleMemoryCommand(text: string): Promise<void> {
|
|
923
|
+
return this.#commandController.handleMemoryCommand(text);
|
|
924
|
+
}
|
|
925
|
+
|
|
922
926
|
showDebugSelector(): void {
|
|
923
927
|
this.#selectorController.showDebugSelector();
|
|
924
928
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -151,6 +151,7 @@ export interface InteractiveModeContext {
|
|
|
151
151
|
handleCompactCommand(customInstructions?: string): Promise<void>;
|
|
152
152
|
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
|
153
153
|
handleMoveCommand(targetPath: string): Promise<void>;
|
|
154
|
+
handleMemoryCommand(text: string): Promise<void>;
|
|
154
155
|
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
|
|
155
156
|
openInBrowser(urlOrPath: string): void;
|
|
156
157
|
refreshSlashCommandState(cwd?: string): Promise<void>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
You are the memory consolidation agent.
|
|
2
|
+
Memory root: {{memory_root}}
|
|
3
|
+
Input corpus (raw memories):
|
|
4
|
+
{{raw_memories}}
|
|
5
|
+
Input corpus (rollout summaries):
|
|
6
|
+
{{rollout_summaries}}
|
|
7
|
+
Produce strict JSON only with this schema:
|
|
8
|
+
{
|
|
9
|
+
"memory_md": "string",
|
|
10
|
+
"memory_summary": "string",
|
|
11
|
+
"skills": [
|
|
12
|
+
{
|
|
13
|
+
"name": "string",
|
|
14
|
+
"content": "string",
|
|
15
|
+
"scripts": [{ "path": "string", "content": "string" }],
|
|
16
|
+
"templates": [{ "path": "string", "content": "string" }],
|
|
17
|
+
"examples": [{ "path": "string", "content": "string" }]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
Requirements:
|
|
22
|
+
- memory_md: full long-term memory document, curated and readable.
|
|
23
|
+
- memory_summary: compact prompt-time memory guidance.
|
|
24
|
+
- skills: reusable procedural playbooks. Empty array allowed.
|
|
25
|
+
- Each skill.name maps to skills/<name>/.
|
|
26
|
+
- Each skill.content maps to skills/<name>/SKILL.md.
|
|
27
|
+
- scripts/templates/examples are optional. When present, each entry writes to skills/<name>/<bucket>/<path>.
|
|
28
|
+
- Only include files worth keeping long-term; omit stale assets so they are pruned.
|
|
29
|
+
- Preserve useful prior themes; remove stale or contradictory guidance.
|
|
30
|
+
- Keep memory advisory: current repository state wins.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Memory Guidance
|
|
2
|
+
Memory root: {{base_path}}
|
|
3
|
+
Operational rules:
|
|
4
|
+
1) Read `{{base_path}}/memory_summary.md` first.
|
|
5
|
+
2) If needed, inspect `{{base_path}}/MEMORY.md` and `{{base_path}}/skills/*/SKILL.md`.
|
|
6
|
+
3) Decision boundary: trust memory for heuristics/process context; trust current repo files, runtime output, and user instruction for factual state and final decisions.
|
|
7
|
+
4) Citation policy: when memory changes your plan, cite the memory artifact path you used (for example `memories/skills/<name>/SKILL.md`) and pair it with current-repo evidence before acting.
|
|
8
|
+
5) Conflict workflow: if memory disagrees with repo state or user instruction, prefer repo/user, treat memory as stale, proceed with corrected behavior, then update/regenerate memory artifacts through normal execution.
|
|
9
|
+
6) Escalate confidence only after repository verification; memory alone is never sufficient proof.
|
|
10
|
+
Memory summary:
|
|
11
|
+
{{memory_summary}}
|