@pasko70/pibo 2.4.1 → 2.4.3

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 (46) hide show
  1. package/dist/agent-runtime/context-build.js +100 -11
  2. package/dist/agent-runtime/profile-validation.js +3 -0
  3. package/dist/agent-runtime/resource-service.js +16 -0
  4. package/dist/agent-runtime/routed-session.js +180 -30
  5. package/dist/agent-runtime/testing/fake-adapter.js +7 -0
  6. package/dist/agent-runtimes/pi/adapter.js +2 -0
  7. package/dist/agent-runtimes/pi/routed-session.js +104 -43
  8. package/dist/agent-runtimes/pi/runtime.js +8 -5
  9. package/dist/apps/chat/web-app.js +21 -6
  10. package/dist/apps/chat-ui/assets/{dist-Cw9po47P.js → dist-C4JGcQjh.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-BeqHbnGN.js → dist-CF92Lv76.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-DTRjeLwO.js → dist-CJ0JS-bE.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-3YG57JXi.js → dist-DrgKyb9n.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-CrDtveZB.js → dist-VoMyV-PE.js} +1 -1
  15. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +1 -0
  16. package/dist/apps/chat-ui/assets/index-DhX1_aRM.js +228 -0
  17. package/dist/apps/chat-ui/index.html +2 -2
  18. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  19. package/dist/apps/vscode-artifacts/{pibo-vscode-ext-2.4.1.vsix → pibo-vscode-ext-2.4.3.vsix} +0 -0
  20. package/dist/cli.js +16 -6
  21. package/dist/core/context-build.js +66 -6
  22. package/dist/core/model-defaults.js +11 -3
  23. package/dist/core/session-router.js +285 -112
  24. package/dist/gateway/server.js +1 -0
  25. package/dist/loops/accounting.js +80 -0
  26. package/dist/loops/service.js +52 -13
  27. package/dist/loops/store.js +67 -19
  28. package/dist/loops/tools.js +3 -1
  29. package/dist/reliability/store.js +11 -6
  30. package/dist/runs/lifecycle.js +38 -1
  31. package/dist/runs/registry.js +46 -45
  32. package/dist/runs/tools.js +34 -24
  33. package/dist/subagents/context.js +48 -0
  34. package/dist/subagents/runtime-selection.js +28 -0
  35. package/dist/subagents/tool.js +28 -6
  36. package/dist/tools/codex-compat.js +1 -0
  37. package/dist/tools/contract.js +10 -0
  38. package/dist/tools/mcp-bridge.js +4 -2
  39. package/dist/tools/runtime/node-backend.js +8 -2
  40. package/dist/tools/runtime/python-backend.js +8 -2
  41. package/dist/tools/runtime/tool.js +1 -0
  42. package/dist/tools/session-tool-set.js +19 -12
  43. package/npm-shrinkwrap.json +2 -2
  44. package/package.json +1 -1
  45. package/dist/apps/chat-ui/assets/index-AjnP3ci-.js +0 -228
  46. package/dist/apps/chat-ui/assets/index-BJ56TREg.css +0 -1
