@oh-my-pi/pi-coding-agent 12.2.1 → 12.4.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 +30 -0
- package/package.json +7 -7
- package/src/config/keybindings.ts +2 -1
- 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/lsp/render.ts +1 -1
- package/src/memories/index.ts +1106 -0
- package/src/memories/storage.ts +563 -0
- package/src/modes/components/bash-execution.ts +16 -9
- package/src/modes/components/python-execution.ts +16 -7
- package/src/modes/components/tool-execution.ts +2 -1
- 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/session/streaming-output.ts +1 -1
- package/src/system-prompt.ts +8 -9
- package/src/tools/bash-interactive.ts +10 -6
- package/src/tools/fetch.ts +1 -1
- package/src/tools/output-meta.ts +6 -2
- package/src/web/scrapers/types.ts +1 -0
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Component for displaying bash command execution with streaming output.
|
|
3
3
|
*/
|
|
4
|
+
|
|
5
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
4
6
|
import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
5
7
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
6
8
|
import type { TruncationMeta } from "../../tools/output-meta";
|
|
@@ -10,6 +12,7 @@ import { truncateToVisualLines } from "./visual-truncate";
|
|
|
10
12
|
|
|
11
13
|
// Preview line limit when not expanded (matches tool execution behavior)
|
|
12
14
|
const PREVIEW_LINES = 20;
|
|
15
|
+
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
13
16
|
|
|
14
17
|
export class BashExecutionComponent extends Container {
|
|
15
18
|
#outputLines: string[] = [];
|
|
@@ -73,13 +76,15 @@ export class BashExecutionComponent extends Container {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
appendOutput(chunk: string): void {
|
|
76
|
-
const clean =
|
|
79
|
+
const clean = sanitizeText(chunk);
|
|
77
80
|
|
|
78
81
|
// Append to output lines
|
|
79
|
-
const newLines = clean.split("\n");
|
|
82
|
+
const newLines = clean.split("\n").map(line => this.#clampDisplayLine(line));
|
|
80
83
|
if (this.#outputLines.length > 0 && newLines.length > 0) {
|
|
81
84
|
// Append first chunk to last line (incomplete line continuation)
|
|
82
|
-
this.#outputLines[this.#outputLines.length - 1]
|
|
85
|
+
this.#outputLines[this.#outputLines.length - 1] = this.#clampDisplayLine(
|
|
86
|
+
`${this.#outputLines[this.#outputLines.length - 1]}${newLines[0]}`,
|
|
87
|
+
);
|
|
83
88
|
this.#outputLines.push(...newLines.slice(1));
|
|
84
89
|
} else {
|
|
85
90
|
this.#outputLines.push(...newLines);
|
|
@@ -184,15 +189,17 @@ export class BashExecutionComponent extends Container {
|
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
#clampDisplayLine(line: string): string {
|
|
193
|
+
if (line.length <= MAX_DISPLAY_LINE_CHARS) {
|
|
194
|
+
return line;
|
|
195
|
+
}
|
|
196
|
+
const omitted = line.length - MAX_DISPLAY_LINE_CHARS;
|
|
197
|
+
return `${line.slice(0, MAX_DISPLAY_LINE_CHARS)}… [${omitted} chars omitted]`;
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
#setOutput(output: string): void {
|
|
194
|
-
const clean =
|
|
195
|
-
this.#outputLines = clean ? clean.split("\n") : [];
|
|
201
|
+
const clean = sanitizeText(output);
|
|
202
|
+
this.#outputLines = clean ? clean.split("\n").map(line => this.#clampDisplayLine(line)) : [];
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
/**
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Component for displaying user-initiated Python execution with streaming output.
|
|
3
3
|
* Shares the same kernel session as the agent's Python tool.
|
|
4
4
|
*/
|
|
5
|
+
|
|
6
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
5
7
|
import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
6
8
|
import { getSymbolTheme, highlightCode, theme } from "../../modes/theme/theme";
|
|
7
9
|
import type { TruncationMeta } from "../../tools/output-meta";
|
|
@@ -10,6 +12,7 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
10
12
|
import { truncateToVisualLines } from "./visual-truncate";
|
|
11
13
|
|
|
12
14
|
const PREVIEW_LINES = 20;
|
|
15
|
+
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
13
16
|
|
|
14
17
|
export class PythonExecutionComponent extends Container {
|
|
15
18
|
#outputLines: string[] = [];
|
|
@@ -70,11 +73,13 @@ export class PythonExecutionComponent extends Container {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
appendOutput(chunk: string): void {
|
|
73
|
-
const clean =
|
|
76
|
+
const clean = sanitizeText(chunk);
|
|
74
77
|
|
|
75
|
-
const newLines = clean.split("\n");
|
|
78
|
+
const newLines = clean.split("\n").map(line => this.#clampDisplayLine(line));
|
|
76
79
|
if (this.#outputLines.length > 0 && newLines.length > 0) {
|
|
77
|
-
this.#outputLines[this.#outputLines.length - 1]
|
|
80
|
+
this.#outputLines[this.#outputLines.length - 1] = this.#clampDisplayLine(
|
|
81
|
+
`${this.#outputLines[this.#outputLines.length - 1]}${newLines[0]}`,
|
|
82
|
+
);
|
|
78
83
|
this.#outputLines.push(...newLines.slice(1));
|
|
79
84
|
} else {
|
|
80
85
|
this.#outputLines.push(...newLines);
|
|
@@ -168,13 +173,17 @@ export class PythonExecutionComponent extends Container {
|
|
|
168
173
|
}
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
#
|
|
172
|
-
|
|
176
|
+
#clampDisplayLine(line: string): string {
|
|
177
|
+
if (line.length <= MAX_DISPLAY_LINE_CHARS) {
|
|
178
|
+
return line;
|
|
179
|
+
}
|
|
180
|
+
const omitted = line.length - MAX_DISPLAY_LINE_CHARS;
|
|
181
|
+
return `${line.slice(0, MAX_DISPLAY_LINE_CHARS)}… [${omitted} chars omitted]`;
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
#setOutput(output: string): void {
|
|
176
|
-
const clean =
|
|
177
|
-
this.#outputLines = clean ? clean.split("\n") : [];
|
|
185
|
+
const clean = sanitizeText(output);
|
|
186
|
+
this.#outputLines = clean ? clean.split("\n").map(line => this.#clampDisplayLine(line)) : [];
|
|
178
187
|
}
|
|
179
188
|
|
|
180
189
|
getOutput(): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
4
5
|
type Component,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
Text,
|
|
13
14
|
type TUI,
|
|
14
15
|
} from "@oh-my-pi/pi-tui";
|
|
15
|
-
import { logger
|
|
16
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
16
17
|
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
17
18
|
import type { Theme } from "../../modes/theme/theme";
|
|
18
19
|
import { theme } from "../../modes/theme/theme";
|