@pasko70/pibo 1.9.13 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +183 -183
  2. package/context/codex-base-prompt.md +148 -148
  3. package/context/compute-worker.md +23 -23
  4. package/context/pibo-compaction-prompt.md +100 -100
  5. package/context/pibo-native-tooling.md +18 -18
  6. package/context/pibo-system-prompt.md +77 -77
  7. package/dist/apps/chat/agent-profiles.js +2 -2
  8. package/dist/apps/chat/agent-store.js +263 -250
  9. package/dist/apps/chat/chat-request-normalizers.js +17 -0
  10. package/dist/apps/chat/data/chat-data-mappers.js +2 -0
  11. package/dist/apps/chat/data/project-service.js +168 -168
  12. package/dist/apps/chat/data/read-state-service.js +18 -18
  13. package/dist/apps/chat/data/session-query-service.js +25 -25
  14. package/dist/apps/chat/data/timeline-query-service.js +19 -19
  15. package/dist/apps/chat/loop-api.js +181 -0
  16. package/dist/apps/chat/static-assets.js +854 -854
  17. package/dist/apps/chat/web-app.js +36 -16
  18. package/dist/apps/chat/workflow-manual-trigger-runtime.js +149 -46
  19. package/dist/apps/chat/workflow-persistence.js +255 -255
  20. package/dist/apps/chat-ui/assets/{dist-BZ2eTC4f.js → dist-3Sts0Afa.js} +1 -1
  21. package/dist/apps/chat-ui/assets/{dist-BHa-kcGl.js → dist-4NlLjLs7.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-yCYNNb5d.js → dist-BA6mhAec.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-D811wJeV.js → dist-BPl-fzRB.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-Dq4GxJi3.js → dist-BPnn9e2B.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-CzE6k3F3.js → dist-BpfBSMzK.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-CnVsqwSG.js → dist-Cvx5M97R.js} +1 -1
  27. package/dist/apps/chat-ui/assets/{dist-WyXdYl-w.js → dist-DnXWNQRm.js} +1 -1
  28. package/dist/apps/chat-ui/assets/{dist-BNMu92bb.js → dist-hU-2oAb6.js} +1 -1
  29. package/dist/apps/chat-ui/assets/{dist-BCB6zezO.js → dist-r31x5Fcc.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-HqTN67dc.js → dist-uF6UXzI7.js} +1 -1
  31. package/dist/apps/chat-ui/assets/index-CaTGYBOS.js +173 -0
  32. package/dist/apps/chat-ui/assets/{index-C0x9nEcf.css → index-al8DeEjA.css} +1 -1
  33. package/dist/apps/chat-ui/index.html +18 -18
  34. package/dist/apps/chat-ui/manifest.webmanifest +25 -25
  35. package/dist/apps/chat-ui/sw.js +47 -47
  36. package/dist/apps/chat-vscode-web/assets/index-CK4SMZuu.js +41 -0
  37. package/dist/apps/chat-vscode-web/index.html +13 -13
  38. package/dist/apps/cli-ui/cliSessionsCommand.js +23 -23
  39. package/dist/apps/context-files-ui/index.html +11 -11
  40. package/dist/cli-session/localSessionSource.js +34 -12
  41. package/dist/cli.js +61 -44
  42. package/dist/compute/cli.js +54 -54
  43. package/dist/compute/resource-health.js +35 -2
  44. package/dist/core/events.js +6 -1
  45. package/dist/core/routed-session.js +97 -5
  46. package/dist/core/runtime.js +6 -1
  47. package/dist/core/session-router.js +27 -8
  48. package/dist/cron/cli.js +15 -15
  49. package/dist/cron/store.js +49 -49
  50. package/dist/data/cli.js +23 -23
  51. package/dist/data/event-log.js +23 -23
  52. package/dist/data/ingest-service.js +3 -1
  53. package/dist/data/message-store.js +27 -27
  54. package/dist/data/navigation-store.js +9 -9
  55. package/dist/data/observation-store.js +4 -4
  56. package/dist/data/payload-store.js +17 -17
  57. package/dist/data/schema.js +433 -433
  58. package/dist/data/session-store.js +4 -4
  59. package/dist/data/telemetry-queries.js +54 -54
  60. package/dist/data/telemetry.js +197 -197
  61. package/dist/debug/events.js +12 -12
  62. package/dist/debug/failures.js +6 -6
  63. package/dist/debug/index.js +231 -227
  64. package/dist/debug/messages.js +6 -6
  65. package/dist/debug/pty.js +124 -124
  66. package/dist/debug/session.js +29 -29
  67. package/dist/debug/tools.js +5 -5
  68. package/dist/debug/trace.js +42 -7
  69. package/dist/debug/web-snapshot-browser-scripts.js +294 -294
  70. package/dist/debug/web-streaming-browser-library.js +925 -925
  71. package/dist/debug/web-streaming-browser-scripts.js +232 -232
  72. package/dist/debug/web-streaming-provider-telemetry.js +4 -4
  73. package/dist/debug/web.js +93 -93
  74. package/dist/gateway/cli.js +19 -19
  75. package/dist/gateway/server.js +4 -2
  76. package/dist/gateway/web.js +2 -2
  77. package/dist/index.js +1 -0
  78. package/dist/loops/accounting.js +19 -0
  79. package/dist/loops/channel.js +8 -0
  80. package/dist/loops/cli.js +224 -0
  81. package/dist/loops/plugin.js +16 -0
  82. package/dist/loops/prompts.js +86 -0
  83. package/dist/loops/service.js +446 -0
  84. package/dist/loops/stopping.js +170 -0
  85. package/dist/loops/store.js +642 -0
  86. package/dist/loops/templates.js +232 -0
  87. package/dist/loops/tools.js +184 -0
  88. package/dist/loops/types.js +1 -0
  89. package/dist/mcp/config-command.js +53 -53
  90. package/dist/mcp/index.js +21 -21
  91. package/dist/mcp/registry.js +11 -11
  92. package/dist/pi-packages/cli.js +11 -11
  93. package/dist/plugins/builtin.js +8 -0
  94. package/dist/plugins/context-files-store.js +110 -110
  95. package/dist/plugins/context-files.js +4 -4
  96. package/dist/plugins/registry.js +23 -12
  97. package/dist/ralph/cli.js +18 -18
  98. package/dist/ralph/templates.js +140 -140
  99. package/dist/reliability/store.js +256 -229
  100. package/dist/runs/lifecycle.js +59 -0
  101. package/dist/runs/registry.js +47 -1
  102. package/dist/runs/tools.js +31 -13
  103. package/dist/session-ui/terminalRows.js +66 -2
  104. package/dist/sessions/pibo-data-store.js +16 -16
  105. package/dist/sessions/sqlite-store.js +53 -53
  106. package/dist/setup/cli.js +58 -58
  107. package/dist/shared/trace-async-agent-runs.js +4 -4
  108. package/dist/shared/trace-event-projection.js +59 -19
  109. package/dist/shared/trace-nodes.js +5 -0
  110. package/dist/shared/trace-page-merge.js +15 -0
  111. package/dist/shared/trace-run-notifications.js +3 -1
  112. package/dist/signals/projector.js +6 -2
  113. package/dist/tools/agent-browser-wrapper.js +80 -80
  114. package/dist/tools/browser-pool.js +50 -0
  115. package/dist/tools/browser-use-cdp.js +12 -12
  116. package/dist/tools/browser-use-leases.js +12 -8
  117. package/dist/tools/browser-use-wrapper.js +762 -762
  118. package/dist/tools/guides.js +596 -538
  119. package/dist/tools/index.js +123 -99
  120. package/dist/tools/registry.js +21 -3
  121. package/dist/tools/runtime/node-worker-source.js +205 -205
  122. package/dist/tools/runtime/python-worker-source.js +177 -177
  123. package/dist/vscode/cli.js +9 -9
  124. package/dist/web-annotations/cdp.js +900 -900
  125. package/dist/web-annotations/store.js +96 -96
  126. package/docs/README.md +23 -23
  127. package/docs/ops/install-developer-host.md +112 -112
  128. package/docs/ops/install-user-host.md +96 -96
  129. package/docs/ops/upgrade-user-to-developer-host.md +69 -69
  130. package/docs/ops/vscode-extension-release.md +160 -160
  131. package/package.json +99 -95
  132. package/skills/builtin/graphify/SKILL.md +52 -52
  133. package/skills/builtin/loop/SKILL.md +78 -0
  134. package/skills/builtin/pi-agent-harness/SKILL.md +319 -319
  135. package/skills/builtin/pi-agent-harness/agents/openai.yaml +4 -4
  136. package/skills/builtin/pibo-docker-system/SKILL.md +170 -170
  137. package/skills/builtin/pibo-spec-writing/SKILL.md +330 -330
  138. package/skills/builtin/prd/SKILL.md +143 -143
  139. package/skills/builtin/ralph-loop/SKILL.md +361 -359
  140. package/skills/builtin/ralph-prd-json/SKILL.md +123 -123
  141. package/skills/builtin/skill-creator/LICENSE.txt +201 -201
  142. package/skills/builtin/skill-creator/SKILL.md +513 -513
  143. package/skills/builtin/skill-creator/agents/analyzer.md +274 -274
  144. package/skills/builtin/skill-creator/agents/comparator.md +202 -202
  145. package/skills/builtin/skill-creator/agents/grader.md +223 -223
  146. package/skills/builtin/skill-creator/assets/eval_review.html +146 -146
  147. package/skills/builtin/skill-creator/eval-viewer/generate_review.py +471 -471
  148. package/skills/builtin/skill-creator/eval-viewer/viewer.html +1325 -1325
  149. package/skills/builtin/skill-creator/references/schemas.md +430 -430
  150. package/skills/builtin/skill-creator/scripts/aggregate_benchmark.py +401 -401
  151. package/skills/builtin/skill-creator/scripts/generate_report.py +326 -326
  152. package/skills/builtin/skill-creator/scripts/improve_description.py +247 -247
  153. package/skills/builtin/skill-creator/scripts/package_skill.py +136 -136
  154. package/skills/builtin/skill-creator/scripts/quick_validate.py +102 -102
  155. package/skills/builtin/skill-creator/scripts/run_eval.py +310 -310
  156. package/skills/builtin/skill-creator/scripts/run_loop.py +328 -328
  157. package/skills/builtin/skill-creator/scripts/utils.py +47 -47
  158. package/skills/builtin/web-annotations/SKILL.md +93 -93
  159. package/src/mcp/LICENSE.mcp-cli +21 -21
  160. package/dist/apps/chat-ui/assets/index-DwHJfmiF.js +0 -173
  161. package/dist/apps/chat-vscode-web/assets/index-BAMxIaI_.js +0 -41
  162. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  163. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.9.13.vsix +0 -0
