@pasko70/pibo 2.4.0 → 2.4.2

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 (39) hide show
  1. package/dist/agent-runtime/routed-session.js +151 -7
  2. package/dist/agent-runtimes/codex-native/turn.js +3 -1
  3. package/dist/agent-runtimes/omp/turn.js +36 -7
  4. package/dist/agent-runtimes/pi/routed-session.js +62 -16
  5. package/dist/apps/chat/web-app.js +6 -8
  6. package/dist/apps/chat-ui/assets/{dist-CUcAofmV.js → dist-3YG57JXi.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-Byygd1lH.js → dist-BeqHbnGN.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-DusFwy0L.js → dist-CrDtveZB.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-C9BrS7sL.js → dist-Cw9po47P.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-D4RU6xu3.js → dist-DTRjeLwO.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{index-Bifi_kjN.js → index-AjnP3ci-.js} +89 -89
  12. package/dist/apps/chat-ui/index.html +1 -1
  13. package/dist/apps/chat-vscode-web/assets/{index-WsLm1mo3.js → index-DvTSSvzN.js} +5 -5
  14. package/dist/apps/chat-vscode-web/index.html +1 -1
  15. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  16. package/dist/apps/vscode-artifacts/{pibo-vscode-ext-2.4.0.vsix → pibo-vscode-ext-2.4.2.vsix} +0 -0
  17. package/dist/core/session-router.js +153 -63
  18. package/dist/gateway/web.js +8 -1
  19. package/dist/loops/accounting.js +27 -0
  20. package/dist/loops/cli.js +6 -5
  21. package/dist/loops/prompts.js +13 -5
  22. package/dist/loops/service.js +8 -2
  23. package/dist/loops/store.js +27 -11
  24. package/dist/loops/tools.js +10 -5
  25. package/dist/mcp/config-command.js +3 -2
  26. package/dist/mcp/config.js +10 -4
  27. package/dist/mcp/errors.js +1 -1
  28. package/dist/mcp/index.js +19 -33
  29. package/dist/reliability/store.js +11 -6
  30. package/dist/runs/lifecycle.js +12 -0
  31. package/dist/runs/registry.js +23 -4
  32. package/dist/runs/tools.js +12 -3
  33. package/dist/session-ui/sessionActivity.js +6 -2
  34. package/dist/signals/status.js +9 -4
  35. package/dist/tools/guides.js +1 -1
  36. package/dist/web/channel.js +10 -4
  37. package/npm-shrinkwrap.json +2 -2
  38. package/package.json +1 -1
  39. 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>
@@ -5,7 +5,7 @@ import { RuntimeRoutedSession as RoutedSession, } from "../agent-runtime/routed-
5
5
  import { runtimeSessionErrorDetails } from "./session-errors.js";
6
6
  import { normalizePiboAgentObservationCursor, normalizePiboAgentObservationLimit, normalizePiboAgentObservationOrder, parsePiboAgentObservationTimestamp, piboAgentObservationDetails, piboAgentObservationKind, piboAgentObservationRole, piboAgentObservationSourceFromEvent, piboAgentObservationText, } from "../subagents/observations.js";
7
7
  import { PiboRunRegistry } from "../runs/registry.js";
8
- import { PiboRunExecutionTimeoutError } from "../runs/lifecycle.js";
8
+ import { PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError } from "../runs/lifecycle.js";
9
9
  import { PiboRunResourceLimitError } from "../runs/resource-isolation.js";
10
10
  import { createPiboSignalRegistry } from "../signals/registry.js";
11
11
  import { createDefaultPiboReliabilityStore } from "../reliability/store.js";
@@ -150,7 +150,29 @@ function formatRunReminderMessage(notification) {
150
150
  ].join("\n");
151
151
  }
152
152
  function isRunReminderServiceMessage(event) {
153
- return event.source === "service" && event.capabilityScope === "run-reminder";
153
+ return event.source === "service" && event.text.startsWith("<pibo_run_notification>");
154
+ }
155
+ function yieldedRunOrigin(event) {
156
+ if (!event?.id || event.provenance?.kind !== "loop-run")
157
+ return undefined;
158
+ return {
159
+ eventId: event.provenance.rootEventId ?? event.id,
160
+ provenance: {
161
+ kind: event.provenance.kind,
162
+ jobId: event.provenance.jobId,
163
+ runId: event.provenance.runId,
164
+ },
165
+ };
166
+ }
167
+ function runReminderProvenance(notification) {
168
+ const origin = notification.origin;
169
+ if (!origin || origin.provenance.kind !== "loop-run")
170
+ return undefined;
171
+ return {
172
+ ...origin.provenance,
173
+ cause: "run-reminder",
174
+ rootEventId: origin.eventId,
175
+ };
154
176
  }
