@open-mercato/cezar 0.9.0-pr646.708 → 0.9.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 (109) hide show
  1. package/README.md +1 -17
  2. package/dist/config.d.ts +0 -14
  3. package/dist/config.js +0 -39
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/claude-ui-mapper.d.ts +0 -9
  6. package/dist/core/claude-ui-mapper.js +2 -31
  7. package/dist/core/claude-ui-mapper.js.map +1 -1
  8. package/dist/core/codex-app-server-runner.js +18 -19
  9. package/dist/core/codex-app-server-runner.js.map +1 -1
  10. package/dist/core/opencode-server-runner.js +5 -25
  11. package/dist/core/opencode-server-runner.js.map +1 -1
  12. package/dist/core/tool-display.d.ts +1 -2
  13. package/dist/core/tool-display.js +1 -6
  14. package/dist/core/tool-display.js.map +1 -1
  15. package/dist/index.js +4 -40
  16. package/dist/index.js.map +1 -1
  17. package/dist/runs/auto-name.js +0 -6
  18. package/dist/runs/auto-name.js.map +1 -1
  19. package/dist/runs/store.js +9 -34
  20. package/dist/runs/store.js.map +1 -1
  21. package/dist/server/capabilities.d.ts +0 -6
  22. package/dist/server/capabilities.js +0 -6
  23. package/dist/server/capabilities.js.map +1 -1
  24. package/dist/server/server.d.ts +0 -8
  25. package/dist/server/server.js +7 -146
  26. package/dist/server/server.js.map +1 -1
  27. package/dist/server-install/engine.d.ts +0 -5
  28. package/dist/server-install/engine.js +0 -6
  29. package/dist/server-install/engine.js.map +1 -1
  30. package/dist/server-install/platforms/ubuntu-vps.d.ts +2 -11
  31. package/dist/server-install/platforms/ubuntu-vps.js +10 -123
  32. package/dist/server-install/platforms/ubuntu-vps.js.map +1 -1
  33. package/dist/server-install/types.d.ts +0 -39
  34. package/dist/server-install/types.js +0 -13
  35. package/dist/server-install/types.js.map +1 -1
  36. package/dist/skills-remote.d.ts +0 -11
  37. package/dist/skills-remote.js +2 -31
  38. package/dist/skills-remote.js.map +1 -1
  39. package/dist/skills.d.ts +0 -29
  40. package/dist/skills.js +14 -72
  41. package/dist/skills.js.map +1 -1
  42. package/dist/workflows/run.d.ts +10 -50
  43. package/dist/workflows/run.js +103 -216
  44. package/dist/workflows/run.js.map +1 -1
  45. package/dist/workspace/config.d.ts +0 -30
  46. package/dist/workspace/config.js +0 -5
  47. package/dist/workspace/config.js.map +1 -1
  48. package/dist/workspace/projects-cli.d.ts +0 -2
  49. package/dist/workspace/projects-cli.js +4 -21
  50. package/dist/workspace/projects-cli.js.map +1 -1
  51. package/dist/workspace/projects.d.ts +1 -5
  52. package/dist/workspace/projects.js +8 -5
  53. package/dist/workspace/projects.js.map +1 -1
  54. package/dist/workspace/semaphore.d.ts +2 -53
  55. package/dist/workspace/semaphore.js +5 -93
  56. package/dist/workspace/semaphore.js.map +1 -1
  57. package/package.json +1 -1
  58. package/web/dist/assets/centered-state-zzWBfO_1.js +43 -0
  59. package/web/dist/assets/command-ylf8-fG0.js +1 -0
  60. package/web/dist/assets/compare-variants-BcR3zdtB.js +1 -0
  61. package/web/dist/assets/dialog-BYr_BAil.js +1 -0
  62. package/web/dist/assets/diff-view-DPCR7gon.js +4 -0
  63. package/web/dist/assets/{dist-CqH67S8q.js → dist-Cw_CDvC3.js} +1 -1
  64. package/web/dist/assets/dropdown-menu-C3GD8eLA.js +1 -0
  65. package/web/dist/assets/git-toolbar-CiMaCi03.js +1 -0
  66. package/web/dist/assets/github-BJEZbEOp.js +1 -0
  67. package/web/dist/assets/index-DmW_TYjO.js +16 -0
  68. package/web/dist/assets/index-Qbs-JRWQ.css +2 -0
  69. package/web/dist/assets/loader-circle-CGwIVNTv.js +1 -0
  70. package/web/dist/assets/{project-router-BaWVyqc8.js → project-router-CT8Rt5Eu.js} +1 -1
  71. package/web/dist/assets/prompt-templates-DW7_d8Ys.js +15 -0
  72. package/web/dist/assets/repo-git-BkmyKcvN.js +1 -0
  73. package/web/dist/assets/{run-diff-Cksr4Dk5.js → run-diff-CxIDdRwy.js} +2 -2
  74. package/web/dist/assets/run-header-8nwDIWWO.js +1 -0
  75. package/web/dist/assets/skill-empty-hint-Cl3ESoPu.js +1 -0
  76. package/web/dist/assets/skills-BIpb0mqO.js +1 -0
  77. package/web/dist/assets/{tab-link-vtgjgL_M.js → tab-link-C_bk_Kvn.js} +1 -1
  78. package/web/dist/assets/task-changes-D1bJx2uW.js +1 -0
  79. package/web/dist/assets/task-commits-DlhB21mR.js +1 -0
  80. package/web/dist/assets/task-files-ChJDOFg5.js +2 -0
  81. package/web/dist/assets/task-thread-J7ckxJJh.js +9 -0
  82. package/web/dist/assets/tasks-table--V1pKwuK.js +1 -0
  83. package/web/dist/assets/use-desktop-CuDnWju1.js +3 -0
  84. package/web/dist/assets/workflows-BksgdYAQ.js +11 -0
  85. package/web/dist/assets/x-DgoqYQOJ.js +1 -0
  86. package/web/dist/index.html +12 -6
  87. package/dist/core/v1-text-coalescer.d.ts +0 -36
  88. package/dist/core/v1-text-coalescer.js +0 -72
  89. package/dist/core/v1-text-coalescer.js.map +0 -1
  90. package/web/dist/assets/centered-state-CLmJ67EV.js +0 -43
  91. package/web/dist/assets/commit-list-CSaWuw-x.js +0 -1
  92. package/web/dist/assets/compare-variants-B5BlnpUb.js +0 -1
  93. package/web/dist/assets/diff-view-CYKPGL2w.js +0 -6
  94. package/web/dist/assets/git-toolbar-CDII-3Uv.js +0 -1
  95. package/web/dist/assets/github-C7SNDPf3.js +0 -1
  96. package/web/dist/assets/index-CcfkF2Ey.js +0 -30
  97. package/web/dist/assets/index-DzroxqBG.css +0 -2
  98. package/web/dist/assets/lib-DPEZDBUN.js +0 -1
  99. package/web/dist/assets/repo-git-4l-2tm7d.js +0 -1
  100. package/web/dist/assets/run-header-B3kW2sWC.js +0 -1
  101. package/web/dist/assets/skill-empty-hint-DTUSp6Vl.js +0 -1
  102. package/web/dist/assets/skills-D7hPgl4y.js +0 -1
  103. package/web/dist/assets/task-changes-DvIUGL9a.js +0 -1
  104. package/web/dist/assets/task-commits-QD2JI3zj.js +0 -1
  105. package/web/dist/assets/task-files-BE7JDEwK.js +0 -2
  106. package/web/dist/assets/task-thread-CRjuPhsl.js +0 -9
  107. package/web/dist/assets/textarea-Bj_LCreg.js +0 -1
  108. package/web/dist/assets/use-desktop-4e8LLqMG.js +0 -3
  109. package/web/dist/assets/workflows-bPYx0nbO.js +0 -11
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
4
- import { dirname, join } from 'node:path';
4
+ import { join } from 'node:path';
5
5
  import { parseAskMarker, stripAskMarker } from '../core/ask.js';
