@pasko70/pibo 3.0.1 → 3.1.0

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 (122) hide show
  1. package/README.md +20 -6
  2. package/compute-image/Dockerfile +47 -0
  3. package/compute-image/Dockerfile.dockerignore +5 -0
  4. package/dist/agent-runtime/context-build.js +26 -2
  5. package/dist/agent-runtime/contract.js +78 -28
  6. package/dist/agent-runtime/portable-history.js +19 -9
  7. package/dist/agent-runtime/registry.js +8 -2
  8. package/dist/agent-runtime/routed-session.js +120 -15
  9. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  10. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  11. package/dist/agent-runtimes/omp/adapter.js +3 -7
  12. package/dist/agent-runtimes/pi/adapter.js +5 -3
  13. package/dist/agent-runtimes/pi/routed-session.js +59 -12
  14. package/dist/agent-runtimes/pi/runtime.js +6 -0
  15. package/dist/apps/chat/agent-profiles.js +31 -2
  16. package/dist/apps/chat/agent-store.js +98 -6
  17. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  18. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  19. package/dist/apps/chat/data/project-service.js +119 -15
  20. package/dist/apps/chat/data/room-service.js +21 -0
  21. package/dist/apps/chat/data/session-query-service.js +33 -10
  22. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  23. package/dist/apps/chat/loop-api.js +10 -2
  24. package/dist/apps/chat/output-compactor.js +144 -21
  25. package/dist/apps/chat/output-event-policy.js +179 -2
  26. package/dist/apps/chat/stream.js +16 -7
  27. package/dist/apps/chat/trace-v2.js +1 -0
  28. package/dist/apps/chat/web-app.js +317 -86
  29. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  38. package/dist/apps/chat-vscode-web/index.html +1 -1
  39. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  40. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  41. package/dist/cli-session/localSessionSource.js +153 -161
  42. package/dist/cli.js +7 -11
  43. package/dist/compute/cli.js +35 -16
  44. package/dist/compute/docker.js +341 -94
  45. package/dist/config/config.js +3 -0
  46. package/dist/core/context-build.js +7 -16
  47. package/dist/core/output-persistence-retry.js +484 -0
  48. package/dist/core/output-render-sequence.js +331 -0
  49. package/dist/core/profiles.js +24 -0
  50. package/dist/core/provider-recovery.js +7 -1
  51. package/dist/core/session-model.js +37 -1
  52. package/dist/core/session-router.js +78 -110
  53. package/dist/data/ingest-service.js +91 -9
  54. package/dist/data/navigation-store.js +5 -5
  55. package/dist/data/pibo-store.js +8 -1
  56. package/dist/data/schema.js +243 -3
  57. package/dist/data/session-store.js +3 -3
  58. package/dist/debug/agents.js +91 -89
  59. package/dist/debug/index.js +46 -2
  60. package/dist/debug/pty.js +127 -48
  61. package/dist/gateway/cli.js +4 -0
  62. package/dist/gateway/web.js +8 -8
  63. package/dist/local/client.js +3 -2
  64. package/dist/loops/channel.js +3 -1
  65. package/dist/loops/cli.js +2 -1
  66. package/dist/loops/service.js +90 -11
  67. package/dist/loops/store.js +100 -74
  68. package/dist/mcp/agent-context.js +13 -26
  69. package/dist/mcp/commands/info.js +3 -1
  70. package/dist/mcp/config-command.js +46 -2
  71. package/dist/mcp/config.js +18 -4
  72. package/dist/plugins/context-files-store.js +36 -6
  73. package/dist/plugins/context-files.js +52 -1
  74. package/dist/plugins/registry.js +2 -0
  75. package/dist/plugins/user-profile-resources.js +22 -0
  76. package/dist/previews/base-url.js +34 -0
  77. package/dist/previews/config.js +2 -32
  78. package/dist/ralph/store.js +5 -0
  79. package/dist/reliability/store.js +378 -106
  80. package/dist/session-ui/terminalRows.js +8 -4
  81. package/dist/sessions/pibo-data-store.js +145 -0
  82. package/dist/sessions/store.js +42 -0
  83. package/dist/setup/cli.js +422 -6
  84. package/dist/setup/installation-profiles.js +464 -0
  85. package/dist/shared/deterministic-digest.js +94 -0
  86. package/dist/shared/trace-engine.js +54 -0
  87. package/dist/shared/trace-event-projection.js +124 -67
  88. package/dist/shared/trace-history.js +47 -12
  89. package/dist/shared/trace-live-reducer.js +23 -4
  90. package/dist/shared/trace-nodes.js +17 -28
  91. package/dist/shared/trace-order.js +41 -5
  92. package/dist/shared/trace-page-merge.js +44 -14
  93. package/dist/shared/trace-patch-nodes.js +2 -0
  94. package/dist/shared/trace-subagent-links.js +4 -1
  95. package/dist/shared/trace-tool-identity.js +15 -8
  96. package/dist/signals/projector.js +28 -6
  97. package/dist/subagents/observation-query.js +121 -0
  98. package/dist/subagents/tool.js +7 -3
  99. package/dist/tools/agent-browser-leases.js +58 -23
  100. package/dist/tools/agent-browser-wrapper.js +1 -1
  101. package/dist/tools/browser-use-leases.js +129 -16
  102. package/dist/tools/browser-use-wrapper.js +1 -1
  103. package/dist/tools/index.js +27 -11
  104. package/dist/tools/python-runtime.js +10 -3
  105. package/dist/tools/registry.js +1 -1
  106. package/dist/tools/runtime/node-backend.js +16 -5
  107. package/dist/tools/runtime/node-worker-source.js +112 -33
  108. package/dist/tools/runtime/python-backend.js +16 -5
  109. package/dist/tools/runtime/python-worker-source.js +167 -16
  110. package/dist/tools/runtime/registry.js +23 -10
  111. package/dist/user-skills/store.js +0 -1
  112. package/docs/ops/vscode-extension-release.md +9 -1
  113. package/npm-shrinkwrap.json +9 -2
  114. package/package.json +9 -2
  115. package/scripts/docker-entrypoint.sh +46 -0
  116. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  117. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  118. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  119. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  120. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
  121. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  122. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