155
177
  function isTerminalRunStatus(status) {
156
178
  return status === "completed" || status === "failed" || status === "timed_out" || status === "cancelled";
@@ -235,7 +257,7 @@ export class PiboSessionRouter {
235
257
  portableHistoryProvider;
236
258
  runtimeResourceSessions = new Map();
237
259
  runtimeAuthFingerprints = new Map();
238
- activeSubagentChildren = new Map();
260
+ activeSubagentRequests = new Map();
239
261
  agentObservations = [];
240
262
  agentObservationEvictedThroughByParent = new Map();
241
263
  nextAgentObservationSequence = 1;
@@ -388,11 +410,24 @@ export class PiboSessionRouter {
388
410
  teardownCompleted = true;
389
411
  return output;
390
412
  }
391
- const childAbort = event.action === "abort"
392
- ? this.abortActiveSubagentSessions(event.piboSessionId)
393
- : undefined;
394
- const output = await session.executeAction(event);
395
- await childAbort;
413
+ let output;
414
+ if (event.action === "abort") {
415
+ const [sessionAbort, childAbort] = await Promise.allSettled([
416
+ session.executeAction(event),
417
+ this.abortActiveSubagentSessions(event.piboSessionId),
418
+ ]);
419
+ if (sessionAbort.status === "rejected" && childAbort.status === "rejected") {
420
+ throw new AggregateError([sessionAbort.reason, childAbort.reason], "Failed to abort the session and its active subagent requests.");
421
+ }
422
+ if (sessionAbort.status === "rejected")
423
+ throw sessionAbort.reason;
424
+ if (childAbort.status === "rejected")
425
+ throw childAbort.reason;
426
+ output = sessionAbort.value;
427
+ }
428
+ else {
429
+ output = await session.executeAction(event);
430
+ }
396
431
  if (event.action === "kill" || event.action === "kill_all") {
397
432
  await this.disposeSessionSubtree(event.piboSessionId, `${event.action} action`, { cancelRuns: event.action === "kill_all" });
398
433
  teardownCompleted = true;
@@ -837,20 +872,10 @@ export class PiboSessionRouter {
837
872
  const eventWithId = { ...event, id: event.id ?? randomUUID() };
838
873
  return await new Promise((resolve, reject) => {
839
874
  let settled = false;
840
- let messageDispatched = false;
875
+ let dispatchPromise;
841
876
  let lastAssistantMessage;
842
877
  let timeout;
843
- const abortChild = () => {
844
- if (!messageDispatched)
845
- return;
846
- void this.emit({
847
- type: "execution",
848
- piboSessionId: eventWithId.piboSessionId,
849
- action: "abort",
850
- id: randomUUID(),
851
- }).catch(() => { });
852
- };
853
- const finish = (result) => {
878
+ const claimSettlement = () => {
854
879
  if (settled)
855
880
  return false;
856
881
  settled = true;
@@ -858,16 +883,33 @@ export class PiboSessionRouter {
858
883
  clearTimeout(timeout);
859
884
  signal?.removeEventListener("abort", onAbort);
860
885
  unsubscribe();
886
+ return true;
887
+ };
888
+ const finish = (result) => {
889
+ if (!claimSettlement())
890
+ return;
861
891
  if (result instanceof Error)
862
892
  reject(result);
863
893
  else
864
894
  resolve(result);
865
- return true;
866
895
  };
867
- const onAbort = () => {
868
- if (!finish(subagentAbortError()))
896
+ const rejectAfterMessageCancellation = (error) => {
897
+ if (!claimSettlement())
869
898
  return;
870
- abortChild();
899
+ void (async () => {
900
+ try {
901
+ await dispatchPromise;
902
+ await this.cancelSessionMessage(eventWithId.piboSessionId, eventWithId.id);
903
+ }
904
+ catch (cancellationError) {
905
+ reject(new PiboRunCancellationError(`Failed to cancel subagent request "${eventWithId.id}" in Pibo session "${eventWithId.piboSessionId}".`, { cause: cancellationError }));
906
+ return;
907
+ }
908
+ reject(error);
909
+ })();
910
+ };
911
+ const onAbort = () => {
912
+ rejectAfterMessageCancellation(subagentAbortError());
871
913
  };
872
914
  const unsubscribe = this.subscribe((output) => {
873
915
  if (output.piboSessionId !== eventWithId.piboSessionId ||
@@ -886,20 +928,25 @@ export class PiboSessionRouter {
886
928
  }
887
929
  });
888
930
  if (signal?.aborted) {
889
- onAbort();
931
+ finish(subagentAbortError());
890
932
  return;
891
933
  }
892
934
  signal?.addEventListener("abort", onAbort, { once: true });
893
935
  timeout = setTimeout(() => {
894
- const timeoutError = new Error(`Timed out waiting for assistant reply from Pibo session "${eventWithId.piboSessionId}"`);
895
- if (!finish(timeoutError))
896
- return;
897
- abortChild();
936
+ rejectAfterMessageCancellation(new PiboRunExecutionTimeoutError(`Timed out waiting for assistant reply from Pibo session "${eventWithId.piboSessionId}"`, "lifetime"));
898
937
  }, timeoutMs);
899
- messageDispatched = true;
900
- this.emit(eventWithId).catch(finish);
938
+ dispatchPromise = this.emit(eventWithId);
939
+ dispatchPromise.catch((error) => {
940
+ finish(error instanceof Error ? error : new Error(String(error)));
941
+ });
901
942
  });
902
943
  }
944
+ async cancelSessionMessage(piboSessionId, eventId) {
945
+ const session = this.sessions.get(piboSessionId);
946
+ if (!session || !await session.cancelMessage(eventId)) {
947
+ throw new Error(`Pibo session "${piboSessionId}" no longer owns message "${eventId}".`);
948
+ }
949
+ }
903
950
  async disposeAll() {
904
951
  if (this.disposePromise)
905
952
  return this.disposePromise;
@@ -946,7 +993,7 @@ export class PiboSessionRouter {
946
993
  await this.portableToolService.dispose();
947
994
  await this.runtimeResourceService.dispose();
948
995
  this.runtimeResourceSessions.clear();
949
- this.activeSubagentChildren.clear();
996
+ this.activeSubagentRequests.clear();
950
997
  this.agentObservations.length = 0;
951
998
  this.agentObservationEvictedThroughByParent.clear();
952
999
  await this.telemetryWriter?.dispose();
@@ -1562,44 +1609,40 @@ export class PiboSessionRouter {
1562
1609
  this.signalRegistry.project({ type: "session_created", session: created });
1563
1610
  return created;
1564
1611
  }
1565
- trackActiveSubagent(parentPiboSessionId, childPiboSessionId) {
1566
- let children = this.activeSubagentChildren.get(parentPiboSessionId);
1567
- if (!children) {
1568
- children = new Map();
1569
- this.activeSubagentChildren.set(parentPiboSessionId, children);
1612
+ trackActiveSubagent(parentPiboSessionId, request) {
1613
+ let requests = this.activeSubagentRequests.get(parentPiboSessionId);
1614
+ if (!requests) {
1615
+ requests = new Set();
1616
+ this.activeSubagentRequests.set(parentPiboSessionId, requests);
1570
1617
  }
1571
- children.set(childPiboSessionId, (children.get(childPiboSessionId) ?? 0) + 1);
1618
+ requests.add(request);
1572
1619
  let active = true;
1573
1620
  return () => {
1574
1621
  if (!active)
1575
1622
  return;
1576
1623
  active = false;
1577
- const current = this.activeSubagentChildren.get(parentPiboSessionId);
1624
+ const current = this.activeSubagentRequests.get(parentPiboSessionId);
1578
1625
  if (!current)
1579
1626
  return;
1580
- const remaining = (current.get(childPiboSessionId) ?? 1) - 1;
1581
- if (remaining > 0)
1582
- current.set(childPiboSessionId, remaining);
1583
- else
1584
- current.delete(childPiboSessionId);
1627
+ current.delete(request);
1585
1628
  if (current.size === 0)
1586
- this.activeSubagentChildren.delete(parentPiboSessionId);
1629
+ this.activeSubagentRequests.delete(parentPiboSessionId);
1587
1630
  };
1588
1631
  }
1589
1632
  async abortActiveSubagentSessions(parentPiboSessionId) {
1590
- const childIds = [...(this.activeSubagentChildren.get(parentPiboSessionId)?.keys() ?? [])];
1591
- if (childIds.length === 0)
1592
- return;
1593
- await Promise.allSettled(childIds.map(async (childPiboSessionId) => await this.emit({
1594
- type: "execution",
1595
- piboSessionId: childPiboSessionId,
1596
- action: "abort",
1597
- id: randomUUID(),
1598
- })));
1633
+ const requests = [...(this.activeSubagentRequests.get(parentPiboSessionId) ?? [])];
1634
+ for (const request of requests)
1635
+ request.abortController.abort();
1636
+ const settlements = await Promise.all(requests.map(async (request) => await request.settled));
1637
+ const failures = settlements.flatMap((settlement) => settlement.status === "rejected" ? [settlement.reason] : []);
1638
+ if (failures.length > 0)
1639
+ throw new AggregateError(failures, "Failed to cancel active subagent requests.");
1599
1640
  }
1600
1641
  createAgentsController(parentPiboSessionId) {
1601
1642
  return {
1602
1643
  sendMessage: async ({ subagent, message, threadKey, toolCallId, signal }) => {
1644
+ if (signal?.aborted)
1645
+ throw subagentAbortError();
1603
1646
  this.assertSubagentDepth(parentPiboSessionId, subagent);
1604
1647
  const child = this.resolveSubagentSession(parentPiboSessionId, subagent, threadKey);
1605
1648
  const resolvedThreadKey = typeof child.metadata?.threadKey === "string" ? child.metadata.threadKey : "";
@@ -1619,9 +1662,21 @@ export class PiboSessionRouter {
1619
1662
  childPiboSessionId: child.id,
1620
1663
  threadKey: resolvedThreadKey,
1621
1664
  });
1622
- const untrack = this.trackActiveSubagent(parentPiboSessionId, child.id);
1665
+ const parentAbortController = new AbortController();
1666
+ const requestSignal = signal
1667
+ ? AbortSignal.any([signal, parentAbortController.signal])
1668
+ : parentAbortController.signal;
1669
+ let resolveSettled;
1670
+ const settled = new Promise((resolve) => {
1671
+ resolveSettled = resolve;
1672
+ });
1673
+ const untrack = this.trackActiveSubagent(parentPiboSessionId, {
1674
+ abortController: parentAbortController,
1675
+ settled,
1676
+ });
1677
+ let settlement = { status: "fulfilled" };
1623
1678
  try {
1624
- const reply = await this.emitMessageAndWaitForReply(event, subagent.timeoutMs ?? DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS, signal);
1679
+ const reply = await this.emitMessageAndWaitForReply(event, subagent.timeoutMs ?? DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS, requestSignal);
1625
1680
  return {
1626
1681
  agentId: child.id,
1627
1682
  name: subagent.name,
@@ -1631,8 +1686,17 @@ export class PiboSessionRouter {
1631
1686
  reply,
1632
1687
  };
1633
1688
  }
1689
+ catch (error) {
1690
+ const confirmedParentCancellation = parentAbortController.signal.aborted
1691
+ && error instanceof Error
1692
+ && error.name === "AbortError";
1693
+ if (!confirmedParentCancellation)
1694
+ settlement = { status: "rejected", reason: error };
1695
+ throw error;
1696
+ }
1634
1697
  finally {
1635
1698
  untrack();
1699
+ resolveSettled?.(settlement);
1636
1700
  }
1637
1701
  },
1638
1702
  listAgents: () => this.listManagedAgents(parentPiboSessionId),
@@ -1767,8 +1831,9 @@ export class PiboSessionRouter {
1767
1831
  const cancel = this.runCancellationHandlers.get(run.runId);
1768
1832
  if (!cancel)
1769
1833
  return;
1770
- this.runCancellationHandlers.delete(run.runId);
1771
1834
  await cancel();
1835
+ if (this.runCancellationHandlers.get(run.runId) === cancel)
1836
+ this.runCancellationHandlers.delete(run.runId);
1772
1837
  }));
1773
1838
  const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1774
1839
  if (failures.length > 0)
@@ -1796,14 +1861,32 @@ export class PiboSessionRouter {
1796
1861
  timeoutMs,
1797
1862
  serviceWarning,
1798
1863
  resources,
1864
+ origin: yieldedRunOrigin(this.sessions.get(parentPiboSessionId)?.getActiveMessage?.()),
1799
1865
  });
1800
1866
  }
1801
1867
  catch (error) {
1802
1868
  admission.release();
1803
1869
  throw error;
1804
1870
  }
1805
- if (cancel)
1806
- this.runCancellationHandlers.set(run.runId, cancel);
1871
+ const cancellation = { state: "none" };
1872
+ if (cancel) {
1873
+ this.runCancellationHandlers.set(run.runId, async () => {
1874
+ cancellation.state = "pending";
1875
+ let resolveDecision;
1876
+ cancellation.decision = new Promise((resolve) => { resolveDecision = resolve; });
1877
+ try {
1878
+ await cancel();
1879
+ cancellation.state = "confirmed";
1880
+ }
1881
+ catch (error) {
1882
+ cancellation.state = "failed";
1883
+ throw error;
1884
+ }
1885
+ finally {
1886
+ resolveDecision?.();
1887
+ }
1888
+ });
1889
+ }
1807
1890
  void (async () => {
1808
1891
  try {
1809
1892
  const result = await execute();
@@ -1814,6 +1897,12 @@ export class PiboSessionRouter {
1814
1897
  this.handleTerminalRunReminder(parentPiboSessionId, completed.runId, reminderGeneration);
1815
1898
  }
1816
1899
  catch (error) {
1900
+ if (error instanceof PiboRunCancelledError) {
1901
+ if (cancellation.state === "pending")
1902
+ await cancellation.decision;
1903
+ if (cancellation.state === "confirmed")
1904
+ return;
1905
+ }
1817
1906
  const message = error instanceof Error ? error.message : String(error);
1818
1907
  if (resources)
1819
1908
  this.runRegistry.updateResources(run.runId, resources);
@@ -1842,14 +1931,15 @@ export class PiboSessionRouter {
1842
1931
  return run;
1843
1932
  },
1844
1933
  cancelRun: async (runId) => {
1845
- const cancelled = this.runRegistry.cancel(parentPiboSessionId, runId);
1934
+ const current = this.runRegistry.status(parentPiboSessionId, runId);
1846
1935
  try {
1847
- await this.invokeRunCancellationHandlers([cancelled]);
1936
+ if (!isTerminalRunStatus(current.status))
1937
+ await this.invokeRunCancellationHandlers([current]);
1938
+ return this.runRegistry.cancel(parentPiboSessionId, runId);
1848
1939
  }
1849
1940
  finally {
1850
1941
  this.refreshQueuedRunReminders(parentPiboSessionId);
1851
1942
  }
1852
- return cancelled;
1853
1943
  },
1854
1944
  ackRun: (runId) => {
1855
1945
  const run = this.runRegistry.ack(parentPiboSessionId, runId);
@@ -2067,8 +2157,8 @@ export class PiboSessionRouter {
2067
2157
  piboSessionId,
2068
2158
  text: formatRunReminderMessage(notification),
2069
2159
  source: "service",
2070
- capabilityScope: "run-reminder",
2071
2160
  id: randomUUID(),
2161
+ provenance: runReminderProvenance(notification),
2072
2162
  });
2073
2163
  }
2074
2164
  catch (error) {
@@ -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';
@@ -530,13 +531,18 @@ export class PiboLoopService {
530
531
  this.store.updateRunMessageState(eventId, 'invalidated');
531
532
  if (event.type !== 'assistant_usage')
532
533
  return;
533
- const run = this.store.getRunByMessageEventId(eventId);
534
+ const provenance = event.provenance?.kind === 'loop-run' ? event.provenance : undefined;
535
+ const run = this.store.getRunByMessageEventId(eventId) ?? (provenance ? this.store.getRun(provenance.runId) : undefined);
534
536
  if (!run || run.piboSessionId !== event.piboSessionId)
535
537
  return;
538
+ if (provenance && (run.jobId !== provenance.jobId
539
+ || (provenance.cause === 'run-reminder' && run.messageEventId !== provenance.rootEventId)))
540
+ return;
536
541
  const job = this.store.getJob(run.jobId);
537
542
  if (!job || job.mode !== 'goal')
538
543
  return;
539
- this.store.recordGoalTurnUsage(job.id, run.id, event.totalTokens);
544
+ const basis = run.accounting?.tokenAccounting?.basis ?? goalTokenAccounting(job).basis;
545
+ this.store.recordGoalTurnUsage(job.id, run.id, goalBudgetTokens(event, basis));
540
546
  }
541
547
  handleProductEvent(event) {
542
548
  if (event.type !== 'pibo.loop.fact' && event.type !== 'loop.fact' && event.type !== 'pibo.ralph.fact' && event.type !== 'ralph.fact')