6
6
  import { onUsage, registerRunProcess, unregisterRunProcess } from '../core/process-usage.js';
7
7
  import { createRunner } from '../core/runner-factory.js';
@@ -46,19 +46,6 @@ const DONE_MARKER_RE = /CEZ:DONE\s*$/;
46
46
  * backends can't split the marker across text events.
47
47
  */
48
48
  const MONITORING_MARKER_RE = /CEZ:MONITORING\s*$/;
49
- /**
50
- * Preserve boundaries between complete assistant text blocks while a turn is
51
- * accumulated for marker parsing. The runners join these same v1 blocks with
52
- * newlines in `AgentRunResult`; matching that contract here prevents a
53
- * trailing `CEZ:TITLE=` block from absorbing later commentary (#623).
54
- */
55
- export function appendTurnText(current, next) {
56
- if (!current)
57
- return next;
58
- if (!next)
59
- return current;
60
- return `${current}\n${next}`;
61
- }
62
49
  /** Strip a trailing marker from one text event so transcripts stay free of
63
50
  * protocol noise. Delta backends may split the marker across events — then
64
51
  * it stays visible; detection above is unaffected. */
@@ -209,9 +196,6 @@ export class RunManager {
209
196
  * ordinary follow-up turns the moment the session opens. In-memory only. */
210
197
  deferredMessages = new Map();
211
198
  pumping = false;
212
- /** A pump that arrived while one was in flight — replayed by `pump()`'s own
213
- * loop so a slot freed mid-sweep is never a lost wakeup. */
214
- pumpAgain = false;
215
199
  /**
216
200
  * Runs normally isolate in worktrees and may execute in parallel. When that
217
201
  * isolation is unavailable (or explicitly disabled), serialize access to
@@ -240,8 +224,7 @@ export class RunManager {
240
224
  this.semaphore = options.semaphore ?? new WorkspaceSemaphore();
241
225
  this.offSemaphore = this.semaphore.register({
242
226
  busySlots: () => this.busySlots(),
243
- pump: () => this.pump(),
244
- oldestQueuedAt: () => this.oldestQueuedAt(),
227
+ pump: () => void this.pump(),
245
228
  });
246
229
  // Memory guard (#memory-guard): the shared process-tree sampler already ticks ~every 2 s for
247
230
  // the runs table; piggyback on it to enforce the per-task memory ceiling.
@@ -361,19 +344,6 @@ export class RunManager {
361
344
  // Persist the full definition so a queued run survives a restart (#367) —
362
345
  // ad-hoc "(planned)" chains exist nowhere else to re-resolve from.
363
346
  this.store.updateRun(run.id, { workflowDef: workflow });
364
- // Initial pasted images must be visible while the run is still queued (#612),
365
- // and must survive a restart before a slot opens. Persist them before the job
366
- // enters `pendingJobs`; `hydrateQueuedInput` reconstructs their content blocks
367
- // from these URLs when a recovered run eventually starts.
368
- if (input.images?.length) {
369
- const persisted = input.images
370
- .filter((b) => b.type === 'image')
371
- .map((b) => this.persistImage(run.id, b.source.media_type, b.source.data, 'pasted'))
372
- .filter((saved) => saved !== null);
373
- if (persisted.length) {
374
- this.store.updateRun(run.id, { taskImages: persisted.map((saved) => saved.url) });
375
- }
376
- }
377
347
  // Step-0 reference extraction (task auto-naming spec): the regex layer's
378
348
  // numbers persist immediately; the namer may add the kind it verified later.
379
349
  const skillHint = workflow.steps.find((s) => stepKind(s) === 'agent' && s.skill)?.skill?.trim();
@@ -420,95 +390,54 @@ export class RunManager {
420
390
  busySlots() {
421
391
  return this.active.size + this.starting.size - this.waiting.size;
422
392
  }
423
- /** Epoch ms of this manager's oldest queued run (the semaphore's fairness
424
- * key when a freed slot is broadcast), or null when nothing is queued.
425
- * `queue` is FIFO — `startRun` pushes and `recover()` re-queues by
426
- * `createdAt` — so the head is the oldest. */
427
- oldestQueuedAt() {
428
- const head = this.queue[0];
429
- if (!head)
430
- return null;
431
- const createdAt = this.store.getRun(head)?.createdAt;
432
- const ms = createdAt ? Date.parse(createdAt) : Number.NaN;
433
- return Number.isNaN(ms) ? null : ms;
434
- }
435
393
  /**
436
- * A slot this manager held just came free. Pump the whole WORKSPACE, not
437
- * just this manager: `maxParallel` is counted across every project, so the
438
- * run that should take the slot is the workspace's oldest queued one — which
439
- * usually sits in another project's queue. Pumping only `this` is what left
440
- * a queued run in project B stuck at `queued` while project A's runs came
441
- * and went. `release()` pumps this manager too, so it replaces the local
442
- * `pump()` at every slot-freeing transition.
443
- */
444
- releaseSlot() {
445
- void this.semaphore.release();
446
- }
447
- /**
448
- * Start queued runs while parallel slots are free. A run starts only under
449
- * BOTH ceilings: the WORKSPACE `resources.maxParallel` (default 2, counted
450
- * across every manager — spec 2026-07-20, step 2.5) AND this project's own
451
- * per-project `maxParallel` when the registry sets one (spec 2026-07-22,
452
- * inherits the workspace cap when unset). Legacy per-repo `maxParallel` keys
453
- * are ignored. A non-git directory degrades to 1 sequential run in the repo
454
- * root (spec 006 degradation rule), which is always the tighter bound.
394
+ * Start queued runs while parallel slots are free. The cap is the WORKSPACE
395
+ * `resources.maxParallel` (default 2), cached in the shared semaphore and
396
+ * counted across every manager (spec 2026-07-20, step 2.5) legacy per-repo
397
+ * `maxParallel` keys are ignored. A non-git directory degrades to 1
398
+ * sequential run in the repo root (spec 006 degradation rule).
455
399
  */
456
400
  async pump() {
457
- // A pump requested while one is in flight can't just be dropped: the
458
- // in-flight pass may already have read capacity (it awaits `getRepoInfo`
459
- // before the first check), so a slot freed in that window would be lost
460
- // until the next unrelated event. Re-run the sweep instead.
461
- if (this.pumping) {
462
- this.pumpAgain = true;
401
+ if (this.pumping)
463
402
  return;
464
- }
465
403
  this.pumping = true;
466
404
  try {
467
- do {
468
- this.pumpAgain = false;
469
- const repo = await getRepoInfo(this.repoRoot);
470
- const maxParallel = this.semaphore.maxParallel();
471
- // Per-project ceiling (spec 2026-07-22-per-project-concurrency): this
472
- // project never runs more than its own configured `maxParallel`; absent
473
- // an override it equals the workspace cap, so behavior is unchanged.
474
- const projectMax = this.semaphore.projectMaxParallel(this.repoRoot);
475
- // `waiting` runs don't hold a slot (#347) — see busySlots(). The check
476
- // below is the only slot gate: resumes never pass through it. A run
477
- // starts only under BOTH the workspace cap and this project's ceiling.
478
- const capacity = () => this.semaphore.busy() < maxParallel &&
479
- this.busySlots() < projectMax &&
480
- (repo !== null || this.busySlots() < 1);
481
- while (this.queue.length > 0 && capacity()) {
482
- const runId = this.queue.shift();
483
- if (!runId)
484
- break;
485
- const job = this.pendingJobs.get(runId);
486
- this.pendingJobs.delete(runId);
487
- if (!job)
488
- continue;
489
- this.starting.add(runId);
490
- // Rebuild the prompt from the store at the last instant (#472), so an edit
491
- // or a stacked message that landed while the run waited is honored. Entered
492
- // in the same synchronous tick as the `pendingJobs.delete` above, so no
493
- // handler can observe a half-dequeued run.
494
- const input = this.hydrateQueuedInput(runId, job.input);
495
- void this.execute(runId, job.workflow, input).catch((err) => {
496
- const message = err instanceof Error ? err.message : String(err);
497
- this.store.updateRun(runId, {
498
- status: 'failed',
499
- error: `engine crashed: ${message}`,
500
- finishedAt: new Date().toISOString(),
501
- });
502
- const state = this.active.get(runId);
503
- if (state) {
504
- this.clearIdleTimer(state);
505
- this.clearAutosaveTimer(state);
506
- }
507
- this.starting.delete(runId);
508
- this.dropActive(runId);
405
+ const repo = await getRepoInfo(this.repoRoot);
406
+ const maxParallel = this.semaphore.maxParallel();
407
+ // `waiting` runs don't hold a slot (#347) — see busySlots(). The check
408
+ // below is the only slot gate: resumes never pass through it.
409
+ const capacity = () => this.semaphore.busy() < maxParallel && (repo !== null || this.busySlots() < 1);
410
+ while (this.queue.length > 0 && capacity()) {
411
+ const runId = this.queue.shift();
412
+ if (!runId)
413
+ break;
414
+ const job = this.pendingJobs.get(runId);
415
+ this.pendingJobs.delete(runId);
416
+ if (!job)
417
+ continue;
418
+ this.starting.add(runId);
419
+ // Rebuild the prompt from the store at the last instant (#472), so an edit
420
+ // or a stacked message that landed while the run waited is honored. Entered
421
+ // in the same synchronous tick as the `pendingJobs.delete` above, so no
422
+ // handler can observe a half-dequeued run.
423
+ const input = this.hydrateQueuedInput(runId, job.input);
424
+ void this.execute(runId, job.workflow, input).catch((err) => {
425
+ const message = err instanceof Error ? err.message : String(err);
426
+ this.store.updateRun(runId, {
427
+ status: 'failed',
428
+ error: `engine crashed: ${message}`,
429
+ finishedAt: new Date().toISOString(),
509
430
  });
510
- }
511
- } while (this.pumpAgain);
431
+ const state = this.active.get(runId);
432
+ if (state) {
433
+ this.clearIdleTimer(state);
434
+ this.clearAutosaveTimer(state);
435
+ }
436
+ this.starting.delete(runId);
437
+ this.dropActive(runId);
438
+ void this.pump();
439
+ });
440
+ }
512
441
  }
513
442
  finally {
514
443
  this.pumping = false;
@@ -635,10 +564,6 @@ export class RunManager {
635
564
  this.active.delete(runId);
636
565
  this.memoryPausing.delete(runId);
637
566
  this.lastNamerKey.delete(runId);
638
- // The run's slot is gone from busySlots() as of the deletes above — hand it
639
- // to the workspace's oldest queued run, in ANY project. Every terminal path
640
- // funnels through here, so this one call covers them all.
641
- this.releaseSlot();
642
567
  // A run leaving the active registry is a terminal transition (done/review/
643
568
  // failed/cancelled) — the one moment the finished-worktree count can grow.
644
569
  // Enforce count-based retention (#483) here so a single hook covers every
@@ -704,7 +629,7 @@ export class RunManager {
704
629
  abort();
705
630
  };
706
631
  this.waiting.add(runId);
707
- this.releaseSlot();
632
+ void this.pump();
708
633
  try {
709
634
  await Promise.race([previous, cancelled]);
710
635
  }
@@ -759,45 +684,34 @@ export class RunManager {
759
684
  if (!run)
760
685
  return input;
761
686
  const stack = run.queuedMessages ?? [];
687
+ if (!stack.length)
688
+ return { ...input, task: run.task };
762
689
  const task = [run.task, ...stack.map((m) => m.text)]
763
690
  .map((part) => part.trim())
764
691
  .filter((part) => part.length > 0)
765
692
  .join('\n\n');
766
- const readImages = (urls, kind) => {
767
- const images = [];
768
- for (const url of urls) {
769
- const name = url.split('/').pop();
770
- if (!name || name.includes('..') || name.includes('/') || name.includes('\\'))
771
- continue;
772
- try {
773
- const data = readFileSync(join(this.dataDir, 'runs', `${runId}-images`, name));
774
- images.push({
775
- type: 'image',
776
- source: { type: 'base64', media_type: mediaTypeFor(name), data: data.toString('base64') },
777
- });
778
- }
779
- catch {
780
- // Degrade, never fail the boot (AGENTS.md): the user deleted `.ai/cezar/`
781
- // or the file is unreadable — start with the text and say which image went.
782
- this.store.appendEvent(runId, {
783
- type: 'note',
784
- message: `${kind} attachment ${name} could not be read — starting without it`,
785
- });
786
- }
693
+ const stackedImages = [];
694
+ for (const url of stack.flatMap((m) => m.images ?? [])) {
695
+ const name = url.split('/').pop();
696
+ if (!name || name.includes('..') || name.includes('/') || name.includes('\\'))
697
+ continue;
698
+ try {
699
+ const data = readFileSync(join(this.dataDir, 'runs', `${runId}-images`, name));
700
+ stackedImages.push({
701
+ type: 'image',
702
+ source: { type: 'base64', media_type: mediaTypeFor(name), data: data.toString('base64') },
703
+ });
787
704
  }
788
- return images;
789
- };
790
- // Keep the original in-memory blocks for a live process (including the
791
- // best-effort case where persistence failed). Recovery has no such copy,
792
- // so rebuild it from the durable task-image URLs.
793
- const images = input.images?.length ? input.images : readImages(run.taskImages ?? [], 'task');
794
- const stackedImages = readImages(stack.flatMap((m) => m.images ?? []), 'queued');
795
- return {
796
- ...input,
797
- task,
798
- ...(images.length ? { images } : { images: undefined }),
799
- ...(stackedImages.length ? { stackedImages } : { stackedImages: undefined }),
800
- };
705
+ catch {
706
+ // Degrade, never fail the boot (AGENTS.md): the user deleted `.ai/cezar/`
707
+ // or the file is unreadable start with the text and say which image went.
708
+ this.store.appendEvent(runId, {
709
+ type: 'note',
710
+ message: `queued attachment ${name} could not be read starting without it`,
711
+ });
712
+ }
713
+ }
714
+ return { ...input, task, ...(stackedImages.length ? { stackedImages } : {}) };
801
715
  }
802
716
  /**
803
717
  * Still waiting for a slot? Checked against the engine's own queue rather than
@@ -1108,7 +1022,7 @@ export class RunManager {
1108
1022
  const continuations = run.steps.filter((s) => s.id.startsWith('continue-')).length;
1109
1023
  const stepId = `continue-${continuations + 1}`;
1110
1024
  this.store.addStep(runId, { id: stepId, name: 'Continue', kind: 'agent' });
1111
- void this.runContinuation(runId, stepId, resume ? sessionStep.sessionId : undefined, targetRunner, opts.text?.trim() || 'Continue.', opts.images ?? []).catch((err) => {
1025
+ void this.runContinuation(runId, stepId, resume ? sessionStep.sessionId : undefined, targetRunner, opts.text?.trim() || 'Continue.').catch((err) => {
1112
1026
  const message = err instanceof Error ? err.message : String(err);
1113
1027
  this.store.updateRun(runId, {
1114
1028
  status: 'failed',
@@ -1116,14 +1030,11 @@ export class RunManager {
1116
1030
  finishedAt: new Date().toISOString(),
1117
1031
  });
1118
1032
  this.dropActive(runId);
1033
+ void this.pump();
1119
1034
  });
1120
1035
  return { ok: true };
1121
1036
  }
1122
- async runContinuation(runId, stepId, sessionId, backend, prompt,
1123
- /** Screenshots pasted into the follow-up composer — delivered with the
1124
- * reopened session's opening message, exactly like a live-session
1125
- * message's attachments. */
1126
- images = []) {
1037
+ async runContinuation(runId, stepId, sessionId, backend, prompt) {
1127
1038
  // Continuation runs in the task's worktree when it still exists (spec
1128
1039
  // 006) — the resumed session sees exactly what the original run left.
1129
1040
  // Retention (#483) may have reclaimed this run's worktree directory while
@@ -1154,6 +1065,7 @@ export class RunManager {
1154
1065
  });
1155
1066
  this.store.appendEvent(runId, { type: 'lifecycle', message: 'run cancelled' });
1156
1067
  this.dropActive(runId);
1068
+ void this.pump();
1157
1069
  return;
1158
1070
  }
1159
1071
  }
@@ -1175,22 +1087,7 @@ export class RunManager {
1175
1087
  backend,
1176
1088
  });
1177
1089
  this.store.appendEvent(runId, { type: 'step-start', stepId, name: 'Continue', kind: 'agent', iteration: 1 });
1178
- // Attachments pasted into the follow-up composer, on the same terms as a live-session
1179
- // message (#357): persisted to the run's own image store so the thread renders the bubble's
1180
- // images rather than a bare count, and handed to the agent BOTH as base64 blocks (so it can
1181
- // view them) and as absolute paths appended to the prompt (so it can operate on them — and
1182
- // because codex/opencode drop image blocks before they reach the model).
1183
- const attachments = images
1184
- .filter((b) => b.type === 'image')
1185
- .map((b) => this.persistImage(runId, b.source.media_type, b.source.data, 'pasted'))
1186
- .filter((saved) => saved !== null);
1187
- this.store.appendEvent(runId, {
1188
- type: 'user-message',
1189
- stepId,
1190
- text: prompt,
1191
- imageCount: images.filter((b) => b.type === 'image').length,
1192
- ...(attachments.length ? { images: attachments.map((saved) => saved.url) } : {}),
1193
- });
1090
+ this.store.appendEvent(runId, { type: 'user-message', stepId, text: prompt, imageCount: 0 });
1194
1091
  let stepCost = 0;
1195
1092
  let turnText = '';
1196
1093
  const sink = this.makeUiSink(runId, stepId);
@@ -1202,7 +1099,7 @@ export class RunManager {
1202
1099
  return;
1203
1100
  }
1204
1101
  if (event.type === 'text') {
1205
- turnText = appendTurnText(turnText, event.text);
1102
+ turnText += event.text;
1206
1103
  const text = stripAskMarker(stripTaskMarkers(stripMonitoringMarker(stripDoneMarker(event.text))));
1207
1104
  if (text)
1208
1105
  this.store.appendEvent(runId, { type: 'text', text, stepId });
@@ -1274,7 +1171,7 @@ export class RunManager {
1274
1171
  }
1275
1172
  this.waiting.add(runId);
1276
1173
  this.armIdleTimer(runId, state);
1277
- this.releaseSlot();
1174
+ void this.pump();
1278
1175
  }
1279
1176
  }
1280
1177
  appendHandoffHeartbeat(this.dataDir, runId, `turn complete — status=${monitoring ? 'monitoring' : sessionOpen ? 'waiting' : 'running'}`);
@@ -1318,6 +1215,7 @@ export class RunManager {
1318
1215
  message: `continue failed — ${err.message}`,
1319
1216
  });
1320
1217
  this.dropActive(runId);
1218
+ void this.pump();
1321
1219
  return;
1322
1220
  }
1323
1221
  const runner = createRunner(continueBackend);
@@ -1326,8 +1224,7 @@ export class RunManager {
1326
1224
  // run's extra system prompt (already resolved at execute time and
1327
1225
  // echoed on the record) rides along with the handoff contract.
1328
1226
  systemPrompt: composeSystemPrompt(record?.systemPrompt, generateFollowups ? HANDOFF_INSTRUCTIONS : HANDOFF_ONLY_INSTRUCTIONS),
1329
- userPrompt: attachments.length ? `${prompt}\n\n${pastedAttachmentsText(attachments)}` : prompt,
1330
- ...(images.length ? { images } : {}),
1227
+ userPrompt: prompt,
1331
1228
  cwd: state.cwd,
1332
1229
  allowedTools: DEFAULT_ALLOWED_TOOLS,
1333
1230
  additionalDirectories: [join(this.dataDir, 'runs')],
@@ -1381,6 +1278,7 @@ export class RunManager {
1381
1278
  if (state.cwd !== this.repoRoot)
1382
1279
  await autosaveCommit(state.cwd, 'turn end');
1383
1280
  this.dropActive(runId);
1281
+ void this.pump();
1384
1282
  }
1385
1283
  }
1386
1284
  // ---- execution -----------------------------------------------------------
@@ -1486,6 +1384,7 @@ export class RunManager {
1486
1384
  });