@@ -5,6 +5,12 @@ import { DatabaseSync } from 'node:sqlite';
5
5
  import { piboHomePath } from '../core/pibo-home.js';
6
6
  import { isPiboThinkingLevel } from '../core/thinking.js';
7
7
  import { addLoopAssistantUsage, newGoalTokenAccounting, normalizeLoopTokenAccounting } from './accounting.js';
8
+ export class PiboLoopActiveRunModeChangeError extends Error {
9
+ constructor() {
10
+ super('Loop mode cannot be changed while a Loop run is active');
11
+ this.name = 'PiboLoopActiveRunModeChangeError';
12
+ }
13
+ }
8
14
  function nowIso(now = new Date()) { return now.toISOString(); }
9
15
  function parseJson(json) { return JSON.parse(json); }
10
16
  function defaultName(prompt) { const normalized = prompt.replace(/\s+/g, ' ').trim(); return normalized ? normalized.slice(0, 80) : 'Loop job'; }
@@ -507,48 +513,56 @@ export class PiboLoopStore {
507
513
  return this.db.prepare(`SELECT * FROM pibo_ralph_jobs ${where} ORDER BY updated_at DESC, id ASC`).all(...values).map(jobFromRow);
508
514
  }
509
515
  updateJob(id, patch, now = new Date()) {
510
- const existing = this.getJob(id);
511
- if (!existing)
512
- return undefined;
513
- const runtimeOptions = normalizeRuntimeOptions({
514
- modelOverride: hasOwn(patch, 'modelOverride') ? patch.modelOverride : existing.modelOverride,
515
- thinkingLevel: hasOwn(patch, 'thinkingLevel') ? patch.thinkingLevel : existing.thinkingLevel,
516
- fastMode: hasOwn(patch, 'fastMode') ? patch.fastMode : existing.fastMode,
517
- });
518
- const stopPolicy = hasOwn(patch, 'stopPolicy') ? normalizeLoopStopPolicy(patch.stopPolicy ?? undefined) : existing.stopPolicy;
519
- const target = patch.target ? normalizeLoopTarget(patch.target) : existing.target;
520
- const mode = patch.mode !== undefined ? normalizeLoopMode(patch.mode) : existing.mode;
521
- const enabled = patch.enabled ?? existing.enabled;
522
- let state = mode === existing.mode
523
- ? { ...existing.state }
524
- : { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokenAccounting: newGoalTokenAccounting(), tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: nowIso(now) } : {}) } : {}) };
525
- if (mode === 'goal' && patch.enabled !== undefined) {
526
- const currentGoalStatus = goalStatus({ mode, enabled: existing.enabled, state: existing.state }) ?? 'paused';
527
- if (patch.enabled) {
528
- if (currentGoalStatus === 'complete')
529
- throw new Error('Completed goals cannot be restarted; create a new goal');
530
- const nextBudget = hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget;
531
- const nextReserve = hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve;
532
- if (currentGoalStatus === 'budget_limited' && nextBudget !== undefined && (existing.state.tokensUsed ?? 0) + (nextReserve ?? 0) >= nextBudget)
533
- throw new Error('Increase or clear the token budget, or lower the token reserve, before resuming this goal');
534
- state.goalStatus = 'active';
535
- state.goalStartedAt ??= nowIso(now);
536
- delete state.goalEndedAt;
537
- state.stopRequestedAt = undefined;
538
- state.cancelRequestedAt = undefined;
539
- state.lastFailure = undefined;
540
- state.nextAttemptAt = undefined;
541
- state.retryBackoffMs = undefined;
542
- state.consecutiveErrors = 0;
516
+ this.db.exec('BEGIN IMMEDIATE');
517
+ try {
518
+ const existing = this.getJob(id);
519
+ if (!existing) {
520
+ this.db.exec('COMMIT');
521
+ return undefined;
543
522
  }
544
- else {
545
- state.goalStatus = currentGoalStatus === 'active' ? 'paused' : currentGoalStatus;
523
+ const runtimeOptions = normalizeRuntimeOptions({
524
+ modelOverride: hasOwn(patch, 'modelOverride') ? patch.modelOverride : existing.modelOverride,
525
+ thinkingLevel: hasOwn(patch, 'thinkingLevel') ? patch.thinkingLevel : existing.thinkingLevel,
526
+ fastMode: hasOwn(patch, 'fastMode') ? patch.fastMode : existing.fastMode,
527
+ });
528
+ const stopPolicy = hasOwn(patch, 'stopPolicy') ? normalizeLoopStopPolicy(patch.stopPolicy ?? undefined) : existing.stopPolicy;
529
+ const target = patch.target ? normalizeLoopTarget(patch.target) : existing.target;
530
+ const mode = patch.mode !== undefined ? normalizeLoopMode(patch.mode) : existing.mode;
531
+ if (mode !== existing.mode && this.db.prepare("SELECT 1 FROM pibo_ralph_runs WHERE job_id = ? AND status = 'running' LIMIT 1").get(id))
532
+ throw new PiboLoopActiveRunModeChangeError();
533
+ const enabled = patch.enabled ?? existing.enabled;
534
+ let state = mode === existing.mode
535
+ ? { ...existing.state }
536
+ : { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokenAccounting: newGoalTokenAccounting(), tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: nowIso(now) } : {}) } : {}) };
537
+ if (mode === 'goal' && patch.enabled !== undefined) {
538
+ const currentGoalStatus = goalStatus({ mode, enabled: existing.enabled, state: existing.state }) ?? 'paused';
539
+ if (patch.enabled) {
540
+ if (isTerminalGoalStatus(currentGoalStatus))
541
+ throw new Error('Terminal Goals cannot be restarted; use the confirmed Goal reopen operation');
542
+ state.goalStatus = 'active';
543
+ state.goalStartedAt ??= nowIso(now);
544
+ delete state.goalEndedAt;
545
+ state.stopRequestedAt = undefined;
546
+ state.cancelRequestedAt = undefined;
547
+ state.lastFailure = undefined;
548
+ state.nextAttemptAt = undefined;
549
+ state.retryBackoffMs = undefined;
550
+ state.consecutiveErrors = 0;
551
+ }
552
+ else {
553
+ state.goalStatus = currentGoalStatus === 'active' ? 'paused' : currentGoalStatus;
554
+ }
546
555
  }
