@pasko70/pibo 1.10.0 → 1.11.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/dist/apps/chat/chat-request-normalizers.js +7 -0
- package/dist/apps/chat/data/chat-data-mappers.js +2 -0
- package/dist/apps/chat/loop-api.js +11 -6
- package/dist/apps/chat/web-app.js +30 -10
- package/dist/apps/chat-ui/assets/{dist-wE9nop9V.js → dist-0E9FVJ6k.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DlATLa-U.js → dist-BvqC0hRM.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DUlaXAk7.js → dist-C2BRHNXr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-nOLTkZrJ.js → dist-C57jNmjf.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Y-AA2omI.js → dist-CBL8UXjd.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D-cxLQO1.js → dist-CI-LPHjw.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-LHRs1Nhr.js → dist-CMRFkfl5.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CS7wdk0Z.js → dist-CpZPhD2y.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BwKObYnX.js → dist-CuCZ-3_K.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-GdEM8UW1.js → dist-CzC5kPWJ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CKtT8YGm.js → dist-D9hLnn1T.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BEvjPUor.js +173 -0
- package/dist/apps/chat-ui/assets/{index-C0x9nEcf.css → index-DNeE4HrG.css} +1 -1
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-BLZRi4Ka.js +41 -0
- package/dist/apps/chat-vscode-web/assets/index-Bf2JvJ9z.css +2 -0
- package/dist/apps/chat-vscode-web/index.html +2 -2
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.11.0.vsix +0 -0
- package/dist/auth/better-auth.js +49 -1
- package/dist/auth/cli.js +220 -0
- package/dist/auth/machine-keys.js +258 -0
- package/dist/auth/machine-session.js +123 -0
- package/dist/bin/pibo.js +0 -0
- package/dist/bin/rg.js +0 -0
- package/dist/cli-session/localSessionSource.js +34 -12
- package/dist/cli.js +6 -0
- package/dist/compute/cli.js +7 -0
- package/dist/compute/resource-health.js +61 -4
- package/dist/config/config.js +5 -0
- package/dist/core/events.js +6 -1
- package/dist/core/routed-session.js +67 -4
- package/dist/core/session-router.js +40 -6
- package/dist/data/ingest-service.js +3 -1
- package/dist/data/schema.js +4 -0
- package/dist/debug/index.js +4 -0
- package/dist/debug/trace-status.js +25 -0
- package/dist/debug/trace.js +51 -17
- package/dist/index.js +1 -0
- package/dist/loops/accounting.js +19 -0
- package/dist/loops/cli.js +22 -6
- package/dist/loops/prompts.js +4 -1
- package/dist/loops/service.js +97 -8
- package/dist/loops/store.js +128 -17
- package/dist/loops/tools.js +24 -7
- package/dist/reliability/store.js +34 -7
- package/dist/resources/cli.js +15 -0
- package/dist/resources/lifecycle.js +28 -4
- package/dist/resources/reaper.js +1 -0
- package/dist/runs/lifecycle.js +59 -0
- package/dist/runs/registry.js +47 -1
- package/dist/runs/tools.js +31 -13
- package/dist/session-ui/terminalRows.js +66 -2
- package/dist/shared/trace-async-agent-runs.js +4 -4
- package/dist/shared/trace-event-projection.js +59 -19
- package/dist/shared/trace-nodes.js +5 -0
- package/dist/shared/trace-page-merge.js +15 -0
- package/dist/shared/trace-run-notifications.js +3 -1
- package/dist/signals/projector.js +16 -3
- package/dist/tools/browser-pool.js +50 -0
- package/dist/tools/browser-use-leases.js +12 -8
- package/dist/tools/guides.js +8 -1
- package/dist/tools/index.js +1 -0
- package/package.json +2 -1
- package/skills/builtin/loop/SKILL.md +12 -3
- package/dist/apps/chat-ui/assets/index-8W_yMHQI.js +0 -173
- package/dist/apps/chat-vscode-web/assets/index-B5QK07zO.css +0 -2
- package/dist/apps/chat-vscode-web/assets/index-BAMxIaI_.js +0 -41
package/dist/loops/store.js
CHANGED
|
@@ -75,13 +75,37 @@ function resourceMetadataJson(resources) {
|
|
|
75
75
|
const normalized = normalizeLoopResourceMetadata(resources);
|
|
76
76
|
return normalized ? JSON.stringify(normalized) : null;
|
|
77
77
|
}
|
|
78
|
+
function parseRunAccounting(json) {
|
|
79
|
+
if (!json)
|
|
80
|
+
return undefined;
|
|
81
|
+
try {
|
|
82
|
+
const value = JSON.parse(json);
|
|
83
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function runAccountingJson(accounting) { return accounting ? JSON.stringify(accounting) : null; }
|
|
90
|
+
function normalizeJobState(state, mode, enabled, createdAt) {
|
|
91
|
+
if (mode !== 'goal')
|
|
92
|
+
return state;
|
|
93
|
+
const activeTimeSeconds = Math.max(0, Math.floor(state.activeTimeSeconds ?? state.timeUsedSeconds ?? 0));
|
|
94
|
+
const goalStartedAt = state.goalStartedAt ?? (enabled || (state.completedIterations ?? 0) > 0 || (state.tokensUsed ?? 0) > 0 || (state.goalStatus !== undefined && state.goalStatus !== 'paused') ? createdAt : undefined);
|
|
95
|
+
const normalized = { ...state, activeTimeSeconds, ...(goalStartedAt ? { goalStartedAt } : {}) };
|
|
96
|
+
delete normalized.timeUsedSeconds;
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
78
99
|
function jobFromRow(row) {
|
|
79
100
|
const resources = parseResourceMetadata(row.resource_json);
|
|
80
|
-
|
|
101
|
+
const mode = normalizeLoopMode(row.loop_mode, 'ralph');
|
|
102
|
+
const enabled = row.enabled === 1;
|
|
103
|
+
return { id: row.id, mode, name: row.name, description: row.description ?? undefined, enabled, target: parseLoopTarget(row.target_json), profile: row.profile, prompt: row.prompt, maxIterations: row.max_iterations ?? undefined, tokenBudget: row.token_budget ?? undefined, tokenReserve: row.token_reserve ?? undefined, stopPolicy: parseStopPolicy(row.stop_policy_json), ...parseRuntimeOptions(row.runtime_options_json), ...(resources ? { resources } : {}), state: normalizeJobState(parseJson(row.state_json), mode, enabled, row.created_at), createdAt: row.created_at, updatedAt: row.updated_at };
|
|
81
104
|
}
|
|
82
105
|
function runFromRow(row) {
|
|
83
106
|
const resources = parseResourceMetadata(row.resource_json);
|
|
84
|
-
|
|
107
|
+
const accounting = parseRunAccounting(row.accounting_json);
|
|
108
|
+
return { id: row.id, jobId: row.job_id, piboSessionId: row.pibo_session_id ?? undefined, status: row.status, reason: row.reason ?? undefined, error: row.error ?? undefined, startedAt: row.started_at ?? undefined, completedAt: row.completed_at ?? undefined, ...(accounting ? { accounting } : {}), ...(resources ? { resources } : {}), createdAt: row.created_at, updatedAt: row.updated_at };
|
|
85
109
|
}
|
|
86
110
|
function mergeResourceMetadata(jobResources, runResources) {
|
|
87
111
|
if (!jobResources && !runResources)
|
|
@@ -106,6 +130,13 @@ function normalizeTokenBudget(value) {
|
|
|
106
130
|
throw new Error('tokenBudget must be a positive integer');
|
|
107
131
|
return value;
|
|
108
132
|
}
|
|
133
|
+
function normalizeTokenReserve(value) {
|
|
134
|
+
if (value === undefined)
|
|
135
|
+
return undefined;
|
|
136
|
+
if (!Number.isInteger(value) || value < 0)
|
|
137
|
+
throw new Error('tokenReserve must be a non-negative integer');
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
109
140
|
function goalStatus(job) {
|
|
110
141
|
if (job.mode !== 'goal')
|
|
111
142
|
return undefined;
|
|
@@ -213,10 +244,15 @@ function validateJobInput(input) {
|
|
|
213
244
|
throw new Error('prompt is required');
|
|
214
245
|
if (input.target.kind === 'room' && !input.target.roomId.trim())
|
|
215
246
|
throw new Error('target.roomId is required');
|
|
216
|
-
if (input.mode === 'ralph' && input.tokenBudget !== undefined)
|
|
217
|
-
throw new Error('tokenBudget
|
|
247
|
+
if (input.mode === 'ralph' && (input.tokenBudget !== undefined || input.tokenReserve !== undefined))
|
|
248
|
+
throw new Error('tokenBudget and tokenReserve are only available for goal mode');
|
|
218
249
|
normalizeMaxIterations(input.maxIterations);
|
|
219
250
|
normalizeTokenBudget(input.tokenBudget);
|
|
251
|
+
normalizeTokenReserve(input.tokenReserve);
|
|
252
|
+
if (input.tokenReserve !== undefined && input.tokenBudget === undefined)
|
|
253
|
+
throw new Error('tokenReserve requires tokenBudget');
|
|
254
|
+
if (input.tokenReserve !== undefined && input.tokenBudget !== undefined && input.tokenReserve >= input.tokenBudget)
|
|
255
|
+
throw new Error('tokenReserve must be smaller than tokenBudget');
|
|
220
256
|
normalizeRuntimeOptions(input);
|
|
221
257
|
normalizeLoopStopPolicy(input.stopPolicy);
|
|
222
258
|
normalizeLoopResourceMetadata(input.resources);
|
|
@@ -246,10 +282,10 @@ export class PiboLoopStore {
|
|
|
246
282
|
const enabled = input.enabled === true;
|
|
247
283
|
const state = {
|
|
248
284
|
completedIterations: 0,
|
|
249
|
-
...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0,
|
|
285
|
+
...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: timestamp } : {}) } : {}),
|
|
250
286
|
...(input.initialPiboSessionId?.trim() ? { lastPiboSessionId: input.initialPiboSessionId.trim() } : {}),
|
|
251
287
|
};
|
|
252
|
-
const job = { id: mode === 'ralph' ? `ralph_${randomUUID()}` : `loop_${randomUUID()}`, mode, name: (input.name ?? defaultName(input.prompt)).trim(), description: input.description?.trim() || undefined, enabled, target, profile: input.profile, prompt: input.prompt, maxIterations: normalizeMaxIterations(input.maxIterations), tokenBudget: normalizeTokenBudget(input.tokenBudget), stopPolicy: normalizeLoopStopPolicy(input.stopPolicy), ...runtimeOptions, ...(resources ? { resources } : {}), state, createdAt: timestamp, updatedAt: timestamp };
|
|
288
|
+
const job = { id: mode === 'ralph' ? `ralph_${randomUUID()}` : `loop_${randomUUID()}`, mode, name: (input.name ?? defaultName(input.prompt)).trim(), description: input.description?.trim() || undefined, enabled, target, profile: input.profile, prompt: input.prompt, maxIterations: normalizeMaxIterations(input.maxIterations), tokenBudget: normalizeTokenBudget(input.tokenBudget), tokenReserve: normalizeTokenReserve(input.tokenReserve), stopPolicy: normalizeLoopStopPolicy(input.stopPolicy), ...runtimeOptions, ...(resources ? { resources } : {}), state, createdAt: timestamp, updatedAt: timestamp };
|
|
253
289
|
this.insertJob(job);
|
|
254
290
|
return this.getJob(job.id);
|
|
255
291
|
}
|
|
@@ -264,6 +300,10 @@ export class PiboLoopStore {
|
|
|
264
300
|
return undefined;
|
|
265
301
|
const timestamp = nowIso(now);
|
|
266
302
|
const state = { ...job.state, goalStatus: status, runningAt: job.state.runningAt };
|
|
303
|
+
if (job.state.runningAt)
|
|
304
|
+
delete state.goalEndedAt;
|
|
305
|
+
else
|
|
306
|
+
state.goalEndedAt = timestamp;
|
|
267
307
|
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, state_json = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(state), timestamp, id);
|
|
268
308
|
return this.getJob(id);
|
|
269
309
|
}
|
|
@@ -272,20 +312,65 @@ export class PiboLoopStore {
|
|
|
272
312
|
if (!job || job.mode !== 'goal')
|
|
273
313
|
return job;
|
|
274
314
|
const tokens = Math.max(0, Math.floor(input.tokens ?? 0));
|
|
275
|
-
const
|
|
315
|
+
const activeTimeSeconds = Math.max(0, Math.floor(input.activeTimeSeconds ?? 0));
|
|
276
316
|
const nextTokens = (job.state.tokensUsed ?? 0) + tokens;
|
|
277
317
|
const currentStatus = goalStatus(job) ?? 'active';
|
|
278
318
|
const budgetLimited = currentStatus === 'active' && job.tokenBudget !== undefined && nextTokens >= job.tokenBudget;
|
|
279
319
|
const state = {
|
|
280
320
|
...job.state,
|
|
281
321
|
tokensUsed: nextTokens,
|
|
282
|
-
|
|
322
|
+
activeTimeSeconds: (job.state.activeTimeSeconds ?? 0) + activeTimeSeconds,
|
|
283
323
|
goalStatus: budgetLimited ? 'budget_limited' : currentStatus,
|
|
284
324
|
};
|
|
285
325
|
const timestamp = nowIso(now);
|
|
326
|
+
if (budgetLimited && !job.state.runningAt)
|
|
327
|
+
state.goalEndedAt = timestamp;
|
|
286
328
|
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(budgetLimited ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, id);
|
|
287
329
|
return this.getJob(id);
|
|
288
330
|
}
|
|
331
|
+
recordGoalTurnUsage(id, runId, tokens, now = new Date()) {
|
|
332
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
333
|
+
try {
|
|
334
|
+
const job = this.recordGoalProgress(id, { tokens }, now);
|
|
335
|
+
if (job?.mode === 'goal') {
|
|
336
|
+
const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
|
|
337
|
+
if (row) {
|
|
338
|
+
const accounting = parseRunAccounting(row.accounting_json) ?? {};
|
|
339
|
+
const turnTokens = (accounting.tokensUsed ?? 0) + Math.max(0, Math.floor(tokens));
|
|
340
|
+
const budget = accounting.tokenBudget;
|
|
341
|
+
const before = accounting.tokensUsedBefore ?? 0;
|
|
342
|
+
const nextAccounting = { ...accounting, tokensUsed: turnTokens, ...(budget !== undefined ? { overshootTokens: Math.max(0, before + turnTokens - budget) } : {}) };
|
|
343
|
+
this.db.prepare('UPDATE pibo_ralph_runs SET accounting_json = ?, updated_at = ? WHERE id = ?').run(runAccountingJson(nextAccounting), nowIso(now), runId);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
this.db.exec('COMMIT');
|
|
347
|
+
return job;
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
this.db.exec('ROLLBACK');
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
recordGoalRunTime(id, runId, activeTimeSeconds, now = new Date()) {
|
|
355
|
+
const seconds = Math.max(0, Math.floor(activeTimeSeconds));
|
|
356
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
357
|
+
try {
|
|
358
|
+
const job = this.recordGoalProgress(id, { activeTimeSeconds: seconds }, now);
|
|
359
|
+
if (job?.mode === 'goal') {
|
|
360
|
+
const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
|
|
361
|
+
if (row) {
|
|
362
|
+
const accounting = { ...(parseRunAccounting(row.accounting_json) ?? {}), activeTimeSeconds: seconds };
|
|
363
|
+
this.db.prepare('UPDATE pibo_ralph_runs SET accounting_json = ?, updated_at = ? WHERE id = ?').run(runAccountingJson(accounting), nowIso(now), runId);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this.db.exec('COMMIT');
|
|
367
|
+
return job;
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
this.db.exec('ROLLBACK');
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
289
374
|
listJobs(input = {}) {
|
|
290
375
|
const clauses = [];
|
|
291
376
|
const values = [];
|
|
@@ -309,16 +394,19 @@ export class PiboLoopStore {
|
|
|
309
394
|
const enabled = patch.enabled ?? existing.enabled;
|
|
310
395
|
let state = mode === existing.mode
|
|
311
396
|
? { ...existing.state }
|
|
312
|
-
: { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0,
|
|
397
|
+
: { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: nowIso(now) } : {}) } : {}) };
|
|
313
398
|
if (mode === 'goal' && patch.enabled !== undefined) {
|
|
314
399
|
const currentGoalStatus = goalStatus({ mode, enabled: existing.enabled, state: existing.state }) ?? 'paused';
|
|
315
400
|
if (patch.enabled) {
|
|
316
401
|
if (currentGoalStatus === 'complete')
|
|
317
402
|
throw new Error('Completed goals cannot be restarted; create a new goal');
|
|
318
403
|
const nextBudget = hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget;
|
|
319
|
-
|
|
320
|
-
|
|
404
|
+
const nextReserve = hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve;
|
|
405
|
+
if (currentGoalStatus === 'budget_limited' && nextBudget !== undefined && (existing.state.tokensUsed ?? 0) + (nextReserve ?? 0) >= nextBudget)
|
|
406
|
+
throw new Error('Increase or clear the token budget, or lower the token reserve, before resuming this goal');
|
|
321
407
|
state.goalStatus = 'active';
|
|
408
|
+
state.goalStartedAt ??= nowIso(now);
|
|
409
|
+
delete state.goalEndedAt;
|
|
322
410
|
state.stopRequestedAt = undefined;
|
|
323
411
|
state.cancelRequestedAt = undefined;
|
|
324
412
|
}
|
|
@@ -326,7 +414,7 @@ export class PiboLoopStore {
|
|
|
326
414
|
state.goalStatus = currentGoalStatus === 'active' ? 'paused' : currentGoalStatus;
|
|
327
415
|
}
|
|
328
416
|
}
|
|
329
|
-
const next = { ...existing, mode, state, name: patch.name !== undefined ? patch.name.trim() : existing.name, description: patch.description !== undefined ? patch.description?.trim() || undefined : existing.description, enabled, target, profile: patch.profile ?? existing.profile, prompt: patch.prompt ?? existing.prompt, maxIterations: hasOwn(patch, 'maxIterations') ? normalizeMaxIterations(patch.maxIterations ?? undefined) : existing.maxIterations, tokenBudget: mode === 'ralph' ? undefined : hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget, stopPolicy, modelOverride: runtimeOptions.modelOverride, thinkingLevel: runtimeOptions.thinkingLevel, fastMode: runtimeOptions.fastMode, updatedAt: nowIso(now) };
|
|
417
|
+
const next = { ...existing, mode, state, name: patch.name !== undefined ? patch.name.trim() : existing.name, description: patch.description !== undefined ? patch.description?.trim() || undefined : existing.description, enabled, target, profile: patch.profile ?? existing.profile, prompt: patch.prompt ?? existing.prompt, maxIterations: hasOwn(patch, 'maxIterations') ? normalizeMaxIterations(patch.maxIterations ?? undefined) : existing.maxIterations, tokenBudget: mode === 'ralph' ? undefined : hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget, tokenReserve: mode === 'ralph' || (hasOwn(patch, 'tokenBudget') && patch.tokenBudget === null && !hasOwn(patch, 'tokenReserve')) ? undefined : hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve, stopPolicy, modelOverride: runtimeOptions.modelOverride, thinkingLevel: runtimeOptions.thinkingLevel, fastMode: runtimeOptions.fastMode, updatedAt: nowIso(now) };
|
|
330
418
|
validateJobInput(next);
|
|
331
419
|
this.writeJob(next);
|
|
332
420
|
return this.getJob(id);
|
|
@@ -418,7 +506,7 @@ export class PiboLoopStore {
|
|
|
418
506
|
const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
|
|
419
507
|
const terminalGoalStatus = job.mode === 'goal' && ['complete', 'blocked', 'budget_limited'].includes(goalStatus(job) ?? '');
|
|
420
508
|
const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
|
|
421
|
-
const state = { ...job.state, runningAt: undefined, completedIterations, lastRunAt: timestamp, lastRunId: input.runId, lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok', lastError: input.error, lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId, consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0, conditionStates: input.conditionStates ?? job.state.conditionStates, lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation };
|
|
509
|
+
const state = { ...job.state, runningAt: undefined, completedIterations, lastRunAt: timestamp, lastRunId: input.runId, lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok', lastError: input.error, lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId, consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0, conditionStates: input.conditionStates ?? job.state.conditionStates, lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation, ...(terminalGoalStatus ? { goalEndedAt: job.state.goalEndedAt ?? timestamp } : {}) };
|
|
422
510
|
this.db.prepare('UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = COALESCE(?, pibo_session_id), reason = ?, error = ?, completed_at = ?, updated_at = ? WHERE id = ?').run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, timestamp, timestamp, input.runId);
|
|
423
511
|
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(shouldDisable ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, job.id);
|
|
424
512
|
}
|
|
@@ -486,6 +574,15 @@ export class PiboLoopStore {
|
|
|
486
574
|
this.db.exec('COMMIT');
|
|
487
575
|
return undefined;
|
|
488
576
|
}
|
|
577
|
+
if (job.mode === 'goal' && job.tokenBudget !== undefined) {
|
|
578
|
+
const remaining = Math.max(0, job.tokenBudget - (job.state.tokensUsed ?? 0));
|
|
579
|
+
if (remaining <= (job.tokenReserve ?? 0)) {
|
|
580
|
+
const state = { ...job.state, goalStatus: 'budget_limited', goalEndedAt: timestamp };
|
|
581
|
+
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, state_json = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(state), timestamp, job.id);
|
|
582
|
+
this.db.exec('COMMIT');
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
489
586
|
const run = this.createRunLocked(job, timestamp);
|
|
490
587
|
const state = { ...job.state, runningAt: timestamp, lastRunAt: timestamp, lastRunId: run.id };
|
|
491
588
|
this.updateJobStateLocked(job.id, state, timestamp);
|
|
@@ -502,13 +599,15 @@ export class PiboLoopStore {
|
|
|
502
599
|
this.ensureJobColumn('loop_mode', "TEXT NOT NULL DEFAULT 'ralph'");
|
|
503
600
|
this.ensureJobColumn('max_iterations', 'INTEGER');
|
|
504
601
|
this.ensureJobColumn('token_budget', 'INTEGER');
|
|
602
|
+
this.ensureJobColumn('token_reserve', 'INTEGER');
|
|
505
603
|
this.ensureJobColumn('runtime_options_json', 'TEXT');
|
|
506
604
|
this.ensureJobColumn('stop_policy_json', 'TEXT');
|
|
507
605
|
this.ensureJobColumn('resource_json', 'TEXT');
|
|
508
606
|
this.ensureRunColumn('resource_json', 'TEXT');
|
|
607
|
+
this.ensureRunColumn('accounting_json', 'TEXT');
|
|
509
608
|
}
|
|
510
609
|
createFreshSchema() {
|
|
511
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS pibo_ralph_jobs (id TEXT PRIMARY KEY, loop_mode TEXT NOT NULL DEFAULT 'goal', name TEXT NOT NULL, description TEXT, enabled INTEGER NOT NULL, target_json TEXT NOT NULL, profile TEXT NOT NULL, prompt TEXT NOT NULL, max_iterations INTEGER, token_budget INTEGER, runtime_options_json TEXT, stop_policy_json TEXT, resource_json TEXT, state_json TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_jobs_enabled ON pibo_ralph_jobs(enabled, updated_at DESC); CREATE TABLE IF NOT EXISTS pibo_ralph_runs (id TEXT PRIMARY KEY, job_id TEXT NOT NULL, pibo_session_id TEXT, status TEXT NOT NULL, reason TEXT, error TEXT, resource_json TEXT, started_at TEXT, completed_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_runs_job_created ON pibo_ralph_runs(job_id, created_at DESC); CREATE TABLE IF NOT EXISTS pibo_ralph_run_facts (id TEXT PRIMARY KEY, job_id TEXT NOT NULL, run_id TEXT, pibo_session_id TEXT, type TEXT NOT NULL, source TEXT NOT NULL, payload_json TEXT NOT NULL, created_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_facts_job_created ON pibo_ralph_run_facts(job_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_facts_run_type ON pibo_ralph_run_facts(run_id, type, created_at DESC);`);
|
|
610
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS pibo_ralph_jobs (id TEXT PRIMARY KEY, loop_mode TEXT NOT NULL DEFAULT 'goal', name TEXT NOT NULL, description TEXT, enabled INTEGER NOT NULL, target_json TEXT NOT NULL, profile TEXT NOT NULL, prompt TEXT NOT NULL, max_iterations INTEGER, token_budget INTEGER, token_reserve INTEGER, runtime_options_json TEXT, stop_policy_json TEXT, resource_json TEXT, state_json TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_jobs_enabled ON pibo_ralph_jobs(enabled, updated_at DESC); CREATE TABLE IF NOT EXISTS pibo_ralph_runs (id TEXT PRIMARY KEY, job_id TEXT NOT NULL, pibo_session_id TEXT, status TEXT NOT NULL, reason TEXT, error TEXT, accounting_json TEXT, resource_json TEXT, started_at TEXT, completed_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_runs_job_created ON pibo_ralph_runs(job_id, created_at DESC); CREATE TABLE IF NOT EXISTS pibo_ralph_run_facts (id TEXT PRIMARY KEY, job_id TEXT NOT NULL, run_id TEXT, pibo_session_id TEXT, type TEXT NOT NULL, source TEXT NOT NULL, payload_json TEXT NOT NULL, created_at TEXT NOT NULL); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_facts_job_created ON pibo_ralph_run_facts(job_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_pibo_ralph_facts_run_type ON pibo_ralph_run_facts(run_id, type, created_at DESC);`);
|
|
512
611
|
}
|
|
513
612
|
ensureJobColumn(name, definition) {
|
|
514
613
|
const columns = this.tableColumns('pibo_ralph_jobs');
|
|
@@ -523,9 +622,21 @@ export class PiboLoopStore {
|
|
|
523
622
|
tableColumns(tableName) {
|
|
524
623
|
return new Set(this.db.prepare(`PRAGMA table_info(${tableName})`).all().map((column) => column.name));
|
|
525
624
|
}
|
|
526
|
-
insertJob(job) { this.db.prepare('INSERT INTO pibo_ralph_jobs (id, loop_mode, name, description, enabled, target_json, profile, prompt, max_iterations, token_budget, runtime_options_json, stop_policy_json, resource_json, state_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(job.id, job.mode, job.name, job.description ?? null, job.enabled ? 1 : 0, targetJson(job.target), job.profile, job.prompt, job.maxIterations ?? null, job.tokenBudget ?? null, runtimeOptionsJson(job), stopPolicyJson(job.stopPolicy), resourceMetadataJson(job.resources), JSON.stringify(job.state), job.createdAt, job.updatedAt); }
|
|
527
|
-
writeJob(job) { this.db.prepare('UPDATE pibo_ralph_jobs SET loop_mode = ?, name = ?, description = ?, enabled = ?, target_json = ?, profile = ?, prompt = ?, max_iterations = ?, token_budget = ?, runtime_options_json = ?, stop_policy_json = ?, resource_json = ?, state_json = ?, updated_at = ? WHERE id = ?').run(job.mode, job.name, job.description ?? null, job.enabled ? 1 : 0, targetJson(job.target), job.profile, job.prompt, job.maxIterations ?? null, job.tokenBudget ?? null, runtimeOptionsJson(job), stopPolicyJson(job.stopPolicy), resourceMetadataJson(job.resources), JSON.stringify(job.state), job.updatedAt, job.id); }
|
|
625
|
+
insertJob(job) { this.db.prepare('INSERT INTO pibo_ralph_jobs (id, loop_mode, name, description, enabled, target_json, profile, prompt, max_iterations, token_budget, token_reserve, runtime_options_json, stop_policy_json, resource_json, state_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(job.id, job.mode, job.name, job.description ?? null, job.enabled ? 1 : 0, targetJson(job.target), job.profile, job.prompt, job.maxIterations ?? null, job.tokenBudget ?? null, job.tokenReserve ?? null, runtimeOptionsJson(job), stopPolicyJson(job.stopPolicy), resourceMetadataJson(job.resources), JSON.stringify(job.state), job.createdAt, job.updatedAt); }
|
|
626
|
+
writeJob(job) { this.db.prepare('UPDATE pibo_ralph_jobs SET loop_mode = ?, name = ?, description = ?, enabled = ?, target_json = ?, profile = ?, prompt = ?, max_iterations = ?, token_budget = ?, token_reserve = ?, runtime_options_json = ?, stop_policy_json = ?, resource_json = ?, state_json = ?, updated_at = ? WHERE id = ?').run(job.mode, job.name, job.description ?? null, job.enabled ? 1 : 0, targetJson(job.target), job.profile, job.prompt, job.maxIterations ?? null, job.tokenBudget ?? null, job.tokenReserve ?? null, runtimeOptionsJson(job), stopPolicyJson(job.stopPolicy), resourceMetadataJson(job.resources), JSON.stringify(job.state), job.updatedAt, job.id); }
|
|
528
627
|
updateJobStateLocked(id, state, updatedAt) { this.db.prepare('UPDATE pibo_ralph_jobs SET state_json = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(state), updatedAt, id); }
|
|
529
|
-
createRunLocked(job, timestamp) {
|
|
628
|
+
createRunLocked(job, timestamp) {
|
|
629
|
+
const tokensUsedBefore = job.state.tokensUsed ?? 0;
|
|
630
|
+
const accounting = job.mode === 'goal' ? {
|
|
631
|
+
...(job.tokenBudget !== undefined ? { tokenBudget: job.tokenBudget, remainingTokensBefore: Math.max(0, job.tokenBudget - tokensUsedBefore) } : {}),
|
|
632
|
+
...(job.tokenReserve !== undefined ? { tokenReserve: job.tokenReserve } : {}),
|
|
633
|
+
tokensUsedBefore,
|
|
634
|
+
tokensUsed: 0,
|
|
635
|
+
overshootTokens: 0,
|
|
636
|
+
} : undefined;
|
|
637
|
+
const run = { id: job.mode === 'ralph' ? `rrun_${randomUUID()}` : `lrun_${randomUUID()}`, jobId: job.id, status: 'running', startedAt: timestamp, ...(accounting ? { accounting } : {}), ...(job.resources ? { resources: job.resources } : {}), createdAt: timestamp, updatedAt: timestamp };
|
|
638
|
+
this.db.prepare('INSERT INTO pibo_ralph_runs (id, job_id, pibo_session_id, status, reason, error, accounting_json, resource_json, started_at, completed_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(run.id, run.jobId, null, run.status, null, null, runAccountingJson(run.accounting), resourceMetadataJson(run.resources), run.startedAt ?? null, null, run.createdAt, run.updatedAt);
|
|
639
|
+
return run;
|
|
640
|
+
}
|
|
530
641
|
}
|
|
531
642
|
export function createDefaultPiboLoopStore(options = {}) { return new PiboLoopStore(options); }
|
package/dist/loops/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StringEnum, Type } from '@earendil-works/pi-ai';
|
|
2
2
|
import { defineTool } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import { goalActiveTimeSeconds, goalCanStartNextTurn, goalElapsedWallClockSeconds, goalRemainingTokens } from './accounting.js';
|
|
3
4
|
import { createDefaultPiboLoopStore } from './store.js';
|
|
4
5
|
export const PIBO_GOAL_TOOL_NAMES = ['get_goal', 'create_goal', 'update_goal'];
|
|
5
6
|
let configuredStorePath;
|
|
@@ -33,17 +34,30 @@ function positiveInteger(value, field) {
|
|
|
33
34
|
throw new Error(`${field} must be a positive integer`);
|
|
34
35
|
return value;
|
|
35
36
|
}
|
|
37
|
+
function nonNegativeInteger(value, field) {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (!Number.isInteger(value) || value < 0)
|
|
41
|
+
throw new Error(`${field} must be a non-negative integer`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
36
44
|
function goalPayload(job) {
|
|
37
|
-
const tokensUsed = job.state.tokensUsed ?? 0;
|
|
38
45
|
const tokenBudget = job.tokenBudget;
|
|
39
46
|
return {
|
|
40
47
|
goalId: job.id,
|
|
41
48
|
objective: job.prompt,
|
|
42
49
|
status: effectiveGoalStatus(job),
|
|
50
|
+
budgetType: tokenBudget === undefined ? 'unbounded' : 'soft',
|
|
43
51
|
tokenBudget: tokenBudget ?? null,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
tokenReserve: job.tokenReserve ?? 0,
|
|
53
|
+
tokensUsed: job.state.tokensUsed ?? 0,
|
|
54
|
+
remainingTokens: goalRemainingTokens(job) ?? null,
|
|
55
|
+
canStartNextTurn: goalCanStartNextTurn(job),
|
|
56
|
+
activeAgentTimeSeconds: goalActiveTimeSeconds(job),
|
|
57
|
+
elapsedWallClockSeconds: goalElapsedWallClockSeconds(job),
|
|
58
|
+
goalStartedAt: job.state.goalStartedAt ?? null,
|
|
59
|
+
goalEndedAt: job.state.goalEndedAt ?? null,
|
|
60
|
+
wallClockIncludesPausedTime: true,
|
|
47
61
|
};
|
|
48
62
|
}
|
|
49
63
|
function effectiveGoalStatus(job) {
|
|
@@ -64,7 +78,7 @@ function createGetGoalTool(context, options) {
|
|
|
64
78
|
return defineTool({
|
|
65
79
|
name: 'get_goal',
|
|
66
80
|
label: 'Get Goal',
|
|
67
|
-
description: 'Get the current goal for this Pibo Session, including
|
|
81
|
+
description: 'Get the current goal for this Pibo Session, including soft-budget risk, per-turn reserve, active agent time, and wall-clock elapsed time.',
|
|
68
82
|
promptSnippet: 'Use get_goal when you need the authoritative persisted status or accounting for the current Pibo Session goal.',
|
|
69
83
|
parameters: Type.Object({}),
|
|
70
84
|
async execute() {
|
|
@@ -89,7 +103,8 @@ function createCreateGoalTool(context, options) {
|
|
|
89
103
|
promptSnippet: 'Call create_goal only when the user or system explicitly requests a persistent goal. Do not infer a goal from an ordinary task.',
|
|
90
104
|
parameters: Type.Object({
|
|
91
105
|
objective: Type.String({ description: 'Concrete objective to pursue across automatic continuations.' }),
|
|
92
|
-
token_budget: Type.Optional(Type.Number({ description: 'Optional
|
|
106
|
+
token_budget: Type.Optional(Type.Number({ description: 'Optional soft token budget. Usage arrives after each model response, so the final turn can overshoot.' })),
|
|
107
|
+
token_reserve: Type.Optional(Type.Number({ description: 'Optional non-negative minimum remaining tokens required before Pibo starts another turn.' })),
|
|
93
108
|
}),
|
|
94
109
|
async execute(_toolCallId, params) {
|
|
95
110
|
try {
|
|
@@ -98,6 +113,7 @@ function createCreateGoalTool(context, options) {
|
|
|
98
113
|
if (!objective)
|
|
99
114
|
throw new Error('objective is required');
|
|
100
115
|
const tokenBudget = positiveInteger(params.token_budget, 'token_budget');
|
|
116
|
+
const tokenReserve = nonNegativeInteger(params.token_reserve, 'token_reserve');
|
|
101
117
|
return await withStore(options, (store) => {
|
|
102
118
|
const existing = store.getLatestGoalForSession(session.piboSessionId);
|
|
103
119
|
if (existing && effectiveGoalStatus(existing) !== 'complete') {
|
|
@@ -110,6 +126,7 @@ function createCreateGoalTool(context, options) {
|
|
|
110
126
|
profile: session.profileName,
|
|
111
127
|
prompt: objective,
|
|
112
128
|
tokenBudget,
|
|
129
|
+
tokenReserve,
|
|
113
130
|
initialPiboSessionId: session.piboSessionId,
|
|
114
131
|
});
|
|
115
132
|
return toolResult({ ok: true, goal: goalPayload(job) });
|
|
@@ -147,7 +164,7 @@ function createUpdateGoalTool(context, options) {
|
|
|
147
164
|
ok: true,
|
|
148
165
|
goal: goalPayload(job),
|
|
149
166
|
...(status === 'complete' && job.tokenBudget !== undefined
|
|
150
|
-
? { completionBudgetReport: `${job.state.tokensUsed ?? 0}/${job.tokenBudget} reported tokens consumed before the current model turn finishes` }
|
|
167
|
+
? { completionBudgetReport: `${job.state.tokensUsed ?? 0}/${job.tokenBudget} reported tokens consumed against a soft budget before the current model turn finishes` }
|
|
151
168
|
: {}),
|
|
152
169
|
});
|
|
153
170
|
});
|
|
@@ -30,6 +30,11 @@ function migratePiboRunControllerColumn(db) {
|
|
|
30
30
|
db.exec("DROP INDEX IF EXISTS idx_pibo_runs_" + ["o", "wner_updated"].join(""));
|
|
31
31
|
db.exec(`ALTER TABLE pibo_runs RENAME COLUMN ${legacyColumn} TO controller_pibo_session_id`);
|
|
32
32
|
}
|
|
33
|
+
function ensurePiboRunColumn(db, name, definition) {
|
|
34
|
+
const columns = sqliteTableColumns(db, "pibo_runs");
|
|
35
|
+
if (!columns.has(name))
|
|
36
|
+
db.exec(`ALTER TABLE pibo_runs ADD COLUMN ${name} ${definition}`);
|
|
37
|
+
}
|
|
33
38
|
export class PiboReliabilityStore {
|
|
34
39
|
db;
|
|
35
40
|
appendEventStatement;
|
|
@@ -135,13 +140,21 @@ export class PiboReliabilityStore {
|
|
|
135
140
|
completed_at TEXT,
|
|
136
141
|
job_id TEXT,
|
|
137
142
|
retryable INTEGER NOT NULL DEFAULT 0,
|
|
138
|
-
max_attempts INTEGER NOT NULL DEFAULT 1
|
|
143
|
+
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
144
|
+
timeout_ms INTEGER,
|
|
145
|
+
timeout_at TEXT,
|
|
146
|
+
timeout_phase TEXT,
|
|
147
|
+
service_warning TEXT
|
|
139
148
|
);
|
|
140
149
|
CREATE INDEX IF NOT EXISTS idx_pibo_runs_controller_updated
|
|
141
150
|
ON pibo_runs(controller_pibo_session_id, updated_at);
|
|
142
151
|
CREATE INDEX IF NOT EXISTS idx_pibo_runs_status
|
|
143
152
|
ON pibo_runs(status);
|
|
144
153
|
`);
|
|
154
|
+
ensurePiboRunColumn(this.db, "timeout_ms", "INTEGER");
|
|
155
|
+
ensurePiboRunColumn(this.db, "timeout_at", "TEXT");
|
|
156
|
+
ensurePiboRunColumn(this.db, "timeout_phase", "TEXT");
|
|
157
|
+
ensurePiboRunColumn(this.db, "service_warning", "TEXT");
|
|
145
158
|
this.appendEventStatement = this.db.prepare(`
|
|
146
159
|
INSERT INTO pibo_event_stream (topic, key, event_id, idempotency_key, created_at, retention_class, payload_json)
|
|
147
160
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -466,6 +479,7 @@ export class PiboReliabilityStore {
|
|
|
466
479
|
const timestamp = now();
|
|
467
480
|
const runId = input.runId ?? `run_${randomUUID()}`;
|
|
468
481
|
const maxAttempts = Math.max(1, input.maxAttempts ?? 1);
|
|
482
|
+
const timeoutAt = input.timeoutMs === undefined ? undefined : new Date(Date.parse(timestamp) + input.timeoutMs).toISOString();
|
|
469
483
|
const job = this.enqueue({
|
|
470
484
|
queue: "runs",
|
|
471
485
|
payload: {
|
|
@@ -473,6 +487,7 @@ export class PiboReliabilityStore {
|
|
|
473
487
|
controllerPiboSessionId: input.controllerPiboSessionId,
|
|
474
488
|
toolName: input.toolName,
|
|
475
489
|
params: input.params,
|
|
490
|
+
timeoutMs: input.timeoutMs,
|
|
476
491
|
},
|
|
477
492
|
maxAttempts,
|
|
478
493
|
});
|
|
@@ -481,10 +496,10 @@ export class PiboReliabilityStore {
|
|
|
481
496
|
INSERT INTO pibo_runs (
|
|
482
497
|
run_id, kind, controller_pibo_session_id, status, completion_policy, consumed, tool_name,
|
|
483
498
|
summary, result_json, error, notified_status, acknowledged_status, created_at, updated_at,
|
|
484
|
-
completed_at, job_id, retryable, max_attempts
|
|
485
|
-
) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?)
|
|
499
|
+
completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning
|
|
500
|
+
) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?)
|
|
486
501
|
`)
|
|
487
|
-
.run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts);
|
|
502
|
+
.run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null);
|
|
488
503
|
this.claimJob(job.jobId, `run-registry:${process.pid}`, 24 * 60 * 60 * 1000);
|
|
489
504
|
return this.requireRun(runId);
|
|
490
505
|
}
|
|
@@ -508,10 +523,14 @@ export class PiboReliabilityStore {
|
|
|
508
523
|
completed_at = ?,
|
|
509
524
|
job_id = ?,
|
|
510
525
|
retryable = ?,
|
|
511
|
-
max_attempts =
|
|
526
|
+
max_attempts = ?,
|
|
527
|
+
timeout_ms = ?,
|
|
528
|
+
timeout_at = ?,
|
|
529
|
+
timeout_phase = ?,
|
|
530
|
+
service_warning = ?
|
|
512
531
|
WHERE run_id = ?
|
|
513
532
|
`)
|
|
514
|
-
.run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, runId);
|
|
533
|
+
.run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, next.timeoutMs ?? null, next.timeoutAt ?? null, next.timeoutPhase ?? null, next.serviceWarning ?? null, runId);
|
|
515
534
|
return this.requireRun(runId);
|
|
516
535
|
}
|
|
517
536
|
getRun(runId) {
|
|
@@ -535,7 +554,7 @@ export class PiboReliabilityStore {
|
|
|
535
554
|
}
|
|
536
555
|
pruneRuns(input) {
|
|
537
556
|
const rows = this.db
|
|
538
|
-
.prepare("SELECT * FROM pibo_runs WHERE status IN ('completed', 'failed', 'cancelled') AND completed_at IS NOT NULL")
|
|
557
|
+
.prepare("SELECT * FROM pibo_runs WHERE status IN ('completed', 'failed', 'timed_out', 'cancelled') AND completed_at IS NOT NULL")
|
|
539
558
|
.all();
|
|
540
559
|
const ids = rows
|
|
541
560
|
.map(runFromRow)
|
|
@@ -760,6 +779,14 @@ function runFromRow(row) {
|
|
|
760
779
|
output.completedAt = row.completed_at;
|
|
761
780
|
if (row.job_id)
|
|
762
781
|
output.jobId = row.job_id;
|
|
782
|
+
if (row.timeout_ms !== null)
|
|
783
|
+
output.timeoutMs = row.timeout_ms;
|
|
784
|
+
if (row.timeout_at)
|
|
785
|
+
output.timeoutAt = row.timeout_at;
|
|
786
|
+
if (row.timeout_phase === "startup" || row.timeout_phase === "lifetime")
|
|
787
|
+
output.timeoutPhase = row.timeout_phase;
|
|
788
|
+
if (row.service_warning)
|
|
789
|
+
output.serviceWarning = row.service_warning;
|
|
763
790
|
return output;
|
|
764
791
|
}
|
|
765
792
|
function retryDelayMs(attempts, input) {
|
package/dist/resources/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import { getComputeResourceHealth } from "../compute/resource-health.js";
|
|
3
4
|
import { renderComputeResourceHealthText } from "../compute/cli.js";
|
|
@@ -17,6 +18,15 @@ function parsePidList(value) {
|
|
|
17
18
|
throw new Error("PIDs must be positive integers separated by commas");
|
|
18
19
|
return [...new Set(pids)];
|
|
19
20
|
}
|
|
21
|
+
function parseAbsolutePathList(value) {
|
|
22
|
+
const paths = value.split(",").map((item) => item.trim());
|
|
23
|
+
if (paths.some((path) => !path || !isAbsolute(path)))
|
|
24
|
+
throw new Error("Browser user-data directories must be absolute paths separated by commas");
|
|
25
|
+
return [...new Set(paths.map((path) => resolve(path)))];
|
|
26
|
+
}
|
|
27
|
+
function shellQuote(value) {
|
|
28
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
29
|
+
}
|
|
20
30
|
export function renderResourceLeasesText(leases) {
|
|
21
31
|
if (leases.length === 0)
|
|
22
32
|
return "No active managed browser-pool leases.\nNext: pibo resources status";
|
|
@@ -53,6 +63,10 @@ export function renderResourceReapText(value) {
|
|
|
53
63
|
];
|
|
54
64
|
if (plan.options.includeDev)
|
|
55
65
|
args.push("--include-dev");
|
|
66
|
+
if (plan.options.exemptBrowserPids.length > 0)
|
|
67
|
+
args.push(`--exempt-browser-pids ${plan.options.exemptBrowserPids.join(",")}`);
|
|
68
|
+
if (plan.options.exemptBrowserUserDataDirs.length > 0)
|
|
69
|
+
args.push(`--exempt-browser-user-data-dirs ${shellQuote(plan.options.exemptBrowserUserDataDirs.join(","))}`);
|
|
56
70
|
if (plan.options.browserPoolRoot)
|
|
57
71
|
args.push(`--browser-pool-root ${plan.options.browserPoolRoot}`);
|
|
58
72
|
if (plan.options.browserUseHome)
|
|
@@ -118,6 +132,7 @@ export async function runResourcesCli(argv) {
|
|
|
118
132
|
.option("--idle-timeout-minutes <n>", "Select browser pools idle for this many minutes", parseNonNegativeNumber, 10)
|
|
119
133
|
.option("--unmanaged-browser-grace-minutes <n>", "Select unmanaged Chromium older than this many minutes", parseNonNegativeNumber, 10)
|
|
120
134
|
.option("--exempt-browser-pids <list>", "Comma-separated browser PIDs or process groups to preserve", parsePidList)
|
|
135
|
+
.option("--exempt-browser-user-data-dirs <list>", "Comma-separated absolute browser profile directories to preserve", parseAbsolutePathList)
|
|
121
136
|
.option("--browser-pool-root <path>", "Browser pool root directory to scan")
|
|
122
137
|
.option("--browser-use-home <path>", "Browser-use home directory to scan for stale CDP files")
|
|
123
138
|
.option("--json", "Print machine-readable cleanup plan or result")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { readdir, readFile, rm } from "node:fs/promises";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { applyComputeWorkerReapPlan, buildComputeWorkerReapPlan, planReapWorkers, } from "../compute/docker.js";
|
|
7
7
|
import { defaultBrowserPoolRoot, defaultBrowserUseHome, getComputeResourceHealth, parseProcessList, } from "../compute/resource-health.js";
|
|
@@ -60,9 +60,14 @@ export async function planResourceReap(options = {}) {
|
|
|
60
60
|
collectManagedBrowserPools(resolved.browserPoolRoot),
|
|
61
61
|
planStaleCdpFiles(resolved.browserUseHome),
|
|
62
62
|
planComputeReapSafely({ includeDev: resolved.includeDev, maxAgeMinutes: resolved.maxAgeMinutes, now }),
|
|
63
|
-
getComputeResourceHealth({
|
|
63
|
+
getComputeResourceHealth({
|
|
64
|
+
now,
|
|
65
|
+
browserPoolRoot: resolved.browserPoolRoot,
|
|
66
|
+
browserUseHome: resolved.browserUseHome,
|
|
67
|
+
exemptBrowserUserDataDirs: [],
|
|
68
|
+
}),
|
|
64
69
|
]);
|
|
65
|
-
const unmanagedBrowsers = buildUnmanagedBrowserPlanItems(health.browserProcesses.unassignedMainProcessDetails, resolved.unmanagedBrowserGraceMinutes, new Set(resolved.exemptBrowserPids));
|
|
70
|
+
const unmanagedBrowsers = buildUnmanagedBrowserPlanItems(health.browserProcesses.unassignedMainProcessDetails, resolved.unmanagedBrowserGraceMinutes, new Set(resolved.exemptBrowserPids), new Set(resolved.exemptBrowserUserDataDirs));
|
|
66
71
|
return buildResourceReapPlan({ now, options: resolved, records, staleFiles, unmanagedBrowsers, compute });
|
|
67
72
|
}
|
|
68
73
|
export function buildResourceReapPlan(input) {
|
|
@@ -126,9 +131,10 @@ function resolveReapOptions(options) {
|
|
|
126
131
|
browserPoolRoot: options.browserPoolRoot ?? defaultBrowserPoolRoot(),
|
|
127
132
|
browserUseHome: options.browserUseHome ?? defaultBrowserUseHome(),
|
|
128
133
|
exemptBrowserPids: options.exemptBrowserPids ?? readExemptBrowserPids(),
|
|
134
|
+
exemptBrowserUserDataDirs: normalizeBrowserUserDataDirs(options.exemptBrowserUserDataDirs ?? readExemptBrowserUserDataDirs()),
|
|
129
135
|
};
|
|
130
136
|
}
|
|
131
|
-
export function buildUnmanagedBrowserPlanItems(processes, graceMinutes, exemptPids = new Set()) {
|
|
137
|
+
export function buildUnmanagedBrowserPlanItems(processes, graceMinutes, exemptPids = new Set(), exemptUserDataDirs = new Set()) {
|
|
132
138
|
const graceSeconds = graceMinutes * 60;
|
|
133
139
|
return processes.map((process) => {
|
|
134
140
|
let action = "terminate";
|
|
@@ -141,6 +147,10 @@ export function buildUnmanagedBrowserPlanItems(processes, graceMinutes, exemptPi
|
|
|
141
147
|
action = "skip";
|
|
142
148
|
reason = "explicitly exempted pid or process group";
|
|
143
149
|
}
|
|
150
|
+
else if (browserUserDataDirIsExempt(process.userDataDir, exemptUserDataDirs)) {
|
|
151
|
+
action = "skip";
|
|
152
|
+
reason = "explicitly exempted browser user-data-dir";
|
|
153
|
+
}
|
|
144
154
|
else if (process.elapsedSeconds !== undefined && process.elapsedSeconds < graceSeconds) {
|
|
145
155
|
action = "skip";
|
|
146
156
|
reason = `process age ${process.elapsedSeconds}s is within ${graceSeconds}s grace period`;
|
|
@@ -211,6 +221,20 @@ function readExemptBrowserPids() {
|
|
|
211
221
|
.map((value) => Number.parseInt(value.trim(), 10))
|
|
212
222
|
.filter((value) => Number.isInteger(value) && value > 0);
|
|
213
223
|
}
|
|
224
|
+
function readExemptBrowserUserDataDirs() {
|
|
225
|
+
return (process.env.PIBO_RESOURCE_REAPER_EXEMPT_BROWSER_USER_DATA_DIRS ?? "").split(",");
|
|
226
|
+
}
|
|
227
|
+
function normalizeBrowserUserDataDirs(values) {
|
|
228
|
+
return [...new Set([...values]
|
|
229
|
+
.map((value) => value.trim())
|
|
230
|
+
.filter((value) => value.length > 0 && isAbsolute(value))
|
|
231
|
+
.map((value) => resolve(value)))];
|
|
232
|
+
}
|
|
233
|
+
function browserUserDataDirIsExempt(userDataDir, exemptions) {
|
|
234
|
+
if (!userDataDir || !isAbsolute(userDataDir))
|
|
235
|
+
return false;
|
|
236
|
+
return exemptions.has(resolve(userDataDir));
|
|
237
|
+
}
|
|
214
238
|
function buildBrowserReapPlanItem(record, now, idleTimeoutMinutes) {
|
|
215
239
|
const { state } = record;
|
|
216
240
|
let action = "skip";
|
package/dist/resources/reaper.js
CHANGED
|
@@ -76,6 +76,7 @@ export class ResourceReaperService {
|
|
|
76
76
|
browserPoolRoot: this.options.browserPoolRoot,
|
|
77
77
|
browserUseHome: this.options.browserUseHome,
|
|
78
78
|
exemptBrowserPids: this.options.exemptBrowserPids,
|
|
79
|
+
exemptBrowserUserDataDirs: this.options.exemptBrowserUserDataDirs,
|
|
79
80
|
now: runAt,
|
|
80
81
|
});
|
|
81
82
|
result = await this.apply(plan);
|