@@ -358,6 +358,7 @@ export class PiboGatewayServer {
358
358
  findSessions: (input) => this.requireSessionStore().find(input),
359
359
  listSessions: () => this.requireSessionStore().list?.() ?? [],
360
360
  getSessionRuntimeBinding: (piboSessionId) => this.requireRouter().getSessionRuntimeBinding(piboSessionId),
361
+ getSessionRuntimeProfile: (piboSessionId) => this.requireRouter().getSessionRuntimeProfile(piboSessionId),
361
362
  inspectSessionRuntimeHistory: async (piboSessionId) => {
362
363
  const session = this.requireSessionStore().get(piboSessionId);
363
364
  if (!session)
@@ -1,4 +1,84 @@
1
1
  export const LOOP_TOKEN_ACCOUNTING_VERSION = 1;
2
+ export function emptyLoopUsageTotals() {
3
+ return {
4
+ inputTokens: 0,
5
+ outputTokens: 0,
6
+ cacheReadTokens: 0,
7
+ cacheWriteTokens: 0,
8
+ reasoningTokens: 0,
9
+ totalTokens: 0,
10
+ costUsd: 0,
11
+ costReportedTurns: 0,
12
+ assistantTurns: 0,
13
+ };
14
+ }
15
+ export function emptyLoopRecursiveUsage() {
16
+ return {
17
+ controller: emptyLoopUsageTotals(),
18
+ descendants: emptyLoopUsageTotals(),
19
+ total: emptyLoopUsageTotals(),
20
+ sessionIds: [],
21
+ };
22
+ }
23
+ export function assistantUsageTotals(usage) {
24
+ return {
25
+ inputTokens: normalizedTokenCount(usage.inputTokens),
26
+ outputTokens: normalizedTokenCount(usage.outputTokens),
27
+ cacheReadTokens: normalizedTokenCount(usage.cacheReadTokens),
28
+ cacheWriteTokens: normalizedTokenCount(usage.cacheWriteTokens),
29
+ reasoningTokens: normalizedTokenCount(usage.reasoningTokens),
30
+ totalTokens: normalizedTokenCount(usage.totalTokens),
31
+ costUsd: typeof usage.costUsd === 'number' && Number.isFinite(usage.costUsd) ? Math.max(0, usage.costUsd) : 0,
32
+ costReportedTurns: typeof usage.costUsd === 'number' && Number.isFinite(usage.costUsd) ? 1 : 0,
33
+ assistantTurns: 1,
34
+ };
35
+ }
36
+ function normalizeLoopUsageTotals(value) {
37
+ const costUsd = typeof value?.costUsd === 'number' && Number.isFinite(value.costUsd) ? Math.max(0, value.costUsd) : 0;
38
+ return {
39
+ inputTokens: normalizedTokenCount(value?.inputTokens),
40
+ outputTokens: normalizedTokenCount(value?.outputTokens),
41
+ cacheReadTokens: normalizedTokenCount(value?.cacheReadTokens),
42
+ cacheWriteTokens: normalizedTokenCount(value?.cacheWriteTokens),
43
+ reasoningTokens: normalizedTokenCount(value?.reasoningTokens),
44
+ totalTokens: normalizedTokenCount(value?.totalTokens),
45
+ costUsd,
46
+ costReportedTurns: value?.costReportedTurns === undefined && costUsd > 0 ? 1 : normalizedTokenCount(value?.costReportedTurns),
47
+ assistantTurns: normalizedTokenCount(value?.assistantTurns),
48
+ };
49
+ }
50
+ function addUsageTotals(left, right) {
51
+ const normalizedLeft = normalizeLoopUsageTotals(left);
52
+ const normalizedRight = normalizeLoopUsageTotals(right);
53
+ return {
54
+ inputTokens: normalizedLeft.inputTokens + normalizedRight.inputTokens,
55
+ outputTokens: normalizedLeft.outputTokens + normalizedRight.outputTokens,
56
+ cacheReadTokens: normalizedLeft.cacheReadTokens + normalizedRight.cacheReadTokens,
57
+ cacheWriteTokens: normalizedLeft.cacheWriteTokens + normalizedRight.cacheWriteTokens,
58
+ reasoningTokens: normalizedLeft.reasoningTokens + normalizedRight.reasoningTokens,
59
+ totalTokens: normalizedLeft.totalTokens + normalizedRight.totalTokens,
60
+ costUsd: normalizedLeft.costUsd + normalizedRight.costUsd,
61
+ costReportedTurns: normalizedLeft.costReportedTurns + normalizedRight.costReportedTurns,
62
+ assistantTurns: normalizedLeft.assistantTurns + normalizedRight.assistantTurns,
63
+ };
64
+ }
65
+ export function addLoopAssistantUsage(current, usage, input) {
66
+ const base = current
67
+ ? {
68
+ controller: normalizeLoopUsageTotals(current.controller),
69
+ descendants: normalizeLoopUsageTotals(current.descendants),
70
+ total: normalizeLoopUsageTotals(current.total),
71
+ sessionIds: Array.isArray(current.sessionIds) ? current.sessionIds.filter((id) => typeof id === 'string') : [],
72
+ }
73
+ : emptyLoopRecursiveUsage();
74
+ const increment = assistantUsageTotals(usage);
75
+ return {
76
+ controller: input.descendant ? { ...base.controller } : addUsageTotals(base.controller, increment),
77
+ descendants: input.descendant ? addUsageTotals(base.descendants, increment) : { ...base.descendants },
78
+ total: addUsageTotals(base.total, increment),
79
+ sessionIds: base.sessionIds.includes(input.piboSessionId) ? [...base.sessionIds] : [...base.sessionIds, input.piboSessionId],
80
+ };
81
+ }
2
82
  function normalizedTokenCount(value) {
3
83
  return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
4
84
  }
@@ -519,26 +519,65 @@ export class PiboLoopService {
519
519
  getStopConditionDefinitions() { return this.options.context.getLoopStopConditionDefinitions?.() ?? this.options.context.getRalphStopConditionDefinitions?.() ?? createBuiltInLoopStopConditions(); }
520
520
  handleOutputEvent(event) {
521
521
  const eventId = 'eventId' in event ? event.eventId : undefined;
522
- if (!eventId)
523
- return;
524
- if (event.type === 'message_queued')
525
- this.store.updateRunMessageState(eventId, 'queued');
526
- else if (event.type === 'message_started')
527
- this.store.updateRunMessageState(eventId, 'active');
528
- else if (event.type === 'message_finished')
529
- this.store.updateRunMessageState(eventId, 'finished');
530
- else if (event.type === 'session_error' && event.errorDetails?.code === 'loop_continuation_invalidated')
531
- this.store.updateRunMessageState(eventId, 'invalidated');
522
+ if (eventId) {
523
+ if (event.type === 'message_queued')
524
+ this.store.updateRunMessageState(eventId, 'queued');
525
+ else if (event.type === 'message_started')
526
+ this.store.updateRunMessageState(eventId, 'active');
527
+ else if (event.type === 'message_finished')
528
+ this.store.updateRunMessageState(eventId, 'finished');
529
+ else if (event.type === 'session_error' && event.errorDetails?.code === 'loop_continuation_invalidated')
530
+ this.store.updateRunMessageState(eventId, 'invalidated');
531
+ }
532
532
  if (event.type !== 'assistant_usage')
533
533
  return;
534
- const run = this.store.getRunByMessageEventId(eventId);
535
- if (!run || run.piboSessionId !== event.piboSessionId)
534
+ const provenance = event.provenance;
535
+ const provenanceRunId = provenance?.kind === 'loop-run'
536
+ ? provenance.runId
537
+ : provenance?.kind === 'subagent-request'
538
+ ? provenance.loopRunId
539
+ : undefined;
540
+ const provenanceJobId = provenance?.kind === 'loop-run'
541
+ ? provenance.jobId
542
+ : provenance?.kind === 'subagent-request'
543
+ ? provenance.loopJobId
544
+ : undefined;
545
+ const run = provenanceRunId
546
+ ? this.store.getRun(provenanceRunId)
547
+ : eventId
548
+ ? this.store.getRunByMessageEventId(eventId)
549
+ : undefined;
550
+ if (!run || (provenanceJobId && run.jobId !== provenanceJobId) || !this.isRunSessionOrDescendant(run, event.piboSessionId))
551
+ return;
552
+ if (provenance?.kind === 'loop-run'
553
+ && provenance.cause === 'run-reminder'
554
+ && run.messageEventId !== provenance.rootEventId)
536
555
  return;
537
556
  const job = this.store.getJob(run.jobId);
538
557
  if (!job || job.mode !== 'goal')
539
558
  return;
540
559
  const basis = run.accounting?.tokenAccounting?.basis ?? goalTokenAccounting(job).basis;
541
- this.store.recordGoalTurnUsage(job.id, run.id, goalBudgetTokens(event, basis));
560
+ this.store.recordGoalAssistantUsage(job.id, run.id, {
561
+ usage: event,
562
+ budgetTokens: goalBudgetTokens(event, basis),
563
+ piboSessionId: event.piboSessionId,
564
+ descendant: event.piboSessionId !== run.piboSessionId,
565
+ });
566
+ }
567
+ isRunSessionOrDescendant(run, piboSessionId) {
568
+ if (!run.piboSessionId)
569
+ return false;
570
+ if (run.piboSessionId === piboSessionId)
571
+ return true;
572
+ let current = this.options.context.getSession(piboSessionId);
573
+ const seen = new Set();
574
+ while (current?.parentId && !seen.has(current.parentId)) {
575
+ if (current.parentId === run.piboSessionId)
576
+ return true;
577
+ seen.add(current.parentId);
578
+ current = this.options.context.getSession(current.parentId);
579
+ }
580
+ return false;
542
581
  }
543
582
  handleProductEvent(event) {
544
583
  if (event.type !== 'pibo.loop.fact' && event.type !== 'loop.fact' && event.type !== 'pibo.ralph.fact' && event.type !== 'ralph.fact')
@@ -4,7 +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
+ import { addLoopAssistantUsage, newGoalTokenAccounting, normalizeLoopTokenAccounting } from './accounting.js';
8
8
  function nowIso(now = new Date()) { return now.toISOString(); }
9
9
  function parseJson(json) { return JSON.parse(json); }
10
10
  function defaultName(prompt) { const normalized = prompt.replace(/\s+/g, ' ').trim(); return normalized ? normalized.slice(0, 80) : 'Loop job'; }
@@ -418,29 +418,65 @@ export class PiboLoopStore {
418
418
  this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(budgetLimited ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, id);
419
419
  return this.getJob(id);
420
420
  }
421
- recordGoalTurnUsage(id, runId, tokens, now = new Date()) {
421
+ recordGoalAssistantUsage(id, runId, input, now = new Date()) {
422
422
  this.db.exec('BEGIN IMMEDIATE');
423
423
  try {
424
- const job = this.recordGoalProgress(id, { tokens }, now);
425
- if (job?.mode === 'goal') {
426
- const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
427
- if (row) {
428
- const accounting = parseRunAccounting(row.accounting_json) ?? { tokenAccounting: normalizeLoopTokenAccounting(job.state.tokenAccounting) };
429
- const turnTokens = (accounting.tokensUsed ?? 0) + Math.max(0, Math.floor(tokens));
430
- const budget = accounting.tokenBudget;
431
- const before = accounting.tokensUsedBefore ?? 0;
432
- const nextAccounting = { ...accounting, tokensUsed: turnTokens, ...(budget !== undefined ? { overshootTokens: Math.max(0, before + turnTokens - budget) } : {}) };
433
- this.db.prepare('UPDATE pibo_ralph_runs SET accounting_json = ?, updated_at = ? WHERE id = ?').run(runAccountingJson(nextAccounting), nowIso(now), runId);
434
- }
424
+ const job = this.getJob(id);
425
+ if (!job || job.mode !== 'goal') {
426
+ this.db.exec('COMMIT');
427
+ return job;
428
+ }
429
+ const tokens = Math.max(0, Math.floor(input.budgetTokens));
430
+ const nextTokens = (job.state.tokensUsed ?? 0) + tokens;
431
+ const currentStatus = goalStatus(job) ?? 'active';
432
+ const budgetLimited = currentStatus === 'active' && job.tokenBudget !== undefined && nextTokens >= job.tokenBudget;
433
+ const timestamp = nowIso(now);
434
+ const state = {
435
+ ...job.state,
436
+ tokensUsed: nextTokens,
437
+ usage: addLoopAssistantUsage(job.state.usage, input.usage, {
438
+ piboSessionId: input.piboSessionId,
439
+ descendant: input.descendant,
440
+ }),
441
+ goalStatus: budgetLimited ? 'budget_limited' : currentStatus,
442
+ };
443
+ if (budgetLimited)
444
+ state.goalEndedAt = job.state.goalEndedAt ?? timestamp;
445
+ this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(budgetLimited ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, id);
446
+ const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
447
+ if (row) {
448
+ const accounting = parseRunAccounting(row.accounting_json) ?? { tokenAccounting: normalizeLoopTokenAccounting(state.tokenAccounting) };
449
+ const turnTokens = (accounting.tokensUsed ?? 0) + tokens;
450
+ const budget = accounting.tokenBudget;
451
+ const before = accounting.tokensUsedBefore ?? 0;
452
+ const nextAccounting = {
453
+ ...accounting,
454
+ tokensUsed: turnTokens,
455
+ usage: addLoopAssistantUsage(accounting.usage, input.usage, {
456
+ piboSessionId: input.piboSessionId,
457
+ descendant: input.descendant,
458
+ }),
459
+ ...(budget !== undefined ? { overshootTokens: Math.max(0, before + turnTokens - budget) } : {}),
460
+ };
461
+ this.db.prepare('UPDATE pibo_ralph_runs SET accounting_json = ?, updated_at = ? WHERE id = ?').run(runAccountingJson(nextAccounting), timestamp, runId);
435
462
  }
436
463
  this.db.exec('COMMIT');
437
- return job;
464
+ return this.getJob(id);
438
465
  }
439
466
  catch (error) {
440
467
  this.db.exec('ROLLBACK');
441
468
  throw error;
442
469
  }
443
470
  }
471
+ recordGoalTurnUsage(id, runId, tokens, now = new Date()) {
472
+ const run = this.getRun(runId);
473
+ return this.recordGoalAssistantUsage(id, runId, {
474
+ usage: { type: 'assistant_usage', piboSessionId: run?.piboSessionId ?? 'unknown', totalTokens: Math.max(0, Math.floor(tokens)) },
475
+ budgetTokens: tokens,
476
+ piboSessionId: run?.piboSessionId ?? 'unknown',
477
+ descendant: false,
478
+ }, now);
479
+ }
444
480
  recordGoalRunTime(id, runId, activeTimeSeconds, now = new Date()) {
445
481
  const seconds = Math.max(0, Math.floor(activeTimeSeconds));
446
482
  this.db.exec('BEGIN IMMEDIATE');
@@ -636,7 +672,11 @@ export class PiboLoopStore {
636
672
  const completedIterations = (job.state.completedIterations ?? 0) + 1;
637
673
  const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
638
674
  const currentGoalStatus = goalStatus(job);
639
- const nextGoalStatus = job.mode === 'goal' ? isTerminalGoalStatus(currentGoalStatus) ? currentGoalStatus : input.goalStatus ?? currentGoalStatus : undefined;
675
+ const nextGoalStatus = job.mode === 'goal'
676
+ ? isTerminalGoalStatus(currentGoalStatus) || currentGoalStatus === 'paused'
677
+ ? currentGoalStatus
678
+ : input.goalStatus ?? currentGoalStatus
679
+ : undefined;
640
680
  const terminalGoalStatus = job.mode === 'goal' && isTerminalGoalStatus(nextGoalStatus);
641
681
  const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
642
682
  const state = {
@@ -833,15 +873,23 @@ export function createLoopMessagePreflight(options = {}) {
833
873
  const job = store.getJob(jobId);
834
874
  const run = store.getRun(runId);
835
875
  const status = job?.mode === 'goal' ? goalStatus(job) ?? (job.enabled ? 'active' : 'paused') : undefined;
876
+ const causalReminder = event.provenance.cause === 'run-reminder';
877
+ const validMessageBinding = causalReminder
878
+ ? event.source === 'service'
879
+ && event.text.startsWith('<pibo_run_notification>')
880
+ && typeof event.provenance.rootEventId === 'string'
881
+ && run?.messageEventId === event.provenance.rootEventId
882
+ : run?.messageEventId === event.id;
883
+ const validRunState = causalReminder
884
+ ? true
885
+ : run?.status === 'running' && Boolean(job?.state.runningAt) && job?.state.lastRunId === runId;
836
886
  const allowed = Boolean(job
837
887
  && run
838
888
  && run.jobId === jobId
839
- && run.status === 'running'
840
- && run.messageEventId === event.id
889
+ && validMessageBinding
890
+ && validRunState
841
891
  && (!run.piboSessionId || run.piboSessionId === event.piboSessionId)
842
892
  && job.enabled
843
- && job.state.runningAt
844
- && job.state.lastRunId === runId
845
893
  && (job.mode !== 'goal' || status === 'active'));
846
894
  if (allowed)
847
895
  return { allowed: true };
@@ -34,7 +34,9 @@ function resolveGoalForTurn(store, context, piboSessionId) {
34
34
  if (provenance?.kind !== 'loop-run')
35
35
  return store.getSessionGoalOwner(piboSessionId) ?? store.getLatestGoalForSession(piboSessionId);
36
36
  const run = store.getRun(provenance.runId);
37
- if (!run || run.jobId !== provenance.jobId || run.piboSessionId !== piboSessionId || run.messageEventId !== activeMessage?.id) {
37
+ const expectedEventId = provenance.cause === 'run-reminder' ? provenance.rootEventId : activeMessage?.id;
38
+ const validReminder = provenance.cause !== 'run-reminder' || (activeMessage?.source === 'service' && typeof provenance.rootEventId === 'string');
39
+ if (!run || !validReminder || run.jobId !== provenance.jobId || run.piboSessionId !== piboSessionId || run.messageEventId !== expectedEventId) {
38
40
  throw new Error('cannot resolve goal because this turn has stale or invalid Loop provenance');
39
41
  }
40
42
  const job = store.getJob(provenance.jobId);
@@ -149,7 +149,8 @@ export class PiboReliabilityStore {
149
149
  timeout_at TEXT,
150
150
  timeout_phase TEXT,
151
151
  service_warning TEXT,
152
- resource_json TEXT
152
+ resource_json TEXT,
153
+ origin_json TEXT
153
154
  );
154
155
  CREATE INDEX IF NOT EXISTS idx_pibo_runs_controller_updated
155
156
  ON pibo_runs(controller_pibo_session_id, updated_at);
@@ -161,6 +162,7 @@ export class PiboReliabilityStore {
161
162
  ensurePiboRunColumn(this.db, "timeout_phase", "TEXT");
162
163
  ensurePiboRunColumn(this.db, "service_warning", "TEXT");
163
164
  ensurePiboRunColumn(this.db, "resource_json", "TEXT");
165
+ ensurePiboRunColumn(this.db, "origin_json", "TEXT");
164
166
  this.appendEventStatement = this.db.prepare(`
165
167
  INSERT INTO pibo_event_stream (topic, key, event_id, idempotency_key, created_at, retention_class, payload_json)
166
168
  VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -502,10 +504,10 @@ export class PiboReliabilityStore {
502
504
  INSERT INTO pibo_runs (
503
505
  run_id, kind, controller_pibo_session_id, status, completion_policy, consumed, tool_name,
504
506
  summary, result_json, error, notified_status, acknowledged_status, created_at, updated_at,
505
- completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json
506
- ) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?)
507
+ completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json, origin_json
508
+ ) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
507
509
  `)
508
- .run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null);
510
+ .run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null, input.origin ? JSON.stringify(input.origin) : null);
509
511
  this.claimJob(job.jobId, input.workerId ?? `run-registry:${process.pid}`, 24 * 60 * 60 * 1000);
510
512
  return this.requireRun(runId);
511
513
  }
@@ -534,10 +536,11 @@ export class PiboReliabilityStore {
534
536
  timeout_at = ?,
535
537
  timeout_phase = ?,
536
538
  service_warning = ?,
537
- resource_json = ?
539
+ resource_json = ?,
540
+ origin_json = ?
538
541
  WHERE run_id = ?
539
542
  `)
540
- .run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, next.timeoutMs ?? null, next.timeoutAt ?? null, next.timeoutPhase ?? null, next.serviceWarning ?? null, next.resources ? JSON.stringify(next.resources) : null, runId);
543
+ .run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, next.timeoutMs ?? null, next.timeoutAt ?? null, next.timeoutPhase ?? null, next.serviceWarning ?? null, next.resources ? JSON.stringify(next.resources) : null, next.origin ? JSON.stringify(next.origin) : null, runId);
541
544
  return this.requireRun(runId);
542
545
  }
543
546
  getRun(runId) {
@@ -810,6 +813,8 @@ function runFromRow(row) {
810
813
  output.serviceWarning = row.service_warning;
811
814
  if (row.resource_json)
812
815
  output.resources = JSON.parse(row.resource_json);
816
+ if (row.origin_json)
817
+ output.origin = JSON.parse(row.origin_json);
813
818
  return output;
814
819
  }
815
820
  function retryDelayMs(attempts, input) {
@@ -1,3 +1,4 @@
1
+ export const PIBO_RUN_CANCELLATION_SETTLEMENT_TIMEOUT_MS = 15_000;
1
2
  export class PiboRunExecutionTimeoutError extends Error {
2
3
  timeoutPhase;
3
4
  constructor(message, timeoutPhase) {
@@ -6,6 +7,34 @@ export class PiboRunExecutionTimeoutError extends Error {
6
7
  this.name = "PiboRunExecutionTimeoutError";
7
8
  }
8
9
  }
10
+ export class PiboRunCancellationError extends Error {
11
+ constructor(message, options) {
12
+ super(message, options);
13
+ this.name = "PiboRunCancellationError";
14
+ }
15
+ }
16
+ export class PiboRunCancelledError extends Error {
17
+ constructor(message = "Yielded run was cancelled.", options) {
18
+ super(message, options);
19
+ this.name = "PiboRunCancelledError";
20
+ }
21
+ }
22
+ export async function waitForRunCancellationSettlement(settled, timeoutMs = PIBO_RUN_CANCELLATION_SETTLEMENT_TIMEOUT_MS) {
23
+ let timer;
24
+ try {
25
+ await Promise.race([
26
+ settled,
27
+ new Promise((_resolve, reject) => {
28
+ timer = setTimeout(() => reject(new Error(`Yielded run did not settle within ${timeoutMs}ms after cancellation.`)), timeoutMs);
29
+ timer.unref?.();
30
+ }),
31
+ ]);
32
+ }
33
+ finally {
34
+ if (timer)
35
+ clearTimeout(timer);
36
+ }
37
+ }
9
38
  export function resolveRunTimeoutMs(toolName, params) {
10
39
  if (!params || typeof params !== "object" || Array.isArray(params))
11
40
  return undefined;
@@ -26,7 +55,15 @@ export function foregroundServiceWarning(toolName, params, timeoutMs) {
26
55
  }
27
56
  export function isConfiguredTimeoutError(error) {
28
57
  const message = error instanceof Error ? error.message : String(error);
29
- return /(?:timed?\s*out|timeout)/i.test(message);
58
+ const terminalLines = message.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-5);
59
+ return terminalLines.some((line) => {
60
+ const normalized = line.replace(/^error:\s*/i, "");
61
+ return /^(?:command|process|tool execution|yielded run)\s+timed?\s*out\b.*$/i.test(normalized)
62
+ || /^timed?\s*out(?:\s+after\s+.+)?[.!]?$/i.test(normalized)
63
+ || /^timeout(?:\s+error)?[.!]?$/i.test(normalized)
64
+ || /^timeout(?::|\s+)(?:occurred|expired|exceeded|elapsed|reached)\b.*$/i.test(normalized)
65
+ || /^timeout(?::|\s+)(?:after\s+)?\d+(?:\.\d+)?\s*(?:ms|milliseconds?|s|secs?|seconds?|m|mins?|minutes?|h|hours?)\b.*$/i.test(normalized);
66
+ });
30
67
  }
31
68
  export function hasMeaningfulTimeoutOutput(value) {
32
69
  const text = extractText(value);
@@ -7,6 +7,25 @@ function now() {
7
7
  function runTimeoutAt(createdAt, timeoutMs) {
8
8
  return timeoutMs === undefined ? undefined : new Date(Date.parse(createdAt) + timeoutMs).toISOString();
9
9
  }
10
+ function sameOrigin(left, right) {
11
+ if (!left || !right)
12
+ return left === right;
13
+ if (left.eventId !== right.eventId || left.provenance.kind !== right.provenance.kind)
14
+ return false;
15
+ if (left.provenance.kind === "loop-run" && right.provenance.kind === "loop-run") {
16
+ return left.provenance.jobId === right.provenance.jobId
17
+ && left.provenance.runId === right.provenance.runId
18
+ && left.provenance.cause === right.provenance.cause
19
+ && left.provenance.rootEventId === right.provenance.rootEventId;
20
+ }
21
+ if (left.provenance.kind === "subagent-request" && right.provenance.kind === "subagent-request") {
22
+ return left.provenance.requestId === right.provenance.requestId
23
+ && left.provenance.controllerPiboSessionId === right.provenance.controllerPiboSessionId
24
+ && left.provenance.loopJobId === right.provenance.loopJobId
25
+ && left.provenance.loopRunId === right.provenance.loopRunId;
26
+ }
27
+ return false;
28
+ }
10
29
  function formatTimeout(timeoutMs) {
11
30
  if (timeoutMs === undefined)
12
31
  return "its configured timeout";
@@ -83,6 +102,7 @@ export class PiboRunRegistry {
83
102
  serviceWarning: input.serviceWarning,
84
103
  resources: input.resources,
85
104
  workerId: this.workerId,
105
+ origin: input.origin,
86
106
  });
87
107
  const record = recordFromStored(stored);
88
108
  this.runs.set(record.runId, record);
@@ -108,6 +128,7 @@ export class PiboRunRegistry {
108
128
  ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs, timeoutAt: runTimeoutAt(timestamp, input.timeoutMs) } : {}),
109
129
  ...(input.serviceWarning ? { serviceWarning: input.serviceWarning } : {}),
110
130
  ...(input.resources ? { resources: structuredClone(input.resources) } : {}),
131
+ ...(input.origin ? { origin: structuredClone(input.origin) } : {}),
111
132
  };
112
133
  this.runs.set(runId, record);
113
134
  const output = snapshot(record);
@@ -206,6 +227,16 @@ export class PiboRunRegistry {
206
227
  .filter((record) => options.includeDetached || record.completionPolicy !== "detached")
207
228
  .map(snapshot);
208
229
  }
230
+ listActiveControllerRuns(controllerPiboSessionId) {
231
+ return [...this.runs.values()]
232
+ .filter((record) => record.controllerPiboSessionId === controllerPiboSessionId && !terminal(record.status))
233
+ .map(snapshot);
234
+ }
235
+ listActiveRuns() {
236
+ return [...this.runs.values()]
237
+ .filter((record) => !terminal(record.status))
238
+ .map(snapshot);
239
+ }
209
240
  status(controllerPiboSessionId, runId) {
210
241
  return snapshot(this.requireRunForController(controllerPiboSessionId, runId));
211
242
  }
@@ -245,7 +276,7 @@ export class PiboRunRegistry {
245
276
  }
246
277
  read(controllerPiboSessionId, runId) {
247
278
  const record = this.requireRunForController(controllerPiboSessionId, runId);
248
- if (terminal(record.status)) {
279
+ if (terminal(record.status) && !record.consumed) {
249
280
  record.consumed = true;
250
281
  record.updatedAt = now();
251
282
  this.options.store?.updateRun(runId, record);
@@ -258,25 +289,29 @@ export class PiboRunRegistry {
258
289
  output.error = record.error;
259
290
  return output;
260
291
  }
261
- cancel(controllerPiboSessionId, runId) {
292
+ cancel(controllerPiboSessionId, runId, reason = "Run was cancelled.") {
262
293
  const record = this.requireRunForController(controllerPiboSessionId, runId);
263
294
  const previousStatus = record.status;
264
295
  if (!terminal(record.status)) {
265
296
  record.status = "cancelled";
297
+ record.error = reason;
266
298
  record.summary = `${record.toolName} run cancelled.`;
267
299
  this.finish(record);
268
300
  if (record.jobId)
269
- this.options.store?.fail(record.jobId, this.workerId, "Run was cancelled.");
301
+ this.options.store?.fail(record.jobId, this.workerId, reason);
270
302
  }
271
303
  record.consumed = true;
272
304
  record.updatedAt = now();
273
305
  this.options.store?.updateRun(runId, record);
274
306
  const output = snapshot(record);
275
- this.notify({ type: "run_changed", run: output, previousStatus, reason: "Run was cancelled." });
307
+ this.notify({ type: "run_changed", run: output, previousStatus, reason });
276
308
  return output;
277
309
  }
278
310
  ack(controllerPiboSessionId, runId) {
279
311
  const record = this.requireRunForController(controllerPiboSessionId, runId);
312
+ const consumesTerminalRun = terminal(record.status) && !record.consumed;
313
+ if (record.acknowledgedStatus === record.status && !consumesTerminalRun)
314
+ return { ...snapshot(record), changed: false };
280
315
  record.acknowledgedStatus = record.status;
281
316
  if (terminal(record.status))
282
317
  record.consumed = true;
@@ -284,7 +319,7 @@ export class PiboRunRegistry {
284
319
  this.options.store?.updateRun(runId, record);
285
320
  const output = snapshot(record);
286
321
  this.notify({ type: "run_acknowledged", run: output });
287
- return output;
322
+ return { ...output, changed: true };
288
323
  }
289
324
  suppressNotification(controllerPiboSessionId, runId) {
290
325
  const record = this.requireRunForController(controllerPiboSessionId, runId);
@@ -306,14 +341,17 @@ export class PiboRunRegistry {
306
341
  return suppressed;
307
342
  }
308
343
  createNotification(controllerPiboSessionId, options = {}) {
309
- const records = [...this.runs.values()].filter((record) => this.needsNotification(record, controllerPiboSessionId, options));
310
- if (records.length === 0)
344
+ const pendingRecords = [...this.runs.values()].filter((record) => this.needsNotification(record, controllerPiboSessionId, options));
345
+ if (pendingRecords.length === 0)
311
346
  return undefined;
347
+ const origin = pendingRecords[0].origin;
348
+ const records = pendingRecords.filter((record) => sameOrigin(record.origin, origin));
312
349
  for (const record of records) {
313
350
  record.notifiedStatus = record.status;
314
351
  this.options.store?.updateRun(record.runId, record);
315
352
  }
316
353
  const notification = {
354
+ ...(origin ? { origin: structuredClone(origin) } : {}),
317
355
  completed: [],
318
356
  failed: [],
319
357
  timedOut: [],
@@ -338,44 +376,6 @@ export class PiboRunRegistry {
338
376
  hasPendingNotification(controllerPiboSessionId, options = {}) {
339
377
  return [...this.runs.values()].some((record) => this.needsNotification(record, controllerPiboSessionId, options));
340
378
  }
341
- cancelControllerRuns(controllerPiboSessionId, reason = "Controller Pibo session was disposed.") {
342
- const cancelled = [];
343
- for (const record of this.runs.values()) {
344
- if (record.controllerPiboSessionId !== controllerPiboSessionId || terminal(record.status))
345
- continue;
346
- record.status = "cancelled";
347
- record.error = reason;
348
- record.consumed = true;
349
- record.summary = `${record.toolName} run cancelled.`;
350
- this.finish(record);
351
- this.options.store?.updateRun(record.runId, record);
352
- if (record.jobId)
353
- this.options.store?.fail(record.jobId, this.workerId, reason);
354
- const output = snapshot(record);
355
- this.notify({ type: "run_changed", run: output, previousStatus: "running", reason });
356
- cancelled.push(output);
357
- }
358
- return cancelled;
359
- }
360
- cancelAll(reason = "Run registry was disposed.") {
361
- const cancelled = [];
362
- for (const record of this.runs.values()) {
363
- if (terminal(record.status))
364
- continue;
365
- record.status = "cancelled";
366
- record.error = reason;
367
- record.consumed = true;
368
- record.summary = `${record.toolName} run cancelled.`;
369
- this.finish(record);
370
- this.options.store?.updateRun(record.runId, record);
371
- if (record.jobId)
372
- this.options.store?.fail(record.jobId, this.workerId, reason);
373
- const output = snapshot(record);
374
- this.notify({ type: "run_changed", run: output, previousStatus: "running", reason });
375
- cancelled.push(output);
376
- }
377
- return cancelled;
378
- }
379
379
  prune(options = {}) {
380
380
  const nowMs = options.nowMs ?? Date.now();
381
381
  const consumedTerminalTtlMs = options.consumedTerminalTtlMs ??
@@ -461,5 +461,6 @@ function recordFromStored(record) {
461
461
  timeoutPhase: record.timeoutPhase,
462
462
  serviceWarning: record.serviceWarning,
463
463
  resources: record.resources,
464
+ origin: record.origin,
464
465
  };
465
466
  }