1487
1385
  emit({ type: 'lifecycle', message: `run failed — ${error}` });
1488
1386
  this.dropActive(runId);
1387
+ void this.pump();
1489
1388
  return;
1490
1389
  }
1491
1390
  }
@@ -1511,18 +1410,21 @@ export class RunManager {
1511
1410
  const retriesUsed = new Map();
1512
1411
  let checkFailure = null;
1513
1412
  let runError = null;
1514
- // `startRun` already persisted task images so a queued bubble can render them
1515
- // (#612). Reuse those files for the agent-facing path note instead of minting
1516
- // duplicate pasted files when execution finally begins.
1517
- let startAttachments = (this.store.getRun(runId)?.taskImages ?? [])
1518
- .map((url) => {
1519
- const name = url.split('/').pop();
1520
- if (!name || name.includes('..') || name.includes('/') || name.includes('\\'))
1521
- return null;
1522
- const path = join(this.dataDir, 'runs', `${runId}-images`, name);
1523
- return existsSync(path) ? { name, url, path } : null;
1524
- })
1525
- .filter((saved) => saved !== null);
1413
+ // Persist the task's attached images so the thread's initial bubble can render them
1414
+ // (#image-display); they still ride the first agent step's opening message below.
1415
+ // `pasted` prefix (#357) marks these as user attachments on disk and keeps their
1416
+ // absolute paths so runAgentStep can tell the agent where to find the real files.
1417
+ let startAttachments = [];
1418
+ if (input.images?.length) {
1419
+ const persisted = input.images
1420
+ .filter((b) => b.type === 'image')
1421
+ .map((b) => this.persistImage(runId, b.source.media_type, b.source.data, 'pasted'))
1422
+ .filter((saved) => saved !== null);
1423
+ if (persisted.length) {
1424
+ this.store.updateRun(runId, { taskImages: persisted.map((p) => p.url) });
1425
+ startAttachments = persisted;
1426
+ }
1427
+ }
1526
1428
  // Task screenshots go with the FIRST agent step's opening message only —
1527
1429
  // later steps and retry loops run in fresh sessions without them. Stacked
1528
1430
  // attachments (#472) ride along too, but are NOT re-persisted above: they
@@ -1620,6 +1522,7 @@ export class RunManager {
1620
1522
  }
1621
1523
  this.clearIdleTimer(state);
1622
1524
  this.dropActive(runId);
1525
+ void this.pump();
1623
1526
  }