@@ -0,0 +1,642 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { piboHomePath } from '../core/pibo-home.js';
6
+ import { isPiboThinkingLevel } from '../core/thinking.js';
7
+ function nowIso(now = new Date()) { return now.toISOString(); }
8
+ function parseJson(json) { return JSON.parse(json); }
9
+ function defaultName(prompt) { const normalized = prompt.replace(/\s+/g, ' ').trim(); return normalized ? normalized.slice(0, 80) : 'Loop job'; }
10
+ function normalizeLoopMode(value, fallback = 'goal') { return value === 'ralph' ? 'ralph' : value === 'goal' ? 'goal' : fallback; }
11
+ function isJsonObject(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
12
+ const cleanupStates = new Set(['none', 'active', 'released', 'retained', 'dirty']);
13
+ function normalizeLoopTarget(target) {
14
+ if (target.kind === 'room')
15
+ return { kind: 'room', roomId: String(target.roomId ?? '').trim() };
16
+ return { kind: 'default-chat' };
17
+ }
18
+ function parseLoopTarget(json) { return normalizeLoopTarget(parseJson(json)); }
19
+ function targetJson(target) { return JSON.stringify(normalizeLoopTarget(target)); }
20
+ function optionalString(value, field) {
21
+ if (value === undefined || value === null)
22
+ return undefined;
23
+ if (typeof value !== 'string')
24
+ throw new Error(`${field} must be a string`);
25
+ const trimmed = value.trim();
26
+ return trimmed || undefined;
27
+ }
28
+ function normalizeBrowserLeaseIds(value) {
29
+ if (value === undefined || value === null)
30
+ return undefined;
31
+ if (!Array.isArray(value))
32
+ throw new Error('resources.browserLeaseIds must be an array');
33
+ const ids = [...new Set(value.map((item) => optionalString(item, 'resources.browserLeaseIds[]')).filter((item) => !!item))];
34
+ return ids.length ? ids : undefined;
35
+ }
36
+ export function normalizeLoopResourceMetadata(value) {
37
+ if (value === undefined || value === null)
38
+ return undefined;
39
+ if (!value || typeof value !== 'object' || Array.isArray(value))
40
+ throw new Error('resources must be an object');
41
+ const cleanupState = optionalString(value.cleanupState, 'resources.cleanupState');
42
+ if (cleanupState && !cleanupStates.has(cleanupState))
43
+ throw new Error('resources.cleanupState must be one of none, active, released, retained, dirty');
44
+ const resources = {};
45
+ const workerId = optionalString(value.workerId, 'resources.workerId');
46
+ const browserLeaseIds = normalizeBrowserLeaseIds(value.browserLeaseIds);
47
+ const retainedUntil = optionalString(value.retainedUntil, 'resources.retainedUntil');
48
+ const dirtyReason = optionalString(value.dirtyReason, 'resources.dirtyReason');
49
+ const updatedAt = optionalString(value.updatedAt, 'resources.updatedAt');
50
+ if (workerId)
51
+ resources.workerId = workerId;
52
+ if (browserLeaseIds)
53
+ resources.browserLeaseIds = browserLeaseIds;
54
+ if (cleanupState)
55
+ resources.cleanupState = cleanupState;
56
+ if (retainedUntil)
57
+ resources.retainedUntil = retainedUntil;
58
+ if (dirtyReason)
59
+ resources.dirtyReason = dirtyReason;
60
+ if (updatedAt)
61
+ resources.updatedAt = updatedAt;
62
+ return Object.keys(resources).length ? resources : undefined;
63
+ }
64
+ function parseResourceMetadata(json) {
65
+ if (!json)
66
+ return undefined;
67
+ try {
68
+ return normalizeLoopResourceMetadata(JSON.parse(json));
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
74
+ function resourceMetadataJson(resources) {
75
+ const normalized = normalizeLoopResourceMetadata(resources);
76
+ return normalized ? JSON.stringify(normalized) : null;
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
+ }
99
+ function jobFromRow(row) {
100
+ const resources = parseResourceMetadata(row.resource_json);
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 };
104
+ }
105
+ function runFromRow(row) {
106
+ const resources = parseResourceMetadata(row.resource_json);
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 };
109
+ }
110
+ function mergeResourceMetadata(jobResources, runResources) {
111
+ if (!jobResources && !runResources)
112
+ return undefined;
113
+ const browserLeaseIds = [...new Set([...(jobResources?.browserLeaseIds ?? []), ...(runResources?.browserLeaseIds ?? [])])];
114
+ return { ...(jobResources ?? {}), ...(runResources ?? {}), ...(browserLeaseIds.length ? { browserLeaseIds } : {}) };
115
+ }
116
+ function factFromRow(row) {
117
+ return { id: row.id, jobId: row.job_id, runId: row.run_id ?? undefined, piboSessionId: row.pibo_session_id ?? undefined, type: row.type, source: row.source, payload: parseJson(row.payload_json), createdAt: row.created_at };
118
+ }
119
+ function normalizeMaxIterations(value) {
120
+ if (value === undefined)
121
+ return undefined;
122
+ if (!Number.isInteger(value) || value < 1)
123
+ throw new Error('maxIterations must be a positive integer');
124
+ return value;
125
+ }
126
+ function normalizeTokenBudget(value) {
127
+ if (value === undefined)
128
+ return undefined;
129
+ if (!Number.isInteger(value) || value < 1)
130
+ throw new Error('tokenBudget must be a positive integer');
131
+ return value;
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
+ }
140
+ function goalStatus(job) {
141
+ if (job.mode !== 'goal')
142
+ return undefined;
143
+ return job.state.goalStatus ?? (job.enabled ? 'active' : 'paused');
144
+ }
145
+ function normalizeModelOverride(value) {
146
+ if (value === undefined || value === null)
147
+ return undefined;
148
+ const provider = value.provider.trim();
149
+ const id = value.id.trim();
150
+ if (!provider || !id)
151
+ throw new Error('modelOverride must include provider and id');
152
+ return { provider, id };
153
+ }
154
+ function normalizeThinkingLevel(value) {
155
+ if (value === undefined || value === null)
156
+ return undefined;
157
+ if (!isPiboThinkingLevel(value))
158
+ throw new Error('thinkingLevel must be one of off, minimal, low, medium, high, xhigh, max');
159
+ return value;
160
+ }
161
+ function normalizeFastMode(value) {
162
+ if (value === undefined || value === null)
163
+ return undefined;
164
+ if (typeof value !== 'boolean')
165
+ throw new Error('fastMode must be a boolean');
166
+ return value;
167
+ }
168
+ function normalizeRuntimeOptions(input) {
169
+ return { modelOverride: normalizeModelOverride(input.modelOverride), thinkingLevel: normalizeThinkingLevel(input.thinkingLevel), fastMode: normalizeFastMode(input.fastMode) };
170
+ }
171
+ function parseRuntimeOptions(json) {
172
+ if (!json)
173
+ return {};
174
+ try {
175
+ const parsed = JSON.parse(json);
176
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
177
+ return {};
178
+ return normalizeRuntimeOptions(parsed);
179
+ }
180
+ catch {
181
+ return {};
182
+ }
183
+ }
184
+ function runtimeOptionsJson(job) {
185
+ const options = normalizeRuntimeOptions(job);
186
+ const json = {};
187
+ if (options.modelOverride)
188
+ json.modelOverride = options.modelOverride;
189
+ if (options.thinkingLevel)
190
+ json.thinkingLevel = options.thinkingLevel;
191
+ if (options.fastMode !== undefined)
192
+ json.fastMode = options.fastMode;
193
+ return Object.keys(json).length ? JSON.stringify(json) : null;
194
+ }
195
+ function hasOwn(object, key) { return Object.prototype.hasOwnProperty.call(object, key); }
196
+ export function normalizeLoopStopPolicy(value) {
197
+ if (value === undefined || value === null)
198
+ return undefined;
199
+ if (!value || typeof value !== 'object' || Array.isArray(value))
200
+ throw new Error('stopPolicy must be an object');
201
+ if (value.mode !== 'any' && value.mode !== 'all')
202
+ throw new Error('stopPolicy.mode must be any or all');
203
+ if (!Array.isArray(value.conditions))
204
+ throw new Error('stopPolicy.conditions must be an array');
205
+ const ids = new Set();
206
+ return { mode: value.mode, conditions: value.conditions.map((condition, index) => {
207
+ if (!condition || typeof condition !== 'object' || Array.isArray(condition))
208
+ throw new Error(`stopPolicy.conditions[${index}] must be an object`);
209
+ const id = typeof condition.id === 'string' && condition.id.trim() ? condition.id.trim() : `condition-${index + 1}`;
210
+ const type = typeof condition.type === 'string' ? condition.type.trim() : '';
211
+ if (!type)
212
+ throw new Error(`stopPolicy.conditions[${index}].type is required`);
213
+ if (ids.has(id))
214
+ throw new Error(`Duplicate Loop stop condition id: ${id}`);
215
+ ids.add(id);
216
+ if (condition.options !== undefined && !isJsonObject(condition.options))
217
+ throw new Error(`stopPolicy.conditions[${index}].options must be an object`);
218
+ if (condition.enabled !== undefined && typeof condition.enabled !== 'boolean')
219
+ throw new Error(`stopPolicy.conditions[${index}].enabled must be a boolean`);
220
+ if (condition.failClosed !== undefined && typeof condition.failClosed !== 'boolean')
221
+ throw new Error(`stopPolicy.conditions[${index}].failClosed must be a boolean`);
222
+ if (condition.timeoutMs !== undefined && (!Number.isInteger(condition.timeoutMs) || condition.timeoutMs < 1))
223
+ throw new Error(`stopPolicy.conditions[${index}].timeoutMs must be a positive integer`);
224
+ return { id, type, ...(condition.enabled !== undefined ? { enabled: condition.enabled } : {}), ...(condition.options ? { options: condition.options } : {}), ...(condition.failClosed !== undefined ? { failClosed: condition.failClosed } : {}), ...(condition.timeoutMs !== undefined ? { timeoutMs: condition.timeoutMs } : {}) };
225
+ }) };
226
+ }
227
+ function parseStopPolicy(json) {
228
+ if (!json)
229
+ return undefined;
230
+ try {
231
+ return normalizeLoopStopPolicy(JSON.parse(json));
232
+ }
233
+ catch {
234
+ return undefined;
235
+ }
236
+ }
237
+ function stopPolicyJson(policy) { return policy ? JSON.stringify(normalizeLoopStopPolicy(policy)) : null; }
238
+ function validateJobInput(input) {
239
+ if (input.mode !== undefined && input.mode !== 'goal' && input.mode !== 'ralph')
240
+ throw new Error('mode must be goal or ralph');
241
+ if (!input.profile.trim())
242
+ throw new Error('profile is required');
243
+ if (!input.prompt.trim())
244
+ throw new Error('prompt is required');
245
+ if (input.target.kind === 'room' && !input.target.roomId.trim())
246
+ throw new Error('target.roomId is required');
247
+ if (input.mode === 'ralph' && (input.tokenBudget !== undefined || input.tokenReserve !== undefined))
248
+ throw new Error('tokenBudget and tokenReserve are only available for goal mode');
249
+ normalizeMaxIterations(input.maxIterations);
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');
256
+ normalizeRuntimeOptions(input);
257
+ normalizeLoopStopPolicy(input.stopPolicy);
258
+ normalizeLoopResourceMetadata(input.resources);
259
+ }
260
+ export class PiboLoopStore {
261
+ db;
262
+ constructor(options = {}) {
263
+ const dbPath = options.path ?? piboHomePath('pibo-ralph.sqlite');
264
+ const resolved = dbPath === ':memory:' ? dbPath : resolve(dbPath);
265
+ if (resolved !== ':memory:')
266
+ mkdirSync(dirname(resolved), { recursive: true });
267
+ this.db = new DatabaseSync(resolved);
268
+ this.db.exec('PRAGMA busy_timeout = 5000');
269
+ this.db.exec('PRAGMA foreign_keys = ON');
270
+ if (resolved !== ':memory:')
271
+ this.db.exec('PRAGMA journal_mode = WAL');
272
+ this.applySchema();
273
+ }
274
+ close() { this.db.close(); }
275
+ createJob(input, now = new Date()) {
276
+ const target = normalizeLoopTarget(input.target);
277
+ validateJobInput({ ...input, target });
278
+ const timestamp = nowIso(now);
279
+ const runtimeOptions = normalizeRuntimeOptions(input);
280
+ const resources = normalizeLoopResourceMetadata(input.resources);
281
+ const mode = normalizeLoopMode(input.mode);
282
+ const enabled = input.enabled === true;
283
+ const state = {
284
+ completedIterations: 0,
285
+ ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: timestamp } : {}) } : {}),
286
+ ...(input.initialPiboSessionId?.trim() ? { lastPiboSessionId: input.initialPiboSessionId.trim() } : {}),
287
+ };
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 };
289
+ this.insertJob(job);
290
+ return this.getJob(job.id);
291
+ }
292
+ getJob(id) { const row = this.db.prepare('SELECT * FROM pibo_ralph_jobs WHERE id = ?').get(id); return row ? jobFromRow(row) : undefined; }
293
+ getLatestGoalForSession(piboSessionId) {
294
+ const row = this.db.prepare("SELECT * FROM pibo_ralph_jobs WHERE loop_mode = 'goal' AND json_extract(state_json, '$.lastPiboSessionId') = ? ORDER BY created_at DESC LIMIT 1").get(piboSessionId);
295
+ return row ? jobFromRow(row) : undefined;
296
+ }
297
+ updateGoalStatus(id, status, now = new Date()) {
298
+ const job = this.getJob(id);
299
+ if (!job || job.mode !== 'goal')
300
+ return undefined;
301
+ const timestamp = nowIso(now);
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;
307
+ this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, state_json = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(state), timestamp, id);
308
+ return this.getJob(id);
309
+ }
310
+ recordGoalProgress(id, input, now = new Date()) {
311
+ const job = this.getJob(id);
312
+ if (!job || job.mode !== 'goal')
313
+ return job;
314
+ const tokens = Math.max(0, Math.floor(input.tokens ?? 0));
315
+ const activeTimeSeconds = Math.max(0, Math.floor(input.activeTimeSeconds ?? 0));
316
+ const nextTokens = (job.state.tokensUsed ?? 0) + tokens;
317
+ const currentStatus = goalStatus(job) ?? 'active';
318
+ const budgetLimited = currentStatus === 'active' && job.tokenBudget !== undefined && nextTokens >= job.tokenBudget;
319
+ const state = {
320
+ ...job.state,
321
+ tokensUsed: nextTokens,
322
+ activeTimeSeconds: (job.state.activeTimeSeconds ?? 0) + activeTimeSeconds,
323
+ goalStatus: budgetLimited ? 'budget_limited' : currentStatus,
324
+ };
325
+ const timestamp = nowIso(now);
326
+ if (budgetLimited && !job.state.runningAt)
327
+ state.goalEndedAt = timestamp;
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);
329
+ return this.getJob(id);
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
+ }
374
+ listJobs(input = {}) {
375
+ const clauses = [];
376
+ const values = [];
377
+ if (!input.includeDisabled)
378
+ clauses.push('enabled = 1');
379
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
380
+ return this.db.prepare(`SELECT * FROM pibo_ralph_jobs ${where} ORDER BY updated_at DESC, id ASC`).all(...values).map(jobFromRow);
381
+ }
382
+ updateJob(id, patch, now = new Date()) {
383
+ const existing = this.getJob(id);
384
+ if (!existing)
385
+ return undefined;
386
+ const runtimeOptions = normalizeRuntimeOptions({
387
+ modelOverride: hasOwn(patch, 'modelOverride') ? patch.modelOverride : existing.modelOverride,
388
+ thinkingLevel: hasOwn(patch, 'thinkingLevel') ? patch.thinkingLevel : existing.thinkingLevel,
389
+ fastMode: hasOwn(patch, 'fastMode') ? patch.fastMode : existing.fastMode,
390
+ });
391
+ const stopPolicy = hasOwn(patch, 'stopPolicy') ? normalizeLoopStopPolicy(patch.stopPolicy ?? undefined) : existing.stopPolicy;
392
+ const target = patch.target ? normalizeLoopTarget(patch.target) : existing.target;
393
+ const mode = patch.mode !== undefined ? normalizeLoopMode(patch.mode) : existing.mode;
394
+ const enabled = patch.enabled ?? existing.enabled;
395
+ let state = mode === existing.mode
396
+ ? { ...existing.state }
397
+ : { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: nowIso(now) } : {}) } : {}) };
398
+ if (mode === 'goal' && patch.enabled !== undefined) {
399
+ const currentGoalStatus = goalStatus({ mode, enabled: existing.enabled, state: existing.state }) ?? 'paused';
400
+ if (patch.enabled) {
401
+ if (currentGoalStatus === 'complete')
402
+ throw new Error('Completed goals cannot be restarted; create a new goal');
403
+ const nextBudget = hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget;
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');
407
+ state.goalStatus = 'active';
408
+ state.goalStartedAt ??= nowIso(now);
409
+ delete state.goalEndedAt;
410
+ state.stopRequestedAt = undefined;
411
+ state.cancelRequestedAt = undefined;
412
+ }
413
+ else {
414
+ state.goalStatus = currentGoalStatus === 'active' ? 'paused' : currentGoalStatus;
415
+ }
416
+ }
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) };
418
+ validateJobInput(next);
419
+ this.writeJob(next);
420
+ return this.getJob(id);
421
+ }
422
+ updateJobResources(id, resources, now = new Date()) {
423
+ const existing = this.getJob(id);
424
+ if (!existing)
425
+ return undefined;
426
+ const timestamp = nowIso(now);
427
+ this.db.prepare('UPDATE pibo_ralph_jobs SET resource_json = ?, updated_at = ? WHERE id = ?').run(resourceMetadataJson(resources), timestamp, id);
428
+ return this.getJob(id);
429
+ }
430
+ updateRunResources(input, now = new Date()) {
431
+ const clauses = ['id = ?'];
432
+ const values = [input.runId];
433
+ if (input.jobId) {
434
+ clauses.push('job_id = ?');
435
+ values.push(input.jobId);
436
+ }
437
+ const timestamp = nowIso(now);
438
+ const result = this.db.prepare(`UPDATE pibo_ralph_runs SET resource_json = ?, updated_at = ? WHERE ${clauses.join(' AND ')}`).run(resourceMetadataJson(input.resources), timestamp, ...values);
439
+ if (Number(result.changes ?? 0) === 0)
440
+ return undefined;
441
+ const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ?').get(input.runId);
442
+ return row ? runFromRow(row) : undefined;
443
+ }
444
+ removeJob(id) { const result = this.db.prepare('DELETE FROM pibo_ralph_jobs WHERE id = ?').run(id); return Number(result.changes ?? 0) > 0; }
445
+ listRuns(input = {}) {
446
+ const clauses = [];
447
+ const values = [];
448
+ if (input.jobId) {
449
+ clauses.push('job_id = ?');
450
+ values.push(input.jobId);
451
+ }
452
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
453
+ return this.db.prepare(`SELECT * FROM pibo_ralph_runs ${where} ORDER BY created_at DESC LIMIT ?`).all(...values, Math.max(1, Math.min(input.limit ?? 100, 500))).map(runFromRow);
454
+ }
455
+ reserveRun(id, now = new Date()) { this.updateJob(id, { enabled: true }, now); return this.reserveJob(id, now); }
456
+ reserveDueRuns(limit, now = new Date()) {
457
+ const rows = this.db.prepare('SELECT * FROM pibo_ralph_jobs WHERE enabled = 1 ORDER BY updated_at ASC').all();
458
+ const result = [];
459
+ for (const row of rows) {
460
+ if (result.length >= limit)
461
+ break;
462
+ const reserved = this.reserveJob(row.id, now);
463
+ if (reserved)
464
+ result.push(reserved);
465
+ }
466
+ return result;
467
+ }
468
+ attachRunSession(jobId, runId, piboSessionId, now = new Date()) {
469
+ const timestamp = nowIso(now);
470
+ const job = this.getJob(jobId);
471
+ if (!job)
472
+ return;
473
+ this.db.prepare('UPDATE pibo_ralph_runs SET pibo_session_id = ?, updated_at = ? WHERE id = ?').run(piboSessionId, timestamp, runId);
474
+ this.updateJobStateLocked(jobId, { ...job.state, lastPiboSessionId: piboSessionId }, timestamp);
475
+ }
476
+ requestStop(id, now = new Date()) {
477
+ const job = this.getJob(id);
478
+ if (!job)
479
+ return undefined;
480
+ const state = { ...job.state, ...(job.mode === 'goal' && goalStatus(job) === 'active' ? { goalStatus: 'paused' } : {}), stopRequestedAt: nowIso(now) };
481
+ this.writeJob({ ...job, enabled: false, state, updatedAt: nowIso(now) });
482
+ return this.getJob(id);
483
+ }
484
+ requestCancel(id, now = new Date()) {
485
+ const job = this.getJob(id);
486
+ if (!job)
487
+ return undefined;
488
+ const state = { ...job.state, ...(job.mode === 'goal' && goalStatus(job) === 'active' ? { goalStatus: 'paused' } : {}), stopRequestedAt: nowIso(now), cancelRequestedAt: nowIso(now) };
489
+ this.writeJob({ ...job, enabled: false, state, updatedAt: nowIso(now) });
490
+ return this.getJob(id);
491
+ }
492
+ applyStopEvaluation(input, now = new Date()) {
493
+ const job = this.getJob(input.jobId);
494
+ if (!job)
495
+ return;
496
+ const timestamp = nowIso(now);
497
+ const state = { ...job.state, conditionStates: input.conditionStates ?? job.state.conditionStates, lastStopEvaluation: input.evaluation };
498
+ this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(input.disable ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, job.id);
499
+ }
500
+ completeRun(input, now = new Date()) {
501
+ const timestamp = nowIso(now);
502
+ const job = this.getJob(input.jobId);
503
+ if (!job)
504
+ return;
505
+ const completedIterations = (job.state.completedIterations ?? 0) + 1;
506
+ const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
507
+ const terminalGoalStatus = job.mode === 'goal' && ['complete', 'blocked', 'budget_limited'].includes(goalStatus(job) ?? '');
508
+ const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
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 } : {}) };
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);
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);
512
+ }
513
+ appendRunFact(input) {
514
+ if (!input.jobId.trim())
515
+ throw new Error('fact jobId is required');
516
+ if (!input.type.trim())
517
+ throw new Error('fact type is required');
518
+ if (!isJsonObject(input.payload))
519
+ throw new Error('fact payload must be an object');
520
+ const payloadJson = JSON.stringify(input.payload);
521
+ if (payloadJson.length > 16_384)
522
+ throw new Error('fact payload is too large');
523
+ const fact = { id: input.id ?? `rfact_${randomUUID()}`, jobId: input.jobId, runId: input.runId, piboSessionId: input.piboSessionId, type: input.type.trim(), source: input.source, payload: input.payload, createdAt: input.createdAt ?? nowIso() };
524
+ this.db.prepare('INSERT INTO pibo_ralph_run_facts (id, job_id, run_id, pibo_session_id, type, source, payload_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(fact.id, fact.jobId, fact.runId ?? null, fact.piboSessionId ?? null, fact.type, fact.source, payloadJson, fact.createdAt);
525
+ return fact;
526
+ }
527
+ listRunFacts(input = {}) {
528
+ const clauses = [];
529
+ const values = [];
530
+ if (input.jobId) {
531
+ clauses.push('job_id = ?');
532
+ values.push(input.jobId);
533
+ }
534
+ if (input.runId) {
535
+ clauses.push('run_id = ?');
536
+ values.push(input.runId);
537
+ }
538
+ if (input.type) {
539
+ clauses.push('type = ?');
540
+ values.push(input.type);
541
+ }
542
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
543
+ return this.db.prepare(`SELECT * FROM pibo_ralph_run_facts ${where} ORDER BY created_at DESC LIMIT ?`).all(...values, Math.max(1, Math.min(input.limit ?? 100, 500))).map(factFromRow);
544
+ }
545
+ createFactReader(job) {
546
+ return { list: (input = {}) => this.listRunFacts({ jobId: job.id, type: input.type, runId: input.runId, limit: input.limit }), count: (input = {}) => this.listRunFacts({ jobId: job.id, type: input.type, runId: input.runId, limit: 500 }).length };
547
+ }
548
+ recoverInterruptedRuns(cutoff = new Date(Date.now() - 5 * 60_000)) { const cutoffIso = nowIso(cutoff); const rows = this.db.prepare("SELECT * FROM pibo_ralph_jobs WHERE json_extract(state_json, '$.runningAt') IS NOT NULL").all(); let recovered = 0; for (const row of rows) {
549
+ const job = jobFromRow(row);
550
+ if (!job.state.runningAt || job.state.runningAt > cutoffIso)
551
+ continue;
552
+ if (job.state.lastRunId) {
553
+ this.completeRun({ jobId: job.id, runId: job.state.lastRunId, status: 'error', error: 'Loop run was interrupted by gateway restart', reason: 'interrupted' });
554
+ this.markInterruptedRunResourcesDirty(job, job.state.lastRunId);
555
+ }
556
+ recovered += 1;
557
+ } return recovered; }
558
+ status() { const jobs = this.listJobs({ includeDisabled: false }); return { jobs: jobs.length, running: jobs.filter((job) => job.state.runningAt).length }; }
559
+ markInterruptedRunResourcesDirty(job, runId) {
560
+ const runRow = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, job.id);
561
+ const resources = mergeResourceMetadata(job.resources, runRow ? runFromRow(runRow).resources : undefined);
562
+ if (!resources || (!resources.workerId && (resources.browserLeaseIds ?? []).length === 0))
563
+ return;
564
+ const nextResources = { ...resources, cleanupState: 'dirty', dirtyReason: 'Loop run was interrupted before browser resource cleanup could be confirmed', updatedAt: nowIso() };
565
+ this.updateRunResources({ jobId: job.id, runId, resources: nextResources });
566
+ this.updateJobResources(job.id, nextResources);
567
+ }
568
+ reserveJob(id, now = new Date()) {
569
+ const timestamp = nowIso(now);
570
+ this.db.exec('BEGIN IMMEDIATE');
571
+ try {
572
+ const job = this.getJob(id);
573
+ if (!job || !job.enabled || job.state.runningAt) {
574
+ this.db.exec('COMMIT');
575
+ return undefined;
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
+ }
586
+ const run = this.createRunLocked(job, timestamp);
587
+ const state = { ...job.state, runningAt: timestamp, lastRunAt: timestamp, lastRunId: run.id };
588
+ this.updateJobStateLocked(job.id, state, timestamp);
589
+ this.db.exec('COMMIT');
590
+ return { job: { ...job, state, updatedAt: timestamp }, run };
591
+ }
592
+ catch (error) {
593
+ this.db.exec('ROLLBACK');
594
+ throw error;
595
+ }
596
+ }
597
+ applySchema() {
598
+ this.createFreshSchema();
599
+ this.ensureJobColumn('loop_mode', "TEXT NOT NULL DEFAULT 'ralph'");
600
+ this.ensureJobColumn('max_iterations', 'INTEGER');
601
+ this.ensureJobColumn('token_budget', 'INTEGER');
602
+ this.ensureJobColumn('token_reserve', 'INTEGER');
603
+ this.ensureJobColumn('runtime_options_json', 'TEXT');
604
+ this.ensureJobColumn('stop_policy_json', 'TEXT');
605
+ this.ensureJobColumn('resource_json', 'TEXT');
606
+ this.ensureRunColumn('resource_json', 'TEXT');
607
+ this.ensureRunColumn('accounting_json', 'TEXT');
608
+ }
609
+ createFreshSchema() {
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);`);
611
+ }
612
+ ensureJobColumn(name, definition) {
613
+ const columns = this.tableColumns('pibo_ralph_jobs');
614
+ if (!columns.has(name))
615
+ this.db.exec(`ALTER TABLE pibo_ralph_jobs ADD COLUMN ${name} ${definition}`);
616
+ }
617
+ ensureRunColumn(name, definition) {
618
+ const columns = this.tableColumns('pibo_ralph_runs');
619
+ if (!columns.has(name))
620
+ this.db.exec(`ALTER TABLE pibo_ralph_runs ADD COLUMN ${name} ${definition}`);
621
+ }
622
+ tableColumns(tableName) {
623
+ return new Set(this.db.prepare(`PRAGMA table_info(${tableName})`).all().map((column) => column.name));
624
+ }
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); }
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); }
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
+ }
641
+ }
642
+ export function createDefaultPiboLoopStore(options = {}) { return new PiboLoopStore(options); }