@pasko70/pibo 2.4.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 (33) 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.4.0.vsix → pibo-vscode-ext-2.4.1.vsix} +0 -0
  16. package/dist/gateway/web.js +8 -1
  17. package/dist/loops/accounting.js +27 -0
  18. package/dist/loops/cli.js +6 -5
  19. package/dist/loops/prompts.js +13 -5
  20. package/dist/loops/service.js +3 -1
  21. package/dist/loops/store.js +10 -6
  22. package/dist/loops/tools.js +7 -4
  23. package/dist/mcp/config-command.js +3 -2
  24. package/dist/mcp/config.js +10 -4
  25. package/dist/mcp/errors.js +1 -1
  26. package/dist/mcp/index.js +19 -33
  27. package/dist/session-ui/sessionActivity.js +6 -2
  28. package/dist/signals/status.js +9 -4
  29. package/dist/tools/guides.js +1 -1
  30. package/dist/web/channel.js +10 -4
  31. package/npm-shrinkwrap.json +2 -2
  32. package/package.json +1 -1
  33. package/skills/builtin/loop/SKILL.md +1 -1
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101d22" />
7
7
  <title>Pibo</title>
8
- <script type="module" crossorigin src="/apps/chat-vscode/assets/index-WsLm1mo3.js"></script>
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-DvTSSvzN.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-CF1-9PKU.css">
10
10
  </head>
11
11
  <body>
@@ -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
  }
package/dist/mcp/index.js CHANGED
@@ -9,9 +9,8 @@
9
9
  * mcp-cli call <server> <tool> Call tool (reads JSON from stdin if no args)
10
10
  * mcp-cli call <server> <tool> {} Call tool with JSON args
11
11
  */
12
- import { readFileSync } from 'node:fs';
13
12
  import { configCommand } from './config-command.js';
14
- import { ensureConfigExists, } from './config.js';
13
+ import { ensureConfigExists, listServerNames, loadConfigUnresolved, } from './config.js';
15
14
  import { ErrorCode, ambiguousCommandError, formatCliError, missingArgumentError, tooManyArgumentsError, unknownOptionError, unknownSubcommandError, } from './errors.js';
16
15
  import { registryCommand } from './registry.js';
17
16
  import { VERSION } from './version.js';
