@pasko70/pibo 2.3.0 → 2.4.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 (44) hide show
  1. package/dist/agent-runtime/routed-session.js +1 -0
  2. package/dist/agent-runtimes/codex-native/turn.js +3 -1
  3. package/dist/agent-runtimes/omp/turn.js +36 -7
  4. package/dist/apps/chat/web-app.js +6 -8
  5. package/dist/apps/chat-ui/assets/{dist-CUcAofmV.js → dist-3YG57JXi.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-Byygd1lH.js → dist-BeqHbnGN.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-DusFwy0L.js → dist-CrDtveZB.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-C9BrS7sL.js → dist-Cw9po47P.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-D4RU6xu3.js → dist-DTRjeLwO.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{index-Bifi_kjN.js → index-AjnP3ci-.js} +89 -89
  11. package/dist/apps/chat-ui/index.html +1 -1
  12. package/dist/apps/chat-vscode-web/assets/{index-WsLm1mo3.js → index-DvTSSvzN.js} +5 -5
  13. package/dist/apps/chat-vscode-web/index.html +1 -1
  14. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  15. package/dist/apps/vscode-artifacts/{pibo-vscode-ext-2.3.0.vsix → pibo-vscode-ext-2.4.1.vsix} +0 -0
  16. package/dist/compute/cli.js +13 -0
  17. package/dist/compute/pool/artifacts.js +116 -0
  18. package/dist/compute/pool/cli.js +156 -0
  19. package/dist/compute/pool/config.js +101 -0
  20. package/dist/compute/pool/docker.js +157 -0
  21. package/dist/compute/pool/seeds.js +201 -0
  22. package/dist/compute/pool/service.js +402 -0
  23. package/dist/compute/pool/store.js +239 -0
  24. package/dist/compute/pool/types.js +1 -0
  25. package/dist/gateway/web.js +8 -1
  26. package/dist/loops/accounting.js +27 -0
  27. package/dist/loops/cli.js +6 -5
  28. package/dist/loops/prompts.js +13 -5
  29. package/dist/loops/service.js +3 -1
  30. package/dist/loops/store.js +10 -6
  31. package/dist/loops/tools.js +7 -4
  32. package/dist/mcp/config-command.js +3 -2
  33. package/dist/mcp/config.js +10 -4
  34. package/dist/mcp/errors.js +1 -1
  35. package/dist/mcp/index.js +19 -33
  36. package/dist/resources/lifecycle.js +22 -2
  37. package/dist/resources/reaper.js +1 -0
  38. package/dist/session-ui/sessionActivity.js +6 -2
  39. package/dist/signals/status.js +9 -4
  40. package/dist/tools/guides.js +1 -1
  41. package/dist/web/channel.js +19 -7
  42. package/npm-shrinkwrap.json +2 -2
  43. package/package.json +1 -1
  44. package/skills/builtin/loop/SKILL.md +1 -1
@@ -0,0 +1,239 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { chmodSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ function slotFromRow(row) {
6
+ return {
7
+ id: row.id,
8
+ ordinal: row.ordinal,
9
+ webPort: row.web_port,
10
+ gatewayPort: row.gateway_port,
11
+ publicUrl: row.public_url ?? undefined,
12
+ state: row.state,
13
+ activeLeaseId: row.active_lease_id ?? undefined,
14
+ dirtyReason: row.dirty_reason ?? undefined,
15
+ updatedAt: row.updated_at,
16
+ };
17
+ }
18
+ function leaseFromRow(row) {
19
+ return {
20
+ id: row.id,
21
+ slotId: row.slot_id,
22
+ holder: row.holder,
23
+ seedMode: row.seed_mode,
24
+ artifactSha256: row.artifact_sha256,
25
+ artifactRuntimePath: row.artifact_runtime_path,
26
+ packageVersion: row.package_version ?? undefined,
27
+ commit: row.commit_sha ?? undefined,
28
+ containerName: row.container_name,
29
+ publicUrl: row.public_url ?? undefined,
30
+ status: row.status,
31
+ createdAt: row.created_at,
32
+ expiresAt: row.expires_at,
33
+ renewedAt: row.renewed_at ?? undefined,
34
+ releasedAt: row.released_at ?? undefined,
35
+ failedAt: row.failed_at ?? undefined,
36
+ failureSnapshotPath: row.failure_snapshot_path ?? undefined,
37
+ lastError: row.last_error ?? undefined,
38
+ };
39
+ }
40
+ export class DeploymentPoolStore {
41
+ path;
42
+ db;
43
+ constructor(path, slots) {
44
+ this.path = path === ":memory:" ? path : resolve(path);
45
+ if (this.path !== ":memory:")
46
+ mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
47
+ this.db = new DatabaseSync(this.path);
48
+ this.db.exec("PRAGMA busy_timeout = 10000");
49
+ if (this.path !== ":memory:")
50
+ this.db.exec("PRAGMA journal_mode = WAL");
51
+ this.applySchema();
52
+ this.ensureSlots(slots);
53
+ if (this.path !== ":memory:")
54
+ chmodSync(this.path, 0o600);
55
+ }
56
+ applySchema() {
57
+ this.db.exec(`
58
+ CREATE TABLE IF NOT EXISTS deployment_pool_slots (
59
+ id TEXT PRIMARY KEY,
60
+ ordinal INTEGER NOT NULL UNIQUE,
61
+ web_port INTEGER NOT NULL UNIQUE,
62
+ gateway_port INTEGER NOT NULL UNIQUE,
63
+ public_url TEXT,
64
+ state TEXT NOT NULL,
65
+ active_lease_id TEXT,
66
+ dirty_reason TEXT,
67
+ updated_at TEXT NOT NULL
68
+ );
69
+ CREATE TABLE IF NOT EXISTS deployment_pool_leases (
70
+ id TEXT PRIMARY KEY,
71
+ slot_id TEXT NOT NULL,
72
+ holder TEXT NOT NULL,
73
+ seed_mode TEXT NOT NULL,
74
+ artifact_sha256 TEXT NOT NULL,
75
+ artifact_runtime_path TEXT NOT NULL,
76
+ package_version TEXT,
77
+ commit_sha TEXT,
78
+ container_name TEXT NOT NULL,
79
+ public_url TEXT,
80
+ status TEXT NOT NULL,
81
+ created_at TEXT NOT NULL,
82
+ expires_at TEXT NOT NULL,
83
+ renewed_at TEXT,
84
+ released_at TEXT,
85
+ failed_at TEXT,
86
+ failure_snapshot_path TEXT,
87
+ last_error TEXT
88
+ );
89
+ CREATE INDEX IF NOT EXISTS deployment_pool_leases_status_idx
90
+ ON deployment_pool_leases (status, expires_at);
91
+ CREATE INDEX IF NOT EXISTS deployment_pool_leases_slot_idx
92
+ ON deployment_pool_leases (slot_id, created_at DESC);
93
+ `);
94
+ }
95
+ ensureSlots(slots) {
96
+ const now = new Date().toISOString();
97
+ const insert = this.db.prepare(`
98
+ INSERT INTO deployment_pool_slots (id, ordinal, web_port, gateway_port, public_url, state, updated_at)
99
+ VALUES (?, ?, ?, ?, ?, 'free', ?)
100
+ ON CONFLICT(id) DO UPDATE SET
101
+ ordinal=excluded.ordinal,
102
+ web_port=excluded.web_port,
103
+ gateway_port=excluded.gateway_port,
104
+ public_url=excluded.public_url
105
+ `);
106
+ for (const slot of slots)
107
+ insert.run(slot.id, slot.ordinal, slot.webPort, slot.gatewayPort, slot.publicUrl ?? null, now);
108
+ }
109
+ listSlots() {
110
+ return this.db.prepare("SELECT * FROM deployment_pool_slots ORDER BY ordinal").all().map(slotFromRow);
111
+ }
112
+ getSlot(id) {
113
+ const row = this.db.prepare("SELECT * FROM deployment_pool_slots WHERE id = ?").get(id);
114
+ return row ? slotFromRow(row) : undefined;
115
+ }
116
+ listLeases(options = {}) {
117
+ const where = options.includeInactive ? "" : "WHERE status IN ('provisioning', 'ready', 'releasing')";
118
+ return this.db.prepare(`SELECT * FROM deployment_pool_leases ${where} ORDER BY created_at DESC`).all().map(leaseFromRow);
119
+ }
120
+ getLease(id) {
121
+ const row = this.db.prepare("SELECT * FROM deployment_pool_leases WHERE id = ?").get(id);
122
+ return row ? leaseFromRow(row) : undefined;
123
+ }
124
+ reserveLease(input) {
125
+ this.db.exec("BEGIN IMMEDIATE");
126
+ try {
127
+ const active = this.db.prepare("SELECT COUNT(*) AS count FROM deployment_pool_slots WHERE state IN ('provisioning', 'ready', 'releasing')").get();
128
+ if (Number(active.count) >= input.maxActive) {
129
+ const nearest = this.db.prepare(`
130
+ SELECT MIN(l.expires_at) AS nearest_expiry
131
+ FROM deployment_pool_slots s
132
+ JOIN deployment_pool_leases l ON l.id = s.active_lease_id
133
+ WHERE s.state IN ('provisioning', 'ready', 'releasing')
134
+ `).get();
135
+ throw new Error(`Deployment pool capacity reached (${input.maxActive} active)${nearest.nearest_expiry ? `; nearest expiry ${nearest.nearest_expiry}` : ""}`);
136
+ }
137
+ const row = this.db.prepare("SELECT * FROM deployment_pool_slots WHERE state = 'free' ORDER BY ordinal LIMIT 1").get();
138
+ if (!row)
139
+ throw new Error("No free deployment pool slot is available");
140
+ const slot = slotFromRow(row);
141
+ const containerName = `pibo-pool-${slot.id}`;
142
+ this.db.prepare(`
143
+ INSERT INTO deployment_pool_leases (
144
+ id, slot_id, holder, seed_mode, artifact_sha256, artifact_runtime_path,
145
+ package_version, commit_sha, container_name, public_url, status, created_at, expires_at
146
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'provisioning', ?, ?)
147
+ `).run(input.id, slot.id, input.holder, input.seedMode, input.artifactSha256, input.artifactRuntimePath, input.packageVersion ?? null, input.commit ?? null, containerName, slot.publicUrl ?? null, input.createdAt, input.expiresAt);
148
+ this.db.prepare("UPDATE deployment_pool_slots SET state='provisioning', active_lease_id=?, dirty_reason=NULL, updated_at=? WHERE id=?")
149
+ .run(input.id, input.createdAt, slot.id);
150
+ this.db.exec("COMMIT");
151
+ return { slot: this.getSlot(slot.id), lease: this.getLease(input.id) };
152
+ }
153
+ catch (error) {
154
+ this.db.exec("ROLLBACK");
155
+ throw error;
156
+ }
157
+ }
158
+ markReady(leaseId, now = new Date().toISOString()) {
159
+ const lease = this.requireLease(leaseId);
160
+ this.db.exec("BEGIN IMMEDIATE");
161
+ try {
162
+ this.db.prepare("UPDATE deployment_pool_leases SET status='ready', last_error=NULL WHERE id=?").run(leaseId);
163
+ this.db.prepare("UPDATE deployment_pool_slots SET state='ready', dirty_reason=NULL, updated_at=? WHERE id=? AND active_lease_id=?")
164
+ .run(now, lease.slotId, leaseId);
165
+ this.db.exec("COMMIT");
166
+ return this.requireLease(leaseId);
167
+ }
168
+ catch (error) {
169
+ this.db.exec("ROLLBACK");
170
+ throw error;
171
+ }
172
+ }
173
+ markReleasing(leaseId, now = new Date().toISOString()) {
174
+ const lease = this.requireLease(leaseId);
175
+ this.db.prepare("UPDATE deployment_pool_leases SET status='releasing' WHERE id=? AND status IN ('provisioning','ready','releasing')").run(leaseId);
176
+ this.db.prepare("UPDATE deployment_pool_slots SET state='releasing', updated_at=? WHERE id=? AND active_lease_id=?").run(now, lease.slotId, leaseId);
177
+ return this.requireLease(leaseId);
178
+ }
179
+ markReleased(leaseId, status, now = new Date().toISOString()) {
180
+ const lease = this.requireLease(leaseId);
181
+ this.db.exec("BEGIN IMMEDIATE");
182
+ try {
183
+ this.db.prepare("UPDATE deployment_pool_leases SET status=?, released_at=?, last_error=NULL WHERE id=?").run(status, now, leaseId);
184
+ this.db.prepare("UPDATE deployment_pool_slots SET state='free', active_lease_id=NULL, dirty_reason=NULL, updated_at=? WHERE id=? AND active_lease_id=?")
185
+ .run(now, lease.slotId, leaseId);
186
+ this.db.exec("COMMIT");
187
+ return this.requireLease(leaseId);
188
+ }
189
+ catch (error) {
190
+ this.db.exec("ROLLBACK");
191
+ throw error;
192
+ }
193
+ }
194
+ markFailed(leaseId, error, snapshotPath, options = { slotClean: false }) {
195
+ const lease = this.requireLease(leaseId);
196
+ const now = options.now ?? new Date().toISOString();
197
+ this.db.exec("BEGIN IMMEDIATE");
198
+ try {
199
+ this.db.prepare("UPDATE deployment_pool_leases SET status='failed', failed_at=?, failure_snapshot_path=?, last_error=? WHERE id=?")
200
+ .run(now, snapshotPath ?? null, error, leaseId);
201
+ if (options.slotClean) {
202
+ this.db.prepare("UPDATE deployment_pool_slots SET state='free', active_lease_id=NULL, dirty_reason=NULL, updated_at=? WHERE id=? AND active_lease_id=?")
203
+ .run(now, lease.slotId, leaseId);
204
+ }
205
+ else {
206
+ this.db.prepare("UPDATE deployment_pool_slots SET state='dirty', dirty_reason=?, updated_at=? WHERE id=? AND active_lease_id=?")
207
+ .run(error, now, lease.slotId, leaseId);
208
+ }
209
+ this.db.exec("COMMIT");
210
+ return this.requireLease(leaseId);
211
+ }
212
+ catch (caught) {
213
+ this.db.exec("ROLLBACK");
214
+ throw caught;
215
+ }
216
+ }
217
+ renewLease(leaseId, holder, expiresAt, now = new Date().toISOString()) {
218
+ const result = this.db.prepare(`
219
+ UPDATE deployment_pool_leases SET expires_at=?, renewed_at=?
220
+ WHERE id=? AND holder=? AND status='ready'
221
+ `).run(expiresAt, now, leaseId, holder);
222
+ if (Number(result.changes ?? 0) !== 1)
223
+ throw new Error(`Active deployment lease "${leaseId}" for holder "${holder}" was not found`);
224
+ return this.requireLease(leaseId);
225
+ }
226
+ freeDirtySlot(slotId, now = new Date().toISOString()) {
227
+ this.db.prepare("UPDATE deployment_pool_slots SET state='free', active_lease_id=NULL, dirty_reason=NULL, updated_at=? WHERE id=?")
228
+ .run(now, slotId);
229
+ }
230
+ requireLease(id) {
231
+ const lease = this.getLease(id);
232
+ if (!lease)
233
+ throw new Error(`Deployment lease "${id}" was not found`);
234
+ return lease;
235
+ }
236
+ close() {
237
+ this.db.close();
238
+ }
239
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
+ import { CHAT_WEB_APP_NAME } from "../apps/chat/web-app.js";
1
2
  import { createDefaultPiboPlugins } from "../plugins/builtin.js";
2
3
  import { createPiboBetterAuthPlugin } from "../plugins/better-auth.js";
3
4
  import { createPiboChatCustomAgentProfilesPlugin } from "../plugins/chat-custom-agents.js";
@@ -144,7 +145,13 @@ export function createWebPiboPluginRegistry(options = {}) {
144
145
  plugins: [
145
146
  ...createDefaultPiboPlugins(),
146
147
  useDevAuth ? createPiboDevAuthPlugin() : createPiboBetterAuthPlugin(resolvedOptions.auth),
147
- createPiboWebHostPlugin({ announce: false, canonicalBaseURL: useDevAuth ? undefined : authBaseURL(resolvedOptions), gatewayMode: webGatewayMode(resolvedOptions, useDevAuth), ...resolvedOptions.web }),
148
+ createPiboWebHostPlugin({
149
+ announce: false,
150
+ canonicalBaseURL: useDevAuth ? undefined : authBaseURL(resolvedOptions),
151
+ gatewayMode: webGatewayMode(resolvedOptions, useDevAuth),
152
+ ...resolvedOptions.web,
153
+ landingAppName: CHAT_WEB_APP_NAME,
154
+ }),
148
155
  createPiboCronPlugin({ cronStorePath: resolvedOptions.chat?.cronStorePath, dataStorePath: resolvedOptions.chat?.dataStorePath, dataPayloadRootDir: resolvedOptions.chat?.dataPayloadRootDir }),
149
156
  createPiboChatUserSkillsPlugin({
150
157
  globalRoot: resolvedOptions.chat?.userSkillGlobalRoot,
@@ -1,3 +1,30 @@
1
+ export const LOOP_TOKEN_ACCOUNTING_VERSION = 1;
2
+ function normalizedTokenCount(value) {
3
+ return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
4
+ }
5
+ export function normalizeLoopTokenAccounting(value, fallback = 'total') {
6
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
7
+ const candidate = value;
8
+ if (candidate.version === LOOP_TOKEN_ACCOUNTING_VERSION && (candidate.basis === 'total' || candidate.basis === 'uncached')) {
9
+ return { version: LOOP_TOKEN_ACCOUNTING_VERSION, basis: candidate.basis };
10
+ }
11
+ }
12
+ return { version: LOOP_TOKEN_ACCOUNTING_VERSION, basis: fallback };
13
+ }
14
+ export function newGoalTokenAccounting() {
15
+ return { version: LOOP_TOKEN_ACCOUNTING_VERSION, basis: 'uncached' };
16
+ }
17
+ export function goalTokenAccounting(job) {
18
+ return normalizeLoopTokenAccounting(job.state.tokenAccounting);
19
+ }
20
+ export function goalBudgetTokens(usage, basis) {
21
+ const totalTokens = normalizedTokenCount(usage.totalTokens);
22
+ if (basis === 'total')
23
+ return totalTokens;
24
+ const cacheReadTokens = normalizedTokenCount(usage.cacheReadTokens);
25
+ const cacheWriteTokens = normalizedTokenCount(usage.cacheWriteTokens);
26
+ return Math.max(0, totalTokens - cacheReadTokens - cacheWriteTokens);
27
+ }
1
28
  export function goalActiveTimeSeconds(job) {
2
29
  return Math.max(0, Math.floor(job.state.activeTimeSeconds ?? job.state.timeUsedSeconds ?? 0));
3
30
  }
package/dist/loops/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { Command } from 'commander';
3
- import { goalActiveTimeSeconds, goalElapsedWallClockSeconds } from './accounting.js';
3
+ import { goalActiveTimeSeconds, goalElapsedWallClockSeconds, goalTokenAccounting } from './accounting.js';
4
4
  import { createDefaultPiboLoopStore } from './store.js';
5
5
  import { createBuiltInLoopStopConditions } from './stopping.js';
6
6
  import { DEFAULT_PIBO_PROFILE_NAME } from '../plugins/builtin.js';
@@ -116,12 +116,13 @@ export function formatLoopResourceSummary(resources) {
116
116
  }
117
117
  function formatLoopJobLine(job) {
118
118
  const goal = job.mode === 'goal' ? job.state.goalStatus ?? (job.enabled ? 'active' : 'paused') : '-';
119
- const budget = job.mode === 'goal' ? job.tokenBudget === undefined ? 'unbounded' : `soft:${job.state.tokensUsed ?? 0}/${job.tokenBudget};reserve=${job.tokenReserve ?? 0}` : '-';
119
+ const tokenBasis = job.mode === 'goal' ? goalTokenAccounting(job).basis : undefined;
120
+ const budget = job.mode === 'goal' ? job.tokenBudget === undefined ? `unbounded:${tokenBasis}` : `soft:${tokenBasis}:${job.state.tokensUsed ?? 0}/${job.tokenBudget};reserve=${job.tokenReserve ?? 0}` : '-';
120
121
  const time = job.mode === 'goal' ? `activeAgent=${goalActiveTimeSeconds(job)}s;elapsedWall=${goalElapsedWallClockSeconds(job)}s;paused=included` : '-';
121
122
  return `${job.id}\t${job.mode}\t${job.enabled ? 'running' : 'stopped'}\t${job.state.runningAt ? 'active' : '-'}\tgoal=${goal}\tbudget=${budget}\ttime=${time}\tresources=${formatLoopResourceSummary(job.resources)}\t${job.name}`;
122
123
  }
123
124
  function formatLoopRunLine(run) {
124
- const accounting = run.accounting ? `tokens=${run.accounting.tokensUsed ?? 0};remainingBefore=${run.accounting.remainingTokensBefore ?? 'unbounded'};overshoot=${run.accounting.overshootTokens ?? 0};activeAgent=${run.accounting.activeTimeSeconds ?? 0}s` : '-';
125
+ const accounting = run.accounting ? `basis=${run.accounting.tokenAccounting?.basis ?? 'total'};tokens=${run.accounting.tokensUsed ?? 0};remainingBefore=${run.accounting.remainingTokensBefore ?? 'unbounded'};overshoot=${run.accounting.overshootTokens ?? 0};activeAgent=${run.accounting.activeTimeSeconds ?? 0}s` : '-';
125
126
  return `${run.id}\t${run.jobId}\t${run.status}\t${run.piboSessionId ?? '-'}\t${run.completedAt ?? '-'}\taccounting=${accounting}\tresources=${formatLoopResourceSummary(run.resources)}`;
126
127
  }
127
128
  export async function runLoopCli(argv = process.argv, defaults = {}) {
@@ -139,13 +140,13 @@ export async function runLoopCli(argv = process.argv, defaults = {}) {
139
140
  else
140
141
  for (const job of jobs)
141
142
  console.log(formatLoopJobLine(job)); store.close(); });
142
- program.command('add').description(defaults.mode === 'ralph' ? 'Create a Ralph job' : 'Create a Loop job').option('--template <id>', 'Built-in job template id').option('--mode <mode>', 'Loop mode: goal or ralph').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile', DEFAULT_PIBO_PROFILE_NAME).option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--token-budget <n>', 'Set a soft Goal token budget; the final turn can overshoot').option('--token-reserve <n>', 'Require more than n tokens to remain before starting another Goal turn').option('--model <provider/model>', 'Runtime model override, for example openai/gpt-5').option('--thinking <level>', 'Runtime thinking level override: off, minimal, low, medium, high, xhigh, max').option('--fast', 'Enable runtime fast mode').option('--no-fast', 'Disable runtime fast mode')
143
+ program.command('add').description(defaults.mode === 'ralph' ? 'Create a Ralph job' : 'Create a Loop job').option('--template <id>', 'Built-in job template id').option('--mode <mode>', 'Loop mode: goal or ralph').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile', DEFAULT_PIBO_PROFILE_NAME).option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--token-budget <n>', 'Set a soft Goal token budget; new Goals use uncached accounting and the final turn can overshoot').option('--token-reserve <n>', 'Require more than n tokens under the Goal accounting basis before starting another turn').option('--model <provider/model>', 'Runtime model override, for example openai/gpt-5').option('--thinking <level>', 'Runtime thinking level override: off, minimal, low, medium, high, xhigh, max').option('--fast', 'Enable runtime fast mode').option('--no-fast', 'Disable runtime fast mode')
143
144
  .option('--start', 'Start immediately').option('--json', 'Print JSON').action((options) => { const base = templatePatch(options.template); const prompt = options.prompt ?? base.prompt; if (typeof prompt !== 'string' || !prompt.trim())
144
145
  throw new Error('Choose --template <id> or provide --prompt <text>'); const input = { mode: loopMode(options.mode) ?? base.mode ?? defaults.mode ?? 'goal', name: options.name ?? base.name, description: options.description ?? base.description, enabled: options.start === true, target: targetFromOptions(options), profile: options.profile, prompt, maxIterations: options.maxIterations !== undefined ? maxIterations(options.maxIterations) : typeof base.maxIterations === 'number' ? base.maxIterations : undefined, tokenBudget: tokenBudget(options.tokenBudget), tokenReserve: tokenReserve(options.tokenReserve), stopPolicy: base.stopPolicy ?? undefined }; applyRuntimeCreateOptions(input, options); const store = createDefaultPiboLoopStore({ path: program.opts().store }); const job = store.createJob(input); if (options.json)
145
146
  printJson(job);
146
147
  else
147
148
  console.log(`${job.id}\t${job.enabled ? 'running' : 'stopped'}\t${job.name}`); store.close(); });
148
- program.command('edit').argument('<id>', defaults.mode === 'ralph' ? 'Ralph job id' : 'Loop job id').description(defaults.mode === 'ralph' ? 'Update a Ralph job' : 'Update a Loop job').option('--template <id>', 'Apply a built-in job template before explicit overrides').option('--mode <mode>', 'Set loop mode: goal or ralph').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile').option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--token-budget <n>', 'Set soft Goal token budget').option('--clear-token-budget', 'Clear Goal token budget').option('--token-reserve <n>', 'Set pre-turn minimum remaining tokens').option('--clear-token-reserve', 'Clear Goal token reserve').option('--model <provider/model>', 'Set runtime model override, for example openai/gpt-5').option('--clear-model', 'Clear runtime model override').option('--thinking <level>', 'Set runtime thinking level override: off, minimal, low, medium, high, xhigh, max').option('--clear-thinking', 'Clear runtime thinking level override').option('--fast', 'Enable runtime fast mode').option('--no-fast', 'Disable runtime fast mode').option('--clear-fast', 'Clear runtime fast mode override').option('--json', 'Print JSON').action((id, options) => { const store = createDefaultPiboLoopStore({ path: program.opts().store }); const patch = { ...templatePatch(options.template) }; if (options.mode !== undefined)
149
+ program.command('edit').argument('<id>', defaults.mode === 'ralph' ? 'Ralph job id' : 'Loop job id').description(defaults.mode === 'ralph' ? 'Update a Ralph job' : 'Update a Loop job').option('--template <id>', 'Apply a built-in job template before explicit overrides').option('--mode <mode>', 'Set loop mode: goal or ralph').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile').option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--token-budget <n>', "Set the soft Goal token budget under the job's persisted accounting basis").option('--clear-token-budget', 'Clear Goal token budget').option('--token-reserve <n>', "Set the pre-turn minimum remaining tokens under the job's persisted accounting basis").option('--clear-token-reserve', 'Clear Goal token reserve').option('--model <provider/model>', 'Set runtime model override, for example openai/gpt-5').option('--clear-model', 'Clear runtime model override').option('--thinking <level>', 'Set runtime thinking level override: off, minimal, low, medium, high, xhigh, max').option('--clear-thinking', 'Clear runtime thinking level override').option('--fast', 'Enable runtime fast mode').option('--no-fast', 'Disable runtime fast mode').option('--clear-fast', 'Clear runtime fast mode override').option('--json', 'Print JSON').action((id, options) => { const store = createDefaultPiboLoopStore({ path: program.opts().store }); const patch = { ...templatePatch(options.template) }; if (options.mode !== undefined)
149
150
  patch.mode = loopMode(options.mode); if (options.name !== undefined)
150
151
  patch.name = options.name; if (options.description !== undefined)
151
152
  patch.description = options.description; if (options.profile !== undefined)
@@ -1,3 +1,4 @@
1
+ import { goalTokenAccounting } from './accounting.js';
1
2
  const completionMarkerInstruction = 'When and only when the full objective is proven complete, end with the XML completion marker on its own line. Compose it from the opening tag <promise>, the word COMPLETE, and the closing tag </promise>. Do not quote, negate, explain, or mention the literal marker before completion.';
2
3
  export function buildLoopTurnPrompt(job, continuation, goalToolsAvailable = true) {
3
4
  if (job.mode === 'ralph')
@@ -22,6 +23,11 @@ function buildGoalTurnPrompt(job, continuation, goalToolsAvailable) {
22
23
  const tokenBudget = job.tokenBudget;
23
24
  const remainingTokens = tokenBudget === undefined ? 'unbounded' : String(Math.max(0, tokenBudget - tokensUsed));
24
25
  const tokenReserve = job.tokenReserve ?? 0;
26
+ const tokenAccounting = goalTokenAccounting(job);
27
+ const tokenBasis = tokenAccounting.basis === 'uncached' ? 'uncached' : 'total';
28
+ const accountingPolicy = tokenAccounting.basis === 'uncached'
29
+ ? '- Cache-read and cache-write tokens do not consume the budget.'
30
+ : '- Legacy compatibility: cache-read and cache-write tokens remain included because prior persisted counters cannot be reconstructed safely.';
25
31
  return [
26
32
  continuation ? 'Continue working toward the active Pibo loop goal.' : 'Start working toward the active Pibo loop goal.',
27
33
  '',
@@ -37,11 +43,13 @@ function buildGoalTurnPrompt(job, continuation, goalToolsAvailable) {
37
43
  '- Temporary rough edges are acceptable while work moves toward the requested end state. Completion still requires the requested end state to be true and verified.',
38
44
  '',
39
45
  'Budget:',
40
- '- Budget enforcement: soft; model usage is reported after a response and the current turn can overshoot the limit.',
41
- `- Reported tokens used before this turn: ${tokensUsed}`,
42
- `- Soft token budget: ${tokenBudget ?? 'none'}`,
43
- `- Pre-turn token reserve: ${tokenReserve}`,
44
- `- Reported tokens remaining before this turn: ${remainingTokens}`,
46
+ `- Accounting basis: ${tokenBasis} tokens (version ${tokenAccounting.version}).`,
47
+ `- Budget enforcement: soft; ${tokenBasis} model usage is reported after a response and the current turn can overshoot the limit.`,
48
+ accountingPolicy,
49
+ `- Reported ${tokenBasis} tokens used before this turn: ${tokensUsed}`,
50
+ `- Soft ${tokenBasis} token budget: ${tokenBudget ?? 'none'}`,
51
+ `- Pre-turn ${tokenBasis} token reserve: ${tokenReserve}`,
52
+ `- Reported ${tokenBasis} tokens remaining before this turn: ${remainingTokens}`,
45
53
  '',
46
54
  'Work from evidence:',
47
55
  'Use the current workspace, repository, runtime, and external state as authoritative. Previous conversation context can help locate relevant work, but inspect current state before relying on it. Improve, replace, or remove existing work as needed to satisfy the actual objective.',
@@ -6,6 +6,7 @@ import { PiboDataStore } from '../data/pibo-store.js';
6
6
  import { ChatRoomService } from '../apps/chat/data/room-service.js';
7
7
  import { isPiboRoomArchived } from '../apps/chat/types/rooms.js';
8
8
  import { acquireBrowserPoolLease, browserPoolPaths, releaseBrowserPoolLease, restartRecordedBrowserPoolChrome } from '../tools/browser-pool.js';
9
+ import { goalBudgetTokens, goalTokenAccounting } from './accounting.js';
9
10
  import { createDefaultPiboLoopStore } from './store.js';
10
11
  import { createBuiltInLoopStopConditions, evaluateLoopStopPolicy } from './stopping.js';
11
12
  import { buildLoopTurnPrompt } from './prompts.js';
@@ -536,7 +537,8 @@ export class PiboLoopService {
536
537
  const job = this.store.getJob(run.jobId);
537
538
  if (!job || job.mode !== 'goal')
538
539
  return;
539
- this.store.recordGoalTurnUsage(job.id, run.id, event.totalTokens);
540
+ const basis = run.accounting?.tokenAccounting?.basis ?? goalTokenAccounting(job).basis;
541
+ this.store.recordGoalTurnUsage(job.id, run.id, goalBudgetTokens(event, basis));
540
542
  }
541
543
  handleProductEvent(event) {
542
544
  if (event.type !== 'pibo.loop.fact' && event.type !== 'loop.fact' && event.type !== 'pibo.ralph.fact' && event.type !== 'ralph.fact')
@@ -4,6 +4,7 @@ import { dirname, resolve } from 'node:path';
4
4
  import { DatabaseSync } from 'node:sqlite';
5
5
  import { piboHomePath } from '../core/pibo-home.js';
6
6
  import { isPiboThinkingLevel } from '../core/thinking.js';
7
+ import { newGoalTokenAccounting, normalizeLoopTokenAccounting } from './accounting.js';
7
8
  function nowIso(now = new Date()) { return now.toISOString(); }
8
9
  function parseJson(json) { return JSON.parse(json); }
9
10
  function defaultName(prompt) { const normalized = prompt.replace(/\s+/g, ' ').trim(); return normalized ? normalized.slice(0, 80) : 'Loop job'; }
@@ -80,7 +81,9 @@ function parseRunAccounting(json) {
80
81
  return undefined;
81
82
  try {
82
83
  const value = JSON.parse(json);
83
- return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
84
+ return value && typeof value === 'object' && !Array.isArray(value)
85
+ ? { ...value, tokenAccounting: normalizeLoopTokenAccounting(value.tokenAccounting) }
86
+ : undefined;
84
87
  }
85
88
  catch {
86
89
  return undefined;
@@ -104,7 +107,7 @@ function normalizeJobState(state, mode, enabled, createdAt) {
104
107
  return state;
105
108
  const activeTimeSeconds = Math.max(0, Math.floor(state.activeTimeSeconds ?? state.timeUsedSeconds ?? 0));
106
109
  const goalStartedAt = state.goalStartedAt ?? (enabled || (state.completedIterations ?? 0) > 0 || (state.tokensUsed ?? 0) > 0 || (state.goalStatus !== undefined && state.goalStatus !== 'paused') ? createdAt : undefined);
107
- const normalized = { ...state, activeTimeSeconds, ...(goalStartedAt ? { goalStartedAt } : {}) };
110
+ const normalized = { ...state, tokenAccounting: normalizeLoopTokenAccounting(state.tokenAccounting), activeTimeSeconds, ...(goalStartedAt ? { goalStartedAt } : {}) };
108
111
  delete normalized.timeUsedSeconds;
109
112
  return normalized;
110
113
  }
@@ -296,7 +299,7 @@ export class PiboLoopStore {
296
299
  const enabled = input.enabled === true;
297
300
  const state = {
298
301
  completedIterations: 0,
299
- ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: timestamp } : {}) } : {}),
302
+ ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokenAccounting: newGoalTokenAccounting(), tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: timestamp } : {}) } : {}),
300
303
  ...(input.initialPiboSessionId?.trim() ? { lastPiboSessionId: input.initialPiboSessionId.trim() } : {}),
301
304
  };
302
305
  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 };
@@ -422,7 +425,7 @@ export class PiboLoopStore {
422
425
  if (job?.mode === 'goal') {
423
426
  const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
424
427
  if (row) {
425
- const accounting = parseRunAccounting(row.accounting_json) ?? {};
428
+ const accounting = parseRunAccounting(row.accounting_json) ?? { tokenAccounting: normalizeLoopTokenAccounting(job.state.tokenAccounting) };
426
429
  const turnTokens = (accounting.tokensUsed ?? 0) + Math.max(0, Math.floor(tokens));
427
430
  const budget = accounting.tokenBudget;
428
431
  const before = accounting.tokensUsedBefore ?? 0;
@@ -446,7 +449,7 @@ export class PiboLoopStore {
446
449
  if (job?.mode === 'goal') {
447
450
  const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
448
451
  if (row) {
449
- const accounting = { ...(parseRunAccounting(row.accounting_json) ?? {}), activeTimeSeconds: seconds };
452
+ const accounting = { ...(parseRunAccounting(row.accounting_json) ?? { tokenAccounting: normalizeLoopTokenAccounting(job.state.tokenAccounting) }), activeTimeSeconds: seconds };
450
453
  this.db.prepare('UPDATE pibo_ralph_runs SET accounting_json = ?, updated_at = ? WHERE id = ?').run(runAccountingJson(accounting), nowIso(now), runId);
451
454
  }
452
455
  }
@@ -481,7 +484,7 @@ export class PiboLoopStore {
481
484
  const enabled = patch.enabled ?? existing.enabled;
482
485
  let state = mode === existing.mode
483
486
  ? { ...existing.state }
484
- : { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: nowIso(now) } : {}) } : {}) };
487
+ : { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokenAccounting: newGoalTokenAccounting(), tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: nowIso(now) } : {}) } : {}) };
485
488
  if (mode === 'goal' && patch.enabled !== undefined) {
486
489
  const currentGoalStatus = goalStatus({ mode, enabled: existing.enabled, state: existing.state }) ?? 'paused';
487
490
  if (patch.enabled) {
@@ -807,6 +810,7 @@ export class PiboLoopStore {
807
810
  createRunLocked(job, timestamp) {
808
811
  const tokensUsedBefore = job.state.tokensUsed ?? 0;
809
812
  const accounting = job.mode === 'goal' ? {
813
+ tokenAccounting: normalizeLoopTokenAccounting(job.state.tokenAccounting),
810
814
  ...(job.tokenBudget !== undefined ? { tokenBudget: job.tokenBudget, remainingTokensBefore: Math.max(0, job.tokenBudget - tokensUsedBefore) } : {}),
811
815
  ...(job.tokenReserve !== undefined ? { tokenReserve: job.tokenReserve } : {}),
812
816
  tokensUsedBefore,
@@ -1,7 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { piboStringEnum } from "../tools/schema.js";
3
3
  import { definePiboTool } from "../tools/contract.js";
4
- import { goalActiveTimeSeconds, goalCanStartNextTurn, goalElapsedWallClockSeconds, goalRemainingTokens } from './accounting.js';
4
+ import { goalActiveTimeSeconds, goalCanStartNextTurn, goalElapsedWallClockSeconds, goalRemainingTokens, goalTokenAccounting } from './accounting.js';
5
5
  import { createDefaultPiboLoopStore } from './store.js';
6
6
  export const PIBO_GOAL_TOOL_NAMES = ['get_goal', 'create_goal', 'update_goal'];
7
7
  let configuredStorePath;
@@ -68,11 +68,13 @@ function nonNegativeInteger(value, field) {
68
68
  }
69
69
  function goalPayload(job) {
70
70
  const tokenBudget = job.tokenBudget;
71
+ const tokenAccounting = goalTokenAccounting(job);
71
72
  return {
72
73
  goalId: job.id,
73
74
  objective: job.prompt,
74
75
  status: effectiveGoalStatus(job),
75
76
  budgetType: tokenBudget === undefined ? 'unbounded' : 'soft',
77
+ tokenAccounting,
76
78
  tokenBudget: tokenBudget ?? null,
77
79
  tokenReserve: job.tokenReserve ?? 0,
78
80
  tokensUsed: job.state.tokensUsed ?? 0,
@@ -130,8 +132,8 @@ function createCreateGoalTool(context, options) {
130
132
  promptSnippet: 'Call create_goal only when the user or system explicitly requests a persistent goal. Do not infer a goal from an ordinary task.',
131
133
  inputSchema: Type.Object({
132
134
  objective: Type.String({ description: 'Concrete objective to pursue across automatic continuations.' }),
133
- token_budget: Type.Optional(Type.Number({ description: 'Optional soft token budget. Usage arrives after each model response, so the final turn can overshoot.' })),
134
- token_reserve: Type.Optional(Type.Number({ description: 'Optional non-negative minimum remaining tokens required before Pibo starts another turn.' })),
135
+ token_budget: Type.Optional(Type.Number({ description: 'Optional soft uncached-token budget. Cache reads and writes are excluded. Usage arrives after each model response, so the final turn can overshoot.' })),
136
+ token_reserve: Type.Optional(Type.Number({ description: 'Optional non-negative minimum remaining uncached tokens required before Pibo starts another turn.' })),
135
137
  }),
136
138
  async execute(_toolCallId, params) {
137
139
  try {
@@ -182,11 +184,12 @@ function createUpdateGoalTool(context, options) {
182
184
  const job = store.updateGoalStatus(existing.id, status);
183
185
  if (!job)
184
186
  throw new Error('goal no longer exists');
187
+ const tokenBasis = goalTokenAccounting(job).basis;
185
188
  return toolResult({
186
189
  ok: true,
187
190
  goal: goalPayload(job),
188
191
  ...(status === 'complete' && job.tokenBudget !== undefined
189
- ? { completionBudgetReport: `${job.state.tokensUsed ?? 0}/${job.tokenBudget} reported tokens consumed against a soft budget before the current model turn finishes` }
192
+ ? { completionBudgetReport: `${job.state.tokensUsed ?? 0}/${job.tokenBudget} reported ${tokenBasis} tokens consumed against a soft budget before the current model turn finishes` }
190
193
  : {}),
191
194
  });
192
195
  });
@@ -36,8 +36,9 @@ MCP config lookup order:
36
36
  1. -c/--config <path>
37
37
  2. MCP_CONFIG_PATH
38
38
  3. ./mcp_servers.json
39
- 4. ~/.mcp_servers.json
40
- 5. ~/.config/mcp/mcp_servers.json
39
+ 4. ~/mcp_servers.json
40
+ 5. ~/.mcp_servers.json
41
+ 6. ~/.config/mcp/mcp_servers.json
41
42
  `);
42
43
  }
43
44
  export function printConfigSchema() {
@@ -5,7 +5,7 @@ import { createHash } from 'node:crypto';
5
5
  import { existsSync } from 'node:fs';
6
6
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
7
7
  import { homedir } from 'node:os';
8
- import { dirname, join, resolve } from 'node:path';
8
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
9
9
  import { ErrorCode, configInvalidJsonError, configMissingFieldError, configNotFoundError, configSearchError, formatCliError, serverNotFoundError, } from './errors.js';
10
10
  export const DEFAULT_MCP_CONFIG_FILE = 'mcp_servers.json';
11
11
  // ============================================================================
@@ -306,9 +306,15 @@ export function getDefaultConfigPaths() {
306
306
  const home = homedir();
307
307
  // Current directory
308
308
  paths.push(resolve(DEFAULT_MCP_CONFIG_FILE));
309
- // Home directory variants
310
- paths.push(join(home, '.mcp_servers.json'));
311
- paths.push(join(home, '.config', 'mcp', 'mcp_servers.json'));
309
+ // Home directory variants. Include the non-dot filename because `pibo mcp
310
+ // config` creates it when invoked from the home directory, and services can
311
+ // later run with a different working directory. Ignore empty or relative
312
+ // platform home values instead of treating them as paths below the cwd.
313
+ if (home && isAbsolute(home)) {
314
+ paths.push(join(home, DEFAULT_MCP_CONFIG_FILE));
315
+ paths.push(join(home, '.mcp_servers.json'));
316
+ paths.push(join(home, '.config', 'mcp', 'mcp_servers.json'));
317
+ }
312
318
  return paths;
313
319
  }
314
320
  export function getConfigSearchPaths(explicitPath) {
@@ -24,7 +24,7 @@ export function configSearchError() {
24
24
  code: ErrorCode.CLIENT_ERROR,
25
25
  type: 'CONFIG_NOT_FOUND',
26
26
  message: 'No mcp_servers.json found in search paths',
27
- details: 'Searched: ./mcp_servers.json, ~/.mcp_servers.json, ~/.config/mcp/mcp_servers.json',
27
+ details: 'Searched: ./mcp_servers.json, ~/mcp_servers.json, ~/.mcp_servers.json, ~/.config/mcp/mcp_servers.json',
28
28
  suggestion: 'Create mcp_servers.json in current directory or use -c/--config to specify path',
29
29
  };
30
30
  }