556
+ const next = { ...existing, mode, state, name: patch.name !== undefined ? patch.name.trim() : existing.name, description: patch.description !== undefined ? patch.description?.trim() || undefined : existing.description, enabled, target, profile: patch.profile ?? existing.profile, prompt: patch.prompt ?? existing.prompt, maxIterations: hasOwn(patch, 'maxIterations') ? normalizeMaxIterations(patch.maxIterations ?? undefined) : existing.maxIterations, tokenBudget: mode === 'ralph' ? undefined : hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget, tokenReserve: mode === 'ralph' || (hasOwn(patch, 'tokenBudget') && patch.tokenBudget === null && !hasOwn(patch, 'tokenReserve')) ? undefined : hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve, stopPolicy, modelOverride: runtimeOptions.modelOverride, thinkingLevel: runtimeOptions.thinkingLevel, fastMode: runtimeOptions.fastMode, updatedAt: nowIso(now) };
557
+ validateJobInput(next);
558
+ this.writeJob(next);
559
+ this.db.exec('COMMIT');
560
+ return this.getJob(id);
561
+ }
562
+ catch (error) {
563
+ this.db.exec('ROLLBACK');
564
+ throw error;
547
565
  }
548
- const next = { ...existing, mode, state, name: patch.name !== undefined ? patch.name.trim() : existing.name, description: patch.description !== undefined ? patch.description?.trim() || undefined : existing.description, enabled, target, profile: patch.profile ?? existing.profile, prompt: patch.prompt ?? existing.prompt, maxIterations: hasOwn(patch, 'maxIterations') ? normalizeMaxIterations(patch.maxIterations ?? undefined) : existing.maxIterations, tokenBudget: mode === 'ralph' ? undefined : hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget, tokenReserve: mode === 'ralph' || (hasOwn(patch, 'tokenBudget') && patch.tokenBudget === null && !hasOwn(patch, 'tokenReserve')) ? undefined : hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve, stopPolicy, modelOverride: runtimeOptions.modelOverride, thinkingLevel: runtimeOptions.thinkingLevel, fastMode: runtimeOptions.fastMode, updatedAt: nowIso(now) };
549
- validateJobInput(next);
550
- this.writeJob(next);
551
- return this.getJob(id);
552
566
  }