@@ -127,37 +126,8 @@ function parseArgs(args) {
127
126
  result.command = 'info';
128
127
  const remaining = positional.slice(1);
129
128
  const { server, tool } = parseServerTool(remaining);
130
- // info requires a server argument - show available servers in error
131
129
  if (!server) {
132
- // Try to load config synchronously to show available servers
133
- let availableServers = [];
134
- const configPaths = [
135
- result.configPath,
136
- process.env.MCP_CONFIG_PATH,
137
- './mcp_servers.json',
138
- `${process.env.HOME}/.mcp_servers.json`,
139
- `${process.env.HOME}/.config/mcp/mcp_servers.json`,
140
- ].filter(Boolean);
141
- for (const cfgPath of configPaths) {
142
- try {
143
- const content = readFileSync(cfgPath, 'utf-8');
144
- const config = JSON.parse(content);
145
- if (config.mcpServers) {
146
- availableServers = Object.keys(config.mcpServers);
147
- break;
148
- }
149
- }
150
- catch {
151
- // Try next path
152
- }
153
- }
154
- const serverList = availableServers.length > 0
155
- ? availableServers.join(', ')
156
- : '(none found)';
157
- console.error('Error [MISSING_ARGUMENT]: Missing required argument for info: server');
158
- console.error(` Available servers: ${serverList}`);
159
- console.error(` Suggestion: Use 'pibo mcp info <server>' to see server details, or just 'pibo mcp' to list all`);
160
- process.exit(ErrorCode.CLIENT_ERROR);
130
+ return result;
161
131
  }
162
132
  result.server = server;
163
133
  result.tool = tool;
@@ -371,10 +341,26 @@ export async function runMcpCli(argv = process.argv) {
371
341
  }
372
342
  break;
373
343
  case 'info':
344
+ if (!args.server) {
345
+ let availableServers = [];
346
+ try {
347
+ availableServers = listServerNames(await loadConfigUnresolved(args.configPath));
348
+ }
349
+ catch {
350
+ // Keep the missing-argument error focused when no valid config exists.
351
+ }
352
+ const serverList = availableServers.length > 0
353
+ ? availableServers.join(', ')
354
+ : '(none found)';
355
+ console.error('Error [MISSING_ARGUMENT]: Missing required argument for info: server');
356
+ console.error(` Available servers: ${serverList}`);
357
+ console.error(` Suggestion: Use 'pibo mcp info <server>' to see server details, or just 'pibo mcp' to list all`);
358
+ process.exitCode = ErrorCode.CLIENT_ERROR;
359
+ break;
360
+ }
374
361
  await ensureConfigExists(args.configPath);
375
362
  {
376
363
  const { infoCommand } = await import('./commands/info.js');
377
- // info always has a server (validated in parseArgs)
378
364
  await infoCommand({
379
365
  target: buildTarget(args.server, args.tool),
380
366
  withDescriptions: args.withDescriptions,
@@ -1,12 +1,16 @@
1
+ import { resolveSessionSignalStatus } from "../signals/status.js";
1
2
  export function resolveSessionActivity(signal, fallback = {}) {
2
3
  if (!signal)
3
4
  return fallbackSessionActivity(fallback);
4
5
  const latestTurn = signal.latestTurn;
5
6
  const isTurnActive = latestTurn?.state === "running";
6
7
  const isTreeActive = signal.isTreeActive || isTurnActive;
7
- const hasError = signal.hasError || signal.hasErrorDescendant || signal.aggregateStatus === "error";
8
8
  return {
9
- status: hasError ? "error" : isTreeActive ? "running" : "idle",
9
+ status: resolveSessionSignalStatus({
10
+ isTreeActive,
11
+ localStatus: signal.localStatus,
12
+ latestTurn,
13
+ }),
10
14
  isTreeActive,
11
15
  isTurnActive,
12
16
  activeTurnId: isTurnActive ? latestTurn.eventId : undefined,
@@ -1,12 +1,17 @@
1
+ export function resolveSessionSignalStatus(snapshot) {
2
+ if (snapshot.isTreeActive || snapshot.latestTurn?.state === "running")
3
+ return "running";
4
+ if (snapshot.localStatus === "error" || snapshot.latestTurn?.state === "failed")
5
+ return "error";
6
+ return "idle";
7
+ }
1
8
  export function summarizeSessionSignalStatus(snapshot) {
2
- const isTurnActive = snapshot.latestTurn?.state === "running";
3
- const hasError = snapshot.hasError || snapshot.hasErrorDescendant || snapshot.aggregateStatus === "error";
4
- const isTreeActive = snapshot.isTreeActive || isTurnActive;
9
+ const isTreeActive = snapshot.isTreeActive || snapshot.latestTurn?.state === "running";
5
10
  return {
6
11
  piboSessionId: snapshot.piboSessionId,
7
12
  rootPiboSessionId: snapshot.rootPiboSessionId,
8
13
  updatedAt: snapshot.updatedAt,
9
- status: isTreeActive ? "running" : hasError ? "error" : "idle",
14
+ status: resolveSessionSignalStatus(snapshot),
10
15
  isTreeActive,
11
16
  };
12
17
  }
@@ -46,7 +46,7 @@ pibo loop stop <job-id>
46
46
  pibo loop cancel <job-id>
47
47
  \`\`\`
48
48
 
49
- Token budgets count usage reported by completed assistant model messages. One request can overshoot because usage is known after the response returns.
49
+ New Goals persist token-accounting version \`1\` with basis \`uncached\`: their budgets count uncached input and output usage reported by completed assistant model messages, while cache-read and cache-write tokens remain telemetry and do not consume the budget. Legacy persisted Goal jobs and runs without an accounting descriptor remain on version \`1\` basis \`total\`, including cache reads and writes, so their existing counters are neither relabeled nor numerically reconstructed without source data. Inspect \`pibo loop list --all --json\` and \`pibo loop runs --job <job-id> --json\` for the persisted basis. Both bases are soft because usage arrives after the response, so one request can overshoot.
50
50
  `,
51
51
  };
52
52
  export const RALPH_GUIDE = {
@@ -284,11 +284,17 @@ export function createWebHostChannel(options = {}) {
284
284
  await sendResponse(nodeResponse, response ?? notFound());
285
285
  return;
286
286
  }
287
- if (url.pathname === "/" && apps[0]) {
288
- await sendResponse(nodeResponse, redirect(apps[0].mountPath));
289
- return;
290
- }
291
287
  if (url.pathname === "/") {
288
+ const landingApp = options.landingAppName
289
+ ? apps.find((candidate) => candidate.name === options.landingAppName)
290
+ : apps[0];
291
+ if (options.landingAppName && !landingApp) {
292
+ throw new Error(`Configured landing web app "${options.landingAppName}" is not registered`);
293
+ }
294
+ if (landingApp) {
295
+ await sendResponse(nodeResponse, redirect(`${landingApp.mountPath}${url.search}`));
296
+ return;
297
+ }
292
298
  await sendResponse(nodeResponse, responseHtml("<!doctype html><title>Pibo</title><p>No web apps registered.</p>"));
293
299
  return;
294
300
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "2.4.0",
9
+ "version": "2.4.1",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "packages/workflows"
@@ -51,7 +51,7 @@ Prefer creating the job stopped when its prompt, target, profile, or safety boun
51
51
 
52
52
  ## Token budgets
53
53
 
54
- Goal token budgets are soft: Pibo accumulates usage reported after model responses, so the final turn can overshoot. Each Goal run records tokens used before the turn, remaining tokens before the turn, turn usage, and overshoot.
54
+ Goal token budgets are soft because usage arrives after model responses, so the final turn can overshoot. New Goals use versioned `uncached` accounting: cache-read and cache-write tokens do not consume the budget. Legacy persisted Goals without an accounting descriptor remain on `total` accounting, including cache usage, because their existing counters cannot be reconstructed safely. Job status and each Goal run expose the accounting basis together with tokens used before the turn, remaining tokens, turn usage, and overshoot.
55
55
 
56
56
  Set `--token-reserve <n>` to require more than `n` tokens to remain before Pibo starts another turn. Increase or clear the budget, or lower the reserve, before resuming a budget-limited Goal.
57
57