1624
1527
  /** Returns an error message, or null on success. */
1625
1528
  async runAgentStep(runId, state, step, input, skills, checkFailure, interactive, emit, images, taskBackend, extraSystemPrompt,
@@ -1696,7 +1599,7 @@ export class RunManager {
1696
1599
  return;
1697
1600
  }
1698
1601
  if (event.type === 'text') {
1699
- turnText = appendTurnText(turnText, event.text);
1602
+ turnText += event.text;
1700
1603
  const text = stripAskMarker(stripTaskMarkers(stripMonitoringMarker(stripDoneMarker(event.text))));
1701
1604
  if (text)
1702
1605
  emit({ type: 'text', text, stepId: step.id });
@@ -1759,7 +1662,7 @@ export class RunManager {
1759
1662
  }
1760
1663
  this.waiting.add(runId);
1761
1664
  this.armIdleTimer(runId, state);
1762
- this.releaseSlot(); // the freed slot can start a queued run right away — in any project
1665
+ void this.pump(); // the freed slot can start a queued run right away
1763
1666
  }
1764
1667
  // Cez's own heartbeat — the handoff stays current even when the
1765
1668
  // agent forgets to write (spec 007).
@@ -1871,7 +1774,7 @@ export class RunManager {
1871
1774
  this.store.updateRun(runId, { status: 'waiting', activity: undefined });
1872
1775
  if (state.currentStepId)
1873
1776
  this.store.updateStep(runId, state.currentStepId, { status: 'waiting' });
1874
- this.releaseSlot();
1777
+ void this.pump();
1875
1778
  }
1876
1779
  /**
1877
1780
  * Turn-end bookkeeping (#389), shared by `runAgentStep` and
@@ -2218,30 +2121,14 @@ export function makeRunTitle(task, workflow) {
2218
2121
  const chars = [...(prefixed || '(untitled task)')];
2219
2122
  return chars.length > 80 ? `${chars.slice(0, 79).join('').trimEnd()}…` : chars.join('');
2220
2123
  }
2221
- /**
2222
- * Skill identity is context, while the Markdown body remains instructions.
2223
- *
2224
- * For an on-disk skill we also hand the agent the ABSOLUTE directory of the
2225
- * installed copy. A run executes in an isolated worktree that has no local
2226
- * `.agents/skills` (gitignored, absent in a fresh checkout), so without this
2227
- * the agent cannot read the skill's companion files (`references/*.md`) — or,
2228
- * worse, reads a stale copy materialized from the team-repo cache. The path
2229
- * resolves against the MAIN project root (`discoverSkills(repoRoot)`), i.e. the
2230
- * current `npx skills`-installed copy, so a worktree agent and the main
2231
- * checkout read the exact same, up-to-date files. Team skills are omitted here:
2232
- * they are materialized into the worktree separately (see the call site).
2233
- */
2124
+ /** Skill identity is context, while the Markdown body remains instructions. */
2234
2125
  export function skillSystemPrompt(skill) {
2235
- const lines = [
2126
+ return [
2236
2127
  `Selected skill: /${skill.name}`,
2237
2128
  ...(skill.description ? [`Description: ${skill.description}`] : []),
2238
- ];
2239
- if (skill.source && skill.source !== 'team' && skill.path) {
2240
- const dir = dirname(skill.path);
2241
- lines.push('', `Skill files are installed on disk at: ${dir}`, `Read any file this skill references (for example references/*.md) from that absolute directory. ` +
2242
- `It is the current installed copy — use it even though your working directory is a separate worktree that does not contain the skill.`);
2243
- }
2244
- lines.push('', 'Skill instructions:', skill.body.trim());
2245
- return lines.join('\n');
2129
+ '',
2130
+ 'Skill instructions:',
2131
+ skill.body.trim(),
2132
+ ].join('\n');
2246
2133
  }
2247
2134
  //# sourceMappingURL=run.js.map