553
567
  updateJobResources(id, resources, now = new Date()) {
554
568
  const existing = this.getJob(id);
@@ -621,6 +635,7 @@ export class PiboLoopStore {
621
635
  return Number(result.changes ?? 0) > 0 ? this.getRunByMessageEventId(eventId) : undefined;
622
636
  }
623
637
  reserveRun(id, now = new Date()) { this.updateJob(id, { enabled: true }, now); return this.reserveJob(id, now); }
638
+ reserveAdmittedRun(id, now = new Date()) { return this.reserveJob(id, now, true); }
624
639
  reserveDueRuns(limit, now = new Date()) {
625
640
  const rows = this.db.prepare('SELECT * FROM pibo_ralph_jobs WHERE enabled = 1 ORDER BY updated_at ASC').all();
626
641
  const result = [];
@@ -667,41 +682,47 @@ export class PiboLoopStore {
667
682
  }
668
683
  completeRun(input, now = new Date()) {
669
684
  const timestamp = nowIso(now);
670
- const job = this.getJob(input.jobId);
671
- if (!job)
672
- return;
673
- const completedIterations = (job.state.completedIterations ?? 0) + 1;
674
- const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
675
- const currentGoalStatus = goalStatus(job);
676
- const nextGoalStatus = job.mode === 'goal'
677
- ? isTerminalGoalStatus(currentGoalStatus) || currentGoalStatus === 'paused'
678
- ? currentGoalStatus
679
- : input.goalStatus ?? currentGoalStatus
680
- : undefined;
681
- const terminalGoalStatus = job.mode === 'goal' && isTerminalGoalStatus(nextGoalStatus);
682
- const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
683
- const state = {
684
- ...job.state,
685
- ...(job.mode === 'goal' ? { activeTimeRunningAt: null } : {}),
686
- runningAt: undefined,
687
- completedIterations,
688
- lastRunAt: timestamp,
689
- lastRunId: input.runId,
690
- lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok',
691
- lastError: input.error,
692
- lastFailure: input.status === 'error' ? input.failure : undefined,
693
- nextAttemptAt: input.status === 'error' ? input.failure?.nextAttemptAt : undefined,
694
- retryBackoffMs: input.status === 'error' ? input.failure?.retryBackoffMs : undefined,
695
- lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId,
696
- consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0,
697
- conditionStates: input.conditionStates ?? job.state.conditionStates,
698
- lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation,
699
- ...(nextGoalStatus ? { goalStatus: nextGoalStatus } : {}),
700
- ...(terminalGoalStatus ? { goalEndedAt: job.state.goalEndedAt ?? timestamp } : {}),
701
- };
702
685
  this.db.exec('BEGIN IMMEDIATE');
703
686
  try {
704
- this.db.prepare("UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = COALESCE(?, pibo_session_id), reason = ?, error = ?, error_details_json = ?, message_state = CASE WHEN message_state = 'invalidated' THEN message_state ELSE 'finished' END, completed_at = ?, updated_at = ? WHERE id = ?").run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, sessionErrorDetailsJson(input.errorDetails), timestamp, timestamp, input.runId);
687
+ const result = this.db.prepare("UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = COALESCE(?, pibo_session_id), reason = ?, error = ?, error_details_json = ?, message_state = CASE WHEN message_state = 'invalidated' THEN message_state ELSE 'finished' END, completed_at = ?, updated_at = ? WHERE id = ? AND job_id = ? AND status = 'running'").run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, sessionErrorDetailsJson(input.errorDetails), timestamp, timestamp, input.runId, input.jobId);
688
+ if (Number(result.changes ?? 0) === 0) {
689
+ this.db.exec('COMMIT');
690
+ return;
691
+ }
692
+ const job = this.getJob(input.jobId);
693
+ if (!job || !job.state.runningAt || job.state.lastRunId !== input.runId) {
694
+ this.db.exec('COMMIT');
695
+ return;
696
+ }
697
+ const completedIterations = (job.state.completedIterations ?? 0) + 1;
698
+ const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
699
+ const currentGoalStatus = goalStatus(job);
700
+ const nextGoalStatus = job.mode === 'goal'
701
+ ? isTerminalGoalStatus(currentGoalStatus) || currentGoalStatus === 'paused'
702
+ ? currentGoalStatus
703
+ : input.goalStatus ?? currentGoalStatus
704
+ : undefined;
705
+ const terminalGoalStatus = job.mode === 'goal' && isTerminalGoalStatus(nextGoalStatus);
706
+ const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
707
+ const state = {
708
+ ...job.state,
709
+ ...(job.mode === 'goal' ? { activeTimeRunningAt: null } : {}),
710
+ runningAt: undefined,
711
+ completedIterations,
712
+ lastRunAt: timestamp,
713
+ lastRunId: input.runId,
714
+ lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok',
715
+ lastError: input.error,
716
+ lastFailure: input.status === 'error' ? input.failure : undefined,
717
+ nextAttemptAt: input.status === 'error' ? input.failure?.nextAttemptAt : undefined,
718
+ retryBackoffMs: input.status === 'error' ? input.failure?.retryBackoffMs : undefined,
719
+ lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId,
720
+ consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0,
721
+ conditionStates: input.conditionStates ?? job.state.conditionStates,
722
+ lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation,
723
+ ...(nextGoalStatus ? { goalStatus: nextGoalStatus } : {}),
724
+ ...(terminalGoalStatus ? { goalEndedAt: job.state.goalEndedAt ?? timestamp } : {}),
725
+ };
705
726
  this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(shouldDisable ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, job.id);
706
727
  this.db.exec('COMMIT');
707
728
  }
@@ -768,12 +789,17 @@ export class PiboLoopStore {
768
789
  this.updateRunResources({ jobId: job.id, runId, resources: nextResources });
769
790
  this.updateJobResources(job.id, nextResources);
770
791
  }
771
- reserveJob(id, now = new Date()) {
792
+ reserveJob(id, now = new Date(), requireAdmission = false) {
772
793
  const timestamp = nowIso(now);
773
794
  this.db.exec('BEGIN IMMEDIATE');
774
795
  try {
775
796
  const job = this.getJob(id);
776
- if (!job || !job.enabled || job.state.runningAt) {
797
+ if (!job || !job.enabled || job.state.runningAt || (requireAdmission && job.state.cancelRequestedAt !== undefined)) {
798
+ this.db.exec('COMMIT');
799
+ return undefined;
800
+ }
801
+ if (job.maxIterations !== undefined && (job.state.completedIterations ?? 0) >= job.maxIterations) {
802
+ this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, updated_at = ? WHERE id = ?').run(timestamp, job.id);
777
803
  this.db.exec('COMMIT');
778
804
  return undefined;
779
805
  }
@@ -1,5 +1,5 @@
1
- import { readFile, writeFile } from 'node:fs/promises';
2
- import { ensureConfigExists, isHttpServer, loadConfigUnresolved, } from './config.js';
1
+ import { stat, writeFile } from 'node:fs/promises';
2
+ import { isHttpServer, loadConfigUnresolved, resolveMcpServerConfigSource, } from './config.js';
3
3
  import { ErrorCode, formatCliError } from './errors.js';
4
4
  export const MCP_SERVER_DESCRIPTION_MAX_LENGTH = 480;
5
5
  export const ENABLED_MCP_SERVERS_CONTEXT_PATH = '.pibo/context/enabled-mcp-servers.md';
@@ -34,15 +34,19 @@ export async function listMcpServerInfos(configPath) {
34
34
  }
35
35
  export async function setMcpServerDescription(serverName, descriptionInput, configPath) {
36
36
  const description = normalizeMcpServerDescription(descriptionInput);
37
- const path = await ensureConfigExists(configPath);
38
- const config = await readMcpConfig(path);
39
- const server = config.mcpServers[serverName];
40
- if (!server) {
37
+ const { path, config, server } = await resolveMcpServerConfigSource(serverName, configPath);
38
+ if (server.pibo?.descriptionSource === 'registry') {
41
39
  throw new Error(formatCliError({
42
40
  code: ErrorCode.CLIENT_ERROR,
43
- type: 'SERVER_NOT_FOUND',
44
- message: `Server "${serverName}" not found in config`,
45
- details: `Available servers: ${Object.keys(config.mcpServers).join(', ') || '(none)'}`,
41
+ type: 'MCP_DESCRIPTION_READ_ONLY',
42
+ message: `Server "${serverName}" description is read-only`,
43
+ }));
44
+ }
45
+ if (process.platform !== 'win32' && ((await stat(path)).mode & 0o222) === 0) {
46
+ throw new Error(formatCliError({
47
+ code: ErrorCode.CLIENT_ERROR,
48
+ type: 'MCP_CONFIG_READ_ONLY',
49
+ message: `Server "${serverName}" config is read-only`,
46
50
  }));
47
51
  }
48
52
  server.pibo = {
@@ -91,23 +95,6 @@ export function getMcpAgentContextFileFromConfig(selectedServerNames, config) {
91
95
  ].join('\n').trimEnd(),
92
96
  };
93
97
  }
94
- async function readMcpConfig(path) {
95
- const content = await readFile(path, 'utf-8');
96
- const parsed = JSON.parse(content);
97
- if (!parsed ||
98
- typeof parsed !== 'object' ||
99
- !('mcpServers' in parsed) ||
100
- typeof parsed.mcpServers !== 'object' ||
101
- parsed.mcpServers === null) {
102
- throw new Error(formatCliError({
103
- code: ErrorCode.CLIENT_ERROR,
104
- type: 'CONFIG_MISSING_FIELD',
105
- message: 'Config file missing required "mcpServers" object',
106
- details: `File: ${path}`,
107
- }));
108
- }
109
- return parsed;
110
- }
111
98
  function mcpServerInfoFromConfig(name, server) {
112
99
  const metadata = normalizeMetadata(server.pibo);
113
100
  const description = metadata.description;
@@ -28,7 +28,9 @@ export async function infoCommand(options) {
28
28
  process.exit(ErrorCode.CLIENT_ERROR);
29
29
  }
30
30
  const { server: serverName, tool: toolName } = parseTarget(options.target);
31
- const serverConfig = config.mcpServers[serverName];
31
+ const serverConfig = Object.hasOwn(config.mcpServers, serverName)
32
+ ? config.mcpServers[serverName]
33
+ : undefined;
32
34
  if (!serverConfig) {
33
35
  const available = Object.keys(config.mcpServers);
34
36
  const serverList = available.length > 0 ? available.join(', ') : '(none)';
@@ -1,6 +1,9 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
- import { ensureConfigExists, findConfigPath, getPreferredConfigPath, } from './config.js';
3
+ import { resolve } from 'node:path';
4
+ import { CustomAgentStore } from '../apps/chat/agent-store.js';
5
+ import { piboHomePath } from '../core/pibo-home.js';
6
+ import { ensureConfigExists, findConfigPath, getConfigSearchPaths, getPreferredConfigPath, } from './config.js';
4
7
  import { ErrorCode, formatCliError } from './errors.js';
5
8
  import { setMcpServerDescription } from './agent-context.js';
6
9
  const EXAMPLE_CONFIG = {
@@ -97,6 +100,35 @@ async function readRawConfig(path) {
97
100
  async function writeRawConfig(path, config) {
98
101
  await writeFile(path, `${JSON.stringify(config, null, 2)}\n`);
99
102
  }
103
+ async function mergedConfigRetainsServer(name, updatedPath, updatedConfig, explicitPath) {
104
+ const resolvedUpdatedPath = resolve(updatedPath);
105
+ for (const sourcePath of getConfigSearchPaths(explicitPath)) {
106
+ if (!existsSync(sourcePath))
107
+ continue;
108
+ const sourceConfig = resolve(sourcePath) === resolvedUpdatedPath
109
+ ? updatedConfig
110
+ : await readRawConfig(sourcePath);
111
+ if (Object.hasOwn(sourceConfig.mcpServers, name))
112
+ return true;
113
+ }
114
+ return false;
115
+ }
116
+ function customAgentsSelectingMcpServer(name) {
117
+ const storePath = piboHomePath('chat-agents.sqlite');
118
+ if (!existsSync(storePath))
119
+ return [];
120
+ const store = new CustomAgentStore(storePath);
121
+ try {
122
+ return store
123
+ .list({ includeArchived: true })
124
+ .filter((agent) => agent.mcpServers.includes(name))
125
+ .map((agent) => agent.profileName)
126
+ .sort((left, right) => left.localeCompare(right));
127
+ }
128
+ finally {
129
+ store.close();
130
+ }
131
+ }
100
132
  function parseServerConfig(input) {
101
133
  let parsed;
102
134
  try {
@@ -205,7 +237,7 @@ export async function configCommand(options) {
205
237
  suggestion: 'Example: pibo mcp config remove filesystem',
206
238
  }));
207
239
  }
208
- if (!(options.name in config.mcpServers)) {
240
+ if (!Object.hasOwn(config.mcpServers, options.name)) {
209
241
  throw new Error(formatCliError({
210
242
  code: ErrorCode.CLIENT_ERROR,
211
243
  type: 'SERVER_NOT_FOUND',
@@ -214,6 +246,18 @@ export async function configCommand(options) {
214
246
  }));
215
247
  }
216
248
  delete config.mcpServers[options.name];
249
+ if (!(await mergedConfigRetainsServer(options.name, path, config, options.configPath))) {
250
+ const affectedAgents = customAgentsSelectingMcpServer(options.name);
251
+ if (affectedAgents.length > 0) {
252
+ throw new Error(formatCliError({
253
+ code: ErrorCode.CLIENT_ERROR,
254
+ type: 'MCP_SERVER_IN_USE',
255
+ message: `MCP server "${options.name}" is selected by custom agents`,
256
+ details: `Custom agents: ${affectedAgents.join(', ')}`,
257
+ suggestion: 'Update those agents to stop selecting this MCP server, then retry the removal.',
258
+ }));
259
+ }
260
+ }
217
261
  await writeRawConfig(path, config);
218
262
  console.log(`Removed MCP server "${options.name}" from ${path}`);
219
263
  }
@@ -496,7 +496,7 @@ export async function loadConfigUnresolved(explicitPath) {
496
496
  for (const configPath of existingPaths) {
497
497
  const config = await readRawConfig(configPath);
498
498
  for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
499
- if (!(serverName in merged.mcpServers)) {
499
+ if (!Object.hasOwn(merged.mcpServers, serverName)) {
500
500
  merged.mcpServers[serverName] = serverConfig;
501
501
  }
502
502
  }
@@ -506,6 +506,21 @@ export async function loadConfigUnresolved(explicitPath) {
506
506
  }
507
507
  return merged;
508
508
  }
509
+ /**
510
+ * Resolve the highest-priority config file that contributes a merged server.
511
+ */
512
+ export async function resolveMcpServerConfigSource(serverName, explicitPath) {
513
+ const merged = await loadConfigUnresolved(explicitPath);
514
+ for (const configPath of getConfigSearchPaths(explicitPath)) {
515
+ if (!existsSync(configPath))
516
+ continue;
517
+ const config = await readRawConfig(configPath);
518
+ if (Object.hasOwn(config.mcpServers, serverName)) {
519
+ return { path: configPath, config, server: config.mcpServers[serverName] };
520
+ }
521
+ }
522
+ throw new Error(formatCliError(serverNotFoundError(serverName, Object.keys(merged.mcpServers))));
523
+ }
509
524
  export async function loadConfig(explicitPath) {
510
525
  return substituteEnvVarsInObject(await loadConfigUnresolved(explicitPath));
511
526
  }
@@ -539,12 +554,11 @@ export function formatConfigSourceSummaries(summaries) {
539
554
  * Get a specific server config by name
540
555
  */
541
556
  export function getServerConfig(config, serverName) {
542
- const server = config.mcpServers[serverName];
543
- if (!server) {
557
+ if (!Object.hasOwn(config.mcpServers, serverName)) {
544
558
  const available = Object.keys(config.mcpServers);
545
559
  throw new Error(formatCliError(serverNotFoundError(serverName, available)));
546
560
  }
547
- return server;
561
+ return config.mcpServers[serverName];
548
562
  }
549
563
  /**
550
564
  * List all server names
@@ -101,6 +101,36 @@ function revisionRowToRecord(row) {
101
101
  ...(row.actor_id ? { actorId: row.actor_id } : {}),
102
102
  };
103
103
  }
104
+ export function readContextFileCatalog(path) {
105
+ const resolvedPath = resolve(path);
106
+ if (!existsSync(resolvedPath))
107
+ return [];
108
+ const db = new DatabaseSync(resolvedPath, { readOnly: true });
109
+ try {
110
+ const table = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'context_files'").get();
111
+ if (!table)
112
+ return [];
113
+ const columns = new Set(db.prepare("PRAGMA table_info(context_files)").all().map((column) => column.name));
114
+ const required = ["key", "label", "managed_path", "scope", "agent_profile_name"];
115
+ if (!required.every((column) => columns.has(column)))
116
+ return [];
117
+ const rows = db.prepare(`
118
+ SELECT key, label, managed_path, scope, agent_profile_name
119
+ FROM context_files
120
+ ORDER BY updated_at DESC
121
+ `).all();
122
+ return rows.map((row) => ({
123
+ key: row.key,
124
+ label: row.label,
125
+ managedPath: row.managed_path,
126
+ scope: row.scope,
127
+ ...(row.agent_profile_name ? { agentProfileName: row.agent_profile_name } : {}),
128
+ }));
129
+ }
130
+ finally {
131
+ db.close();
132
+ }
133
+ }
104
134
  export class ContextFileMetadataStore {
105
135
  legacyStorePath;
106
136
  db;
@@ -415,6 +445,12 @@ export class ContextFileMetadataStore {
415
445
  WHERE key = ?
416
446
  `).run(file.workingContent, file.sourceContent ?? null, file.key);
417
447
  }
448
+ for (const file of recovered) {
449
+ if (!file.restoreManagedFile)
450
+ continue;
451
+ mkdirSync(dirname(file.managedPath), { recursive: true });
452
+ writeFileSync(file.managedPath, file.workingContent, "utf8");
453
+ }
418
454
  const migratedAt = new Date().toISOString();
419
455
  this.db.prepare(`
420
456
  INSERT INTO context_file_store_meta (key, value) VALUES (?, ?)
@@ -430,12 +466,6 @@ export class ContextFileMetadataStore {
430
466
  this.db.exec("ROLLBACK");
431
467
  throw error;
432
468
  }
433
- for (const file of recovered) {
434
- if (!file.restoreManagedFile)
435
- continue;
436
- mkdirSync(dirname(file.managedPath), { recursive: true });
437
- writeFileSync(file.managedPath, file.workingContent, "utf8");
438
- }
439
469
  }
440
470
  migrateLegacyStore() {
441
471
  if (!this.legacyStorePath)
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { readFallbackPidFile, readPidFile } from "../gateway/pidfile.js";
7
7
  import { PiboWebHttpError, readJsonBody, responseHtml, responseJson } from "../web/http.js";
8
8
  import { definePiboPlugin } from "./registry.js";
9
- import { buildContextFileDiff, ContextFileMetadataStore, hashContextFileContent, } from "./context-files-store.js";
9
+ import { buildContextFileDiff, ContextFileMetadataStore, hashContextFileContent, readContextFileCatalog, } from "./context-files-store.js";
10
10
  export const CONTEXT_FILES_APP_NAME = "pibo.context-files";
11
11
  export const CONTEXT_FILES_MOUNT_PATH = "/apps/context-files";
12
12
  export const CONTEXT_FILES_API_PREFIX = "/api/context-files";
@@ -1028,6 +1028,57 @@ function createContextFilesWebApp(service) {
1028
1028
  },
1029
1029
  };
1030
1030
  }
1031
+ export function createPiboContextFilesCatalogPlugin(options = {}) {
1032
+ const paths = resolveContextFilesPaths(options);
1033
+ return definePiboPlugin({
1034
+ id: "pibo.context-files-catalog",
1035
+ name: "Pibo Context Files Catalog",
1036
+ register(api) {
1037
+ const registeredPaths = new Set();
1038
+ const registeredKeys = new Set();
1039
+ try {
1040
+ for (const file of readContextFileCatalog(paths.metadataPath)) {
1041
+ api.upsertContextFile(profileForManaged(file));
1042
+ registeredPaths.add(resolve(file.managedPath));
1043
+ registeredKeys.add(file.key);
1044
+ }
1045
+ }
1046
+ catch (error) {
1047
+ console.warn(`[pibo] Skipping managed context-file catalog registration: ${error instanceof Error ? error.message : String(error)}`);
1048
+ }
1049
+ let entries = [];
1050
+ try {
1051
+ entries = readdirSync(paths.globalDir, { withFileTypes: true });
1052
+ }
1053
+ catch (error) {
1054
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
1055
+ if (code !== "ENOENT") {
1056
+ console.warn(`[pibo] Skipping global context-file discovery: ${error instanceof Error ? error.message : String(error)}`);
1057
+ }
1058
+ }
1059
+ const usedKeys = new Set(registeredKeys);
1060
+ for (const entry of entries) {
1061
+ if (!entry.isFile())
1062
+ continue;
1063
+ const extension = extname(entry.name).toLowerCase();
1064
+ if (extension !== ".md" && extension !== ".markdown")
1065
+ continue;
1066
+ const path = resolve(paths.globalDir, entry.name);
1067
+ if (registeredPaths.has(path))
1068
+ continue;
1069
+ const key = uniqueKey(`ctx:${slugSegment(entry.name.slice(0, -extension.length) || entry.name)}`, usedKeys);
1070
+ usedKeys.add(key);
1071
+ api.upsertContextFile({
1072
+ key,
1073
+ label: labelFromManagedPath(entry.name),
1074
+ path,
1075
+ source: "managed",
1076
+ scope: "global",
1077
+ });
1078
+ }
1079
+ },
1080
+ });
1081
+ }
1031
1082
  export function createPiboContextFilesPlugin(options = {}) {
1032
1083
  const paths = resolveContextFilesPaths(options);
1033
1084
  return definePiboPlugin({
@@ -521,11 +521,13 @@ export class PiboPluginRegistry {
521
521
  nativeTools: sessionContext.tools.filter((tool) => tool.enabled !== false).map((tool) => tool.name),
522
522
  skills: sessionContext.skills.filter((skill) => skill.enabled !== false).map((skill) => skill.name),
523
523
  contextFiles: sessionContext.contextFiles.filter((contextFile) => contextFile.enabled !== false).map(contextFileKey),
524
+ diagnostics: sessionContext.diagnostics.map((diagnostic) => ({ ...diagnostic })),
524
525
  subagents: sessionContext.subagents.filter((subagent) => subagent.enabled !== false),
525
526
  mcpServers: [...sessionContext.mcpServers],
526
527
  piPackages: sessionContext.piPackages.filter((pkg) => pkg.enabled !== false).map((pkg) => pkg.id),
527
528
  model: sessionContext.model ? { ...sessionContext.model } : undefined,
528
529
  mainModel: sessionContext.mainModel ? { ...sessionContext.mainModel } : undefined,
530
+ mainModelFallbacks: sessionContext.mainModelFallbacks.map((model) => ({ ...model })),
529
531
  subagentModel: sessionContext.subagentModel ? { ...sessionContext.subagentModel } : undefined,
530
532
  thinkingLevel: sessionContext.thinkingLevel,
531
533
  mainThinkingLevel: sessionContext.mainThinkingLevel,
@@ -0,0 +1,22 @@
1
+ import { createPiboChatCustomAgentProfilesPlugin, } from "./chat-custom-agents.js";
2
+ import { createPiboChatUserSkillsPlugin } from "./chat-user-skills.js";
3
+ import { createPiboContextFilesCatalogPlugin, createPiboContextFilesPlugin, } from "./context-files.js";
4
+ import { createDefaultPiboPluginRegistry } from "./builtin.js";
5
+ export function createPiboUserProfileResourcePlugins(options = {}) {
6
+ return [
7
+ createPiboChatUserSkillsPlugin(options.userSkills),
8
+ options.contextFilesMode === "catalog"
9
+ ? createPiboContextFilesCatalogPlugin(options.contextFiles)
10
+ : createPiboContextFilesPlugin(options.contextFiles),
11
+ createPiboChatCustomAgentProfilesPlugin(options.customAgents),
12
+ ];
13
+ }
14
+ export function createDefaultPiboUserProfileRegistry(options = {}) {
15
+ const registry = createDefaultPiboPluginRegistry();
16
+ registerPiboUserProfileResources(registry, { contextFilesMode: "catalog", ...options });
17
+ return registry;
18
+ }
19
+ export function registerPiboUserProfileResources(registry, options = {}) {
20
+ for (const plugin of createPiboUserProfileResourcePlugins(options))
21
+ registry.registerPlugin(plugin);
22
+ }
@@ -0,0 +1,34 @@
1
+ import { isIP } from "node:net";
2
+ function isLocalPreviewHostname(hostname) {
3
+ return hostname === "localhost" || hostname.endsWith(".localhost");
4
+ }
5
+ function isValidDnsHostname(hostname) {
6
+ return isIP(hostname) === 0 &&
7
+ hostname.length <= 253 &&
8
+ !hostname.endsWith(".") &&
9
+ hostname.split(".").every((label) => label.length >= 1 &&
10
+ label.length <= 63 &&
11
+ /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label));
12
+ }
13
+ export function parsePreviewBaseURL(value) {
14
+ let url;
15
+ try {
16
+ url = new URL(value);
17
+ }
18
+ catch {
19
+ throw new Error("preview.baseURL must be an absolute HTTP or HTTPS URL");
20
+ }
21
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
22
+ throw new Error("preview.baseURL must use http or https");
23
+ }
24
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
25
+ throw new Error("preview.baseURL must contain only scheme, hostname, and optional port");
26
+ }
27
+ if (!isValidDnsHostname(url.hostname)) {
28
+ throw new Error("preview.baseURL hostname must be a DNS hostname without wildcards, IP literals, or a trailing dot");
29
+ }
30
+ if (url.protocol === "http:" && !isLocalPreviewHostname(url.hostname)) {
31
+ throw new Error("preview.baseURL must use https except for localhost development");
32
+ }
33
+ return url;
34
+ }