@loicngr/kobo 1.11.36 → 1.11.37

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 (100) hide show
  1. package/AGENTS.md +15 -0
  2. package/CHANGELOG.md +4 -0
  3. package/dist/server/index.js +11 -24
  4. package/dist/server/routes/dev-server.js +2 -1
  5. package/dist/server/routes/pull-requests.js +5 -4
  6. package/dist/server/routes/workspaces.js +122 -91
  7. package/dist/server/services/agent/engines/codex/client.js +1 -0
  8. package/dist/server/services/agent/engines/codex/engine.js +47 -10
  9. package/dist/server/services/agent/engines/codex/event-mapper.js +5 -1
  10. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +21 -8
  11. package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +30 -3
  12. package/dist/server/services/agent/orchestrator.js +146 -68
  13. package/dist/server/services/agent/session-controller.js +12 -2
  14. package/dist/server/services/auto-loop-service.js +57 -3
  15. package/dist/server/services/change-source-branch-service.js +36 -4
  16. package/dist/server/services/cron-service.js +19 -0
  17. package/dist/server/services/db-backup-service.js +30 -0
  18. package/dist/server/services/dev-server-service.js +167 -38
  19. package/dist/server/services/image-service.js +25 -28
  20. package/dist/server/services/pr-checkout-service.js +34 -5
  21. package/dist/server/services/quota-backoff-service.js +25 -8
  22. package/dist/server/services/wakeup-service.js +23 -3
  23. package/dist/server/services/worktree-purge-service.js +3 -7
  24. package/dist/server/utils/agent-stop-result.js +12 -0
  25. package/dist/server/utils/git-ops.js +8 -1
  26. package/dist/server/utils/git-worktree-fingerprint.js +86 -0
  27. package/dist/server/utils/safe-path.js +21 -0
  28. package/dist/server/utils/script-runner.js +32 -9
  29. package/dist/server/utils/workspace-lifecycle-guard.js +27 -0
  30. package/package.json +1 -1
  31. package/src/client/dist/pwa/assets/{ActivityFeed-Bakgvay6.css → ActivityFeed-BDyFBoxd.css} +1 -1
  32. package/src/client/dist/pwa/assets/ActivityFeed-C_-hxIo8.js +8 -0
  33. package/src/client/dist/pwa/assets/{ChangelogPage-BFGYQ6eY.js → ChangelogPage-CBlGJ4zT.js} +1 -1
  34. package/src/client/dist/pwa/assets/{ComparisonPanel-C6DfjpZZ.js → ComparisonPanel-DZAE2X9j.js} +1 -1
  35. package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +1 -0
  36. package/src/client/dist/pwa/assets/CreatePage-ArmtcF4d.js +2 -0
  37. package/src/client/dist/pwa/assets/{DashboardPage-DsVRjzzy.js → DashboardPage-eQTCXcdz.js} +1 -1
  38. package/src/client/dist/pwa/assets/{DiffViewer-DSlSAIiW.js → DiffViewer-CaT7vuSr.js} +3 -3
  39. package/src/client/dist/pwa/assets/{DocumentsPanel-C1eXTw_Q.js → DocumentsPanel-A0isCTaI.js} +1 -1
  40. package/src/client/dist/pwa/assets/{DrawerToggleButton-BxYpUb4y.js → DrawerToggleButton-T70fh8dp.js} +1 -1
  41. package/src/client/dist/pwa/assets/{GitPanel-DnBWSdVp.js → GitPanel-CAvk0G7h.js} +3 -3
  42. package/src/client/dist/pwa/assets/{HealthPage-rX1TiaP9.js → HealthPage-D8Mr0V6w.js} +1 -1
  43. package/src/client/dist/pwa/assets/{MainLayout-Ceuh5uJj.js → MainLayout-x6Es4Da-.js} +2 -2
  44. package/src/client/dist/pwa/assets/{QBtnDropdown-K7syiyyH.js → QBtnDropdown-CQqzzlFI.js} +1 -1
  45. package/src/client/dist/pwa/assets/{QDrawer-DapzE8DH.js → QDrawer-Cwj2qh0r.js} +1 -1
  46. package/src/client/dist/pwa/assets/{QSelect-Bodml89k.js → QSelect-BSPtuXJg.js} +1 -1
  47. package/src/client/dist/pwa/assets/{QTooltip-K-9OdDfq.js → QTooltip-DC_kDKku.js} +1 -1
  48. package/src/client/dist/pwa/assets/{SchedulePanel-CfumTzAa.js → SchedulePanel-9aJSoz6Y.js} +1 -1
  49. package/src/client/dist/pwa/assets/{SearchPage-AhZYS-dS.js → SearchPage-BjNvtE_U.js} +1 -1
  50. package/src/client/dist/pwa/assets/{SettingsPage-BoHXWvKn.js → SettingsPage-CvLnT73V.js} +1 -1
  51. package/src/client/dist/pwa/assets/{SplitWorkspacePage-3wK11mcu.js → SplitWorkspacePage-DTao0DHr.js} +1 -1
  52. package/src/client/dist/pwa/assets/{TerminalPanel-CKg0BeNK.js → TerminalPanel-BrKIW-bJ.js} +1 -1
  53. package/src/client/dist/pwa/assets/{TouchPan-D6ctyPYf.js → TouchPan-_n1AQgoo.js} +1 -1
  54. package/src/client/dist/pwa/assets/{TourReplayButton-Dn4Dy_kH.js → TourReplayButton-CJF3nVvn.js} +1 -1
  55. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-DnoabsfK.js → WorkspaceAttentionLabels-CxesUiIm.js} +1 -1
  56. package/src/client/dist/pwa/assets/{WorkspacePage-DYQrfeYw.css → WorkspacePage-DoXtwpjV.css} +1 -1
  57. package/src/client/dist/pwa/assets/WorkspacePage-MxBE0mr3.js +4 -0
  58. package/src/client/dist/pwa/assets/{browser-DHpSUK8-.js → browser-DGWpkvN3.js} +1 -1
  59. package/src/client/dist/pwa/assets/{build-path-tree-DtEjsZlh.js → build-path-tree-Ba1dSLgi.js} +1 -1
  60. package/src/client/dist/pwa/assets/{de-Dt_m-H83.js → de-DVK21RB6.js} +1 -1
  61. package/src/client/dist/pwa/assets/{documents-B2dg1wRf.js → documents-BMHzjW_g.js} +1 -1
  62. package/src/client/dist/pwa/assets/{editor.api-Cd1D35oj.js → editor.api-CYTuNwqu.js} +1 -1
  63. package/src/client/dist/pwa/assets/{efforts-IB1vPjJQ.js → efforts-BPlRSHXp.js} +1 -1
  64. package/src/client/dist/pwa/assets/{engineFeatures-DYSyDEJe.js → engineFeatures-DVB2rklO.js} +1 -1
  65. package/src/client/dist/pwa/assets/{es-DFg25DCS.js → es-C8cQwDZM.js} +1 -1
  66. package/src/client/dist/pwa/assets/{expand-template-CNAHiesL.js → expand-template-Cj82YE-O.js} +1 -1
  67. package/src/client/dist/pwa/assets/{format-DidwgbbX.js → format-Dk7O3LhA.js} +1 -1
  68. package/src/client/dist/pwa/assets/{formatters-BjzyUHlu.js → formatters-TGjvs7Ca.js} +1 -1
  69. package/src/client/dist/pwa/assets/{fr-CQ1slpol.js → fr-BVOMZ2Bl.js} +1 -1
  70. package/src/client/dist/pwa/assets/{freemarker2-jNGWawVw.js → freemarker2-39lO_dWH.js} +1 -1
  71. package/src/client/dist/pwa/assets/{handlebars-DfSqClrW.js → handlebars-M_0V82cD.js} +1 -1
  72. package/src/client/dist/pwa/assets/{html-ByLX-n8T.js → html-yILqYDsl.js} +1 -1
  73. package/src/client/dist/pwa/assets/i18n-2UeMfd5m.js +1 -0
  74. package/src/client/dist/pwa/assets/{index-DcwcFDrZ.js → index-BJU5dnkF.js} +2 -2
  75. package/src/client/dist/pwa/assets/{it-w09rfaP7.js → it-CxkAoOj4.js} +1 -1
  76. package/src/client/dist/pwa/assets/{javascript-Dja1fHfv.js → javascript-DrW-amDc.js} +1 -1
  77. package/src/client/dist/pwa/assets/{liquid-D315hW6C.js → liquid-D_mXmUoL.js} +1 -1
  78. package/src/client/dist/pwa/assets/{mdx-B5yUwpzv.js → mdx-E8O3lvR5.js} +1 -1
  79. package/src/client/dist/pwa/assets/{monaco-contributions-NfxrU0r3.js → monaco-contributions-C2K3sl_V.js} +1 -1
  80. package/src/client/dist/pwa/assets/{monaco.contribution-D1hXPkDZ.js → monaco.contribution-DLhHQIJn.js} +2 -2
  81. package/src/client/dist/pwa/assets/{network-auth-CARVtbNe.js → network-auth-BUwWtnui.js} +1 -1
  82. package/src/client/dist/pwa/assets/{notify-theme-CKC3-EAZ.js → notify-theme-1mOpY2vN.js} +1 -1
  83. package/src/client/dist/pwa/assets/{pending-diff-open-BlLrQaW7.js → pending-diff-open-BL7dKRqF.js} +1 -1
  84. package/src/client/dist/pwa/assets/{permissionModes-DMzFEQhr.js → permissionModes-BtC3_P-P.js} +1 -1
  85. package/src/client/dist/pwa/assets/{project-color-CFjdIj6W.js → project-color-BFBFZWeG.js} +1 -1
  86. package/src/client/dist/pwa/assets/{python-Dc6mnEip.js → python-DoZAYim6.js} +1 -1
  87. package/src/client/dist/pwa/assets/{razor-DArVFBGO.js → razor-9bZ30g2y.js} +1 -1
  88. package/src/client/dist/pwa/assets/{render-chat-markdown-BTPAjU2g.js → render-chat-markdown-7EtWNjG0.js} +1 -1
  89. package/src/client/dist/pwa/assets/{typescript-COE1vIyB.js → typescript-C1CBfL3L.js} +1 -1
  90. package/src/client/dist/pwa/assets/{use-tours-C8x2V1w5.js → use-tours-BJ81clv6.js} +1 -1
  91. package/src/client/dist/pwa/assets/{workspace-templates-DHyA_GEW.js → workspace-templates-r1-bipjM.js} +1 -1
  92. package/src/client/dist/pwa/assets/{xml-eYjr4cGR.js → xml-COuu5isK.js} +1 -1
  93. package/src/client/dist/pwa/assets/{yaml-UooOcssF.js → yaml-ChZ7WqaJ.js} +1 -1
  94. package/src/client/dist/pwa/index.html +1 -1
  95. package/src/client/dist/pwa/sw.js +1 -1
  96. package/src/client/dist/pwa/assets/ActivityFeed-BPx-xhFY.js +0 -8
  97. package/src/client/dist/pwa/assets/CreatePage-DxOyY65u.css +0 -1
  98. package/src/client/dist/pwa/assets/CreatePage-ypEzkAKv.js +0 -2
  99. package/src/client/dist/pwa/assets/WorkspacePage-BI7CW2yT.js +0 -4
  100. package/src/client/dist/pwa/assets/i18n-CnjXQjxy.js +0 -1
@@ -5,7 +5,10 @@ import path from 'node:path';
5
5
  import * as gitOps from '../utils/git-ops.js';
6
6
  import { GitConflictError } from '../utils/git-ops.js';
7
7
  import { withGitRepoLock } from '../utils/git-repo-lock.js';
8
+ import { fingerprintWorktree } from '../utils/git-worktree-fingerprint.js';
9
+ import { withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
8
10
  import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
11
+ import { hasController } from './agent/orchestrator.js';
9
12
  import { createWorktree, listWorktrees, removeWorktreeUnlocked } from './worktree-service.js';
10
13
  /**
11
14
  * A `pathCollision.worktreePath` decision is untrusted input reaching
@@ -114,8 +117,10 @@ function diagnoseChanges(worktreePath) {
114
117
  /** Classify the workspace, if any, that already tracks this branch.
115
118
  * A purged worktree outranks the archived flag: purging always archives, and
116
119
  * "recreate the worktree" is the useful offer, not "unarchive". */
117
- export function resolveWorkspaceState(workspaces, branch) {
118
- const match = workspaces.find((w) => w.workingBranch === branch);
120
+ export function resolveWorkspaceState(workspaces, branch, projectPath) {
121
+ const match = workspaces.find((w) => w.workingBranch === branch &&
122
+ typeof w.projectPath === 'string' &&
123
+ path.resolve(w.projectPath) === path.resolve(projectPath));
119
124
  if (!match)
120
125
  return { state: 'none' };
121
126
  if (match.worktreePurgedAt)
@@ -134,10 +139,13 @@ function safeRevParse(repoPath, ref) {
134
139
  }
135
140
  }
136
141
  /** Hash the observed state so `/resolve` can refuse a plan built on stale facts. */
137
- export function computeFingerprint(report) {
142
+ export async function computeFingerprint(report) {
138
143
  const localHead = safeRevParse(report.projectPath, report.headBranch);
139
144
  const remoteHead = safeRevParse(report.projectPath, `origin/${report.headBranch}`);
140
145
  const payload = JSON.stringify({
146
+ contents: report.worktree.state === 'orphan' || report.worktree.state === 'attached'
147
+ ? await fingerprintWorktree(report.worktree.path)
148
+ : null,
141
149
  localHead,
142
150
  remoteHead,
143
151
  worktree: report.worktree,
@@ -174,7 +182,7 @@ export function diagnoseLocalState(projectPath, headBranch, worktreesPath, attac
174
182
  headBranch,
175
183
  targetWorktreePath,
176
184
  blockers,
177
- workspace: resolveWorkspaceState(workspaces, headBranch),
185
+ workspace: resolveWorkspaceState(workspaces, headBranch, projectPath),
178
186
  worktree,
179
187
  localChanges: inspectPath ? diagnoseChanges(inspectPath) : EMPTY_CHANGES,
180
188
  ongoingOperation: inspectPath ? gitOps.getOngoingGitOperation(inspectPath) : null,
@@ -249,14 +257,35 @@ export class StaleDiagnosisError extends Error {
249
257
  * or a source-branch change.
250
258
  */
251
259
  export function resolvePrCheckout(input) {
260
+ const initial = diagnoseLocalState(input.projectPath, input.headBranch, input.worktreesPath, input.attachedPaths, input.workspaces);
261
+ const owner = initial.worktree.state === 'attached'
262
+ ? initial.worktree.workspaceId
263
+ : initial.workspace.state !== 'none'
264
+ ? initial.workspace.id
265
+ : null;
266
+ const resolve = () => resolvePrCheckoutLocked(input, owner);
267
+ return owner ? withWorkspaceLifecycleGuard(owner, resolve) : resolve();
268
+ }
269
+ function resolvePrCheckoutLocked(input, owner) {
252
270
  return withGitRepoLock(input.projectPath, async () => {
271
+ if (owner && hasController(owner))
272
+ throw new Error('Stop the workspace agent before changing its checkout');
253
273
  const applied = [];
254
274
  // 1. Refresh, then refuse a plan built on facts that no longer hold.
255
275
  gitOps.fetchSourceBranch(input.projectPath, input.headBranch);
256
276
  applied.push({ kind: 'fetch', detail: `origin/${input.headBranch}` });
257
277
  const fresh = diagnoseLocalState(input.projectPath, input.headBranch, input.worktreesPath, input.attachedPaths ?? new Map(), input.workspaces ?? []);
258
- if (computeFingerprint(fresh) !== input.fingerprint)
278
+ if ((await computeFingerprint(fresh)) !== input.fingerprint)
259
279
  throw new StaleDiagnosisError(fresh);
280
+ if ((fresh.worktree.state === 'orphan' || fresh.worktree.state === 'attached') &&
281
+ input.decisions.orphanWorktree === 'create-elsewhere') {
282
+ throw new Error('Branch is already checked out. Attach its existing worktree instead.');
283
+ }
284
+ if (fresh.localChanges.present &&
285
+ (input.decisions.localChanges ?? 'keep') === 'keep' &&
286
+ input.decisions.divergence === 'reset-hard') {
287
+ throw new Error('Cannot preserve local changes during a hard reset. Stash or commit them first, or keep the branch.');
288
+ }
260
289
  // 2. Clear stale worktree metadata before anything needs the path.
261
290
  if (fresh.worktree.state === 'stale-metadata') {
262
291
  git(input.projectPath, ['worktree', 'prune']);
@@ -2,7 +2,15 @@ import { getDb } from '../db/index.js';
2
2
  import { emitEphemeral } from './websocket-service.js';
3
3
  import { getWorkspace } from './workspace-service.js';
4
4
  const timers = new Map();
5
+ let suspended = false;
5
6
  let onFireCallback = null;
7
+ /** Stop in-memory delivery without consuming the schedules needed by the next boot. */
8
+ export function suspendForShutdown() {
9
+ suspended = true;
10
+ for (const timer of timers.values())
11
+ clearTimeout(timer);
12
+ timers.clear();
13
+ }
6
14
  function rowToPending(row) {
7
15
  return {
8
16
  workspaceId: row.workspace_id,
@@ -26,10 +34,9 @@ export function arm(workspaceId, delayMs, meta) {
26
34
  const db = getDb();
27
35
  const now = new Date();
28
36
  const targetAt = new Date(now.getTime() + delayMs).toISOString();
29
- const existing = db
30
- .prepare('SELECT retry_count FROM pending_quota_backoffs WHERE workspace_id = ?')
31
- .get(workspaceId);
32
- const retryCount = (existing?.retry_count ?? 0) + 1;
37
+ const retryCount = meta.retryCount;
38
+ if (!Number.isSafeInteger(retryCount) || retryCount < 1)
39
+ throw new Error('retryCount must be a positive integer');
33
40
  db.prepare(`INSERT INTO pending_quota_backoffs (workspace_id, target_at, resets_at, source, reason, retry_count, created_at)
34
41
  VALUES (?, ?, ?, ?, ?, ?, ?)
35
42
  ON CONFLICT(workspace_id) DO UPDATE SET
@@ -42,9 +49,12 @@ export function arm(workspaceId, delayMs, meta) {
42
49
  const previous = timers.get(workspaceId);
43
50
  if (previous)
44
51
  clearTimeout(previous);
45
- const timer = setTimeout(() => fireOrSkip(workspaceId), Math.max(0, delayMs));
46
- timer.unref?.();
47
- timers.set(workspaceId, timer);
52
+ timers.delete(workspaceId);
53
+ if (!suspended) {
54
+ const timer = setTimeout(() => fireOrSkip(workspaceId), Math.max(0, delayMs));
55
+ timer.unref?.();
56
+ timers.set(workspaceId, timer);
57
+ }
48
58
  emitEphemeral(workspaceId, 'agent:quota-backoff', {
49
59
  targetAt,
50
60
  resetsAt: meta.resetsAt,
@@ -90,6 +100,8 @@ export function setOnFireCallback(fn) {
90
100
  * archived or missing workspaces are deleted without firing.
91
101
  */
92
102
  export function restoreOnBoot(onFire) {
103
+ suspendForShutdown();
104
+ suspended = false;
93
105
  setOnFireCallback(onFire);
94
106
  const db = getDb();
95
107
  const rows = db.prepare('SELECT * FROM pending_quota_backoffs').all();
@@ -107,7 +119,12 @@ export function restoreOnBoot(onFire) {
107
119
  }
108
120
  /** Internal — invoked when a timer fires. */
109
121
  function fireOrSkip(workspaceId) {
122
+ if (suspended)
123
+ return;
110
124
  timers.delete(workspaceId);
125
+ const pending = getPending(workspaceId);
126
+ if (!pending)
127
+ return;
111
128
  // Final archive check before firing — workspace might have been archived
112
129
  // between the timer being armed and now.
113
130
  const ws = getWorkspace(workspaceId);
@@ -124,7 +141,7 @@ function fireOrSkip(workspaceId) {
124
141
  const cb = onFireCallback;
125
142
  if (!cb)
126
143
  return;
127
- cb(workspaceId);
144
+ cb(workspaceId, pending);
128
145
  }
129
146
  /** @internal test-only */
130
147
  export const _timers = timers;
@@ -1,5 +1,6 @@
1
1
  import { getDb } from '../db/index.js';
2
2
  import { slugifyProjectName } from '../utils/project-slug.js';
3
+ import { isWorkspaceLifecycleBusy } from '../utils/workspace-lifecycle-guard.js';
3
4
  import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
4
5
  import * as orchestrator from './agent/orchestrator.js';
5
6
  import * as settingsService from './settings-service.js';
@@ -12,6 +13,14 @@ const AUTONOMOUS_LOOP_SENTINEL = '<<autonomous-loop-dynamic>>';
12
13
  const AUTONOMOUS_LOOP_FALLBACK_PROMPT = 'Continue where you left off.';
13
14
  /** In-memory timers — cleared on cancel/fire; rebuilt on boot via rehydrate. */
14
15
  const timers = new Map();
16
+ let suspended = false;
17
+ /** Preserve persisted wakeups while preventing delivery during teardown. */
18
+ export function suspendForShutdown() {
19
+ suspended = true;
20
+ for (const timer of timers.values())
21
+ clearTimeout(timer);
22
+ timers.clear();
23
+ }
15
24
  function clamp(n, lo, hi) {
16
25
  return Math.max(lo, Math.min(hi, n));
17
26
  }
@@ -54,9 +63,12 @@ export function schedule(workspaceId, delaySeconds, prompt, reason, agentSession
54
63
  db.prepare(`INSERT OR REPLACE INTO pending_wakeups
55
64
  (workspace_id, target_at, prompt, reason, created_at, agent_session_id)
56
65
  VALUES (?, ?, ?, ?, ?, ?)`).run(workspaceId, targetAtIso, effectivePrompt, reason ?? null, new Date().toISOString(), agentSessionId ?? null);
57
- const timeout = setTimeout(() => fire(workspaceId), clampedSeconds * 1000);
58
- timeout.unref?.();
59
- timers.set(workspaceId, timeout);
66
+ timers.delete(workspaceId);
67
+ if (!suspended) {
68
+ const timeout = setTimeout(() => fire(workspaceId), clampedSeconds * 1000);
69
+ timeout.unref?.();
70
+ timers.set(workspaceId, timeout);
71
+ }
60
72
  emitEphemeral(workspaceId, 'wakeup:scheduled', { targetAt: targetAtIso, reason });
61
73
  }
62
74
  catch (err) {
@@ -105,6 +117,8 @@ export function isWakeupScheduled(workspaceId) {
105
117
  }
106
118
  /** Re-register timers for rows persisted across restart. Skips stale entries. */
107
119
  export function rehydrate() {
120
+ suspendForShutdown();
121
+ suspended = false;
108
122
  try {
109
123
  const db = getDb();
110
124
  const rows = db.prepare('SELECT * FROM pending_wakeups').all();
@@ -139,12 +153,18 @@ export function rehydrate() {
139
153
  }
140
154
  /** Internal — invoked by setTimeout. */
141
155
  function fire(workspaceId) {
156
+ if (suspended)
157
+ return;
142
158
  try {
143
159
  const db = getDb();
144
160
  const row = db.prepare('SELECT * FROM pending_wakeups WHERE workspace_id = ?').get(workspaceId);
145
161
  timers.delete(workspaceId);
146
162
  if (!row)
147
163
  return;
164
+ if (isWorkspaceLifecycleBusy(workspaceId)) {
165
+ defer(workspaceId, row);
166
+ return;
167
+ }
148
168
  if (orchestrator.hasController(workspaceId)) {
149
169
  // Claude can keep the stream open after a result while a background
150
170
  // task runs. Waiting for that controller to disappear deadlocks a
@@ -1,6 +1,7 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import { promisify } from 'node:util';
4
+ import { assertAgentStopped } from '../utils/agent-stop-result.js';
4
5
  import { logError, logWarn } from '../utils/logger.js';
5
6
  import { withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
6
7
  import * as agentManager from './agent/orchestrator.js';
@@ -29,19 +30,14 @@ async function purgeWorktreeGuarded(workspaceId, expectedArchivedAt) {
29
30
  const warnings = [];
30
31
  // `removeWorktree` runs later in this same function — the agent must be dead
31
32
  // by then, not merely asked to stop.
32
- try {
33
- await agentManager.stopAgentAndWait(workspaceId, undefined, 'purge');
34
- }
35
- catch (err) {
36
- const msg = err instanceof Error ? err.message : String(err);
37
- logError('purge', `stopAgent failed for '${workspace.name}'`, { workspaceId: workspace.id, error: msg });
38
- }
33
+ assertAgentStopped(await agentManager.stopAgentAndWait(workspaceId, undefined, 'purge'));
39
34
  try {
40
35
  await devServerService.stopDevServer(workspaceId);
41
36
  }
42
37
  catch (err) {
43
38
  const msg = err instanceof Error ? err.message : String(err);
44
39
  logError('purge', `stopDevServer failed for '${workspace.name}'`, { workspaceId: workspace.id, error: msg });
40
+ throw err;
45
41
  }
46
42
  try {
47
43
  destroyTerminal(workspaceId);
@@ -0,0 +1,12 @@
1
+ export class AgentStopError extends Error {
2
+ outcome;
3
+ code = 'agent-stop-incomplete';
4
+ constructor(outcome) {
5
+ super(`Agent stop is not confirmed (${outcome}). Wait for it to stop and retry.`);
6
+ this.outcome = outcome;
7
+ }
8
+ }
9
+ export function assertAgentStopped(outcome) {
10
+ if (outcome !== 'stopped' && outcome !== 'not-running')
11
+ throw new AgentStopError(outcome);
12
+ }
@@ -669,7 +669,13 @@ export function listProperCommits(repoPath, workingBranch, newBase, oldBase) {
669
669
  * `repoPath`. The caller (and the Kōbō worktree orchestrator) must ensure
670
670
  * `workingBranch` is the active branch — do NOT add a `git checkout` here.
671
671
  */
672
+ export function assertCurrentBranch(repoPath, expected) {
673
+ const actual = getCurrentBranch(repoPath);
674
+ if (actual !== expected)
675
+ throw new Error(`Expected checkout '${expected}', found '${actual || 'detached HEAD'}'`);
676
+ }
672
677
  export function reconstructBranchOnto(repoPath, workingBranch, newBase, commits) {
678
+ assertCurrentBranch(repoPath, workingBranch);
673
679
  const baseRef = resolveBase(repoPath, newBase);
674
680
  const backupBranch = `kobo-backup/${workingBranch}-${Date.now()}`;
675
681
  git(repoPath, ['branch', backupBranch, workingBranch]);
@@ -749,8 +755,9 @@ export function pruneBackupBranches(repoPath, workingBranch, keep = 3) {
749
755
  }
750
756
  /** Abort any in-progress operation, then hard-reset `workingBranch` to a backup branch. */
751
757
  export function restoreBranchFromBackup(repoPath, workingBranch, backupBranch) {
758
+ assertCurrentBranch(repoPath, workingBranch);
752
759
  abortOngoingGitOperation(repoPath);
753
- git(repoPath, ['checkout', '-q', workingBranch]);
760
+ assertCurrentBranch(repoPath, workingBranch);
754
761
  git(repoPath, ['reset', '--hard', backupBranch]);
755
762
  }
756
763
  /** Return structured diff shortstat between two refs (three-dot merge base). */
@@ -0,0 +1,86 @@
1
+ import { execFileSync, spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { constants } from 'node:fs';
4
+ import { lstat, open, readlink, realpath } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ /** Hash diffs and untracked contents without loading large files into memory. */
7
+ export async function fingerprintWorktree(cwd) {
8
+ return fingerprintRepository(await realpath(cwd), new Set());
9
+ }
10
+ /** Nested repositories contribute HEAD and their own Git-filtered working state, never a .git traversal. */
11
+ async function fingerprintRepository(cwd, visited) {
12
+ if (visited.has(cwd))
13
+ throw new Error('Cannot fingerprint a recursive repository path');
14
+ visited.add(cwd);
15
+ const gitOptions = { cwd, encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024 };
16
+ const topLevel = execFileSync('git', ['rev-parse', '--show-toplevel'], gitOptions).trim();
17
+ if ((await realpath(topLevel)) !== cwd)
18
+ throw new Error('Working tree changed during fingerprinting; retry diagnosis');
19
+ const hash = createHash('sha256');
20
+ let head;
21
+ try {
22
+ head = execFileSync('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], gitOptions).trim();
23
+ }
24
+ catch (err) {
25
+ // A freshly initialized nested repository has no commit yet. Other Git failures remain fatal.
26
+ if (err.status !== 1)
27
+ throw err;
28
+ head = 'unborn';
29
+ }
30
+ hash.update(`HEAD\0${head}\0`);
31
+ for (const staged of [false, true]) {
32
+ hash.update(staged ? 'index\0' : 'working-tree\0');
33
+ await new Promise((resolve, reject) => {
34
+ const child = spawn('git', ['diff', '--no-ext-diff', '--no-textconv', '--binary', '--no-color', ...(staged ? ['--cached'] : []), '--'], { cwd, stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000 });
35
+ child.stdout.on('data', (chunk) => hash.update(chunk));
36
+ child.stderr.resume();
37
+ child.on('error', reject);
38
+ child.on('close', (code) => (code === 0 ? resolve() : reject(new Error('Cannot fingerprint working tree diff'))));
39
+ });
40
+ }
41
+ const names = execFileSync('git', ['ls-files', '--others', '--exclude-standard', '-z'], {
42
+ cwd,
43
+ encoding: 'utf8',
44
+ timeout: 30_000,
45
+ maxBuffer: 16 * 1024 * 1024,
46
+ })
47
+ .split('\0')
48
+ .filter(Boolean)
49
+ .sort();
50
+ for (const name of names) {
51
+ hash.update(`\0untracked\0${name}\0`);
52
+ // Remove Git's trailing slash for nested repos before lstat, so a link is never dereferenced.
53
+ const filePath = path.resolve(cwd, name);
54
+ const stat = await lstat(filePath);
55
+ hash.update(`${stat.mode}\0`);
56
+ if (stat.isSymbolicLink()) {
57
+ hash.update(await readlink(filePath));
58
+ }
59
+ else if (stat.isFile()) {
60
+ const file = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
61
+ try {
62
+ const before = await file.stat();
63
+ for await (const chunk of file.createReadStream({ autoClose: false }))
64
+ hash.update(chunk);
65
+ const after = await file.stat();
66
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
67
+ throw new Error('Working tree changed during fingerprinting; retry diagnosis');
68
+ }
69
+ finally {
70
+ await file.close();
71
+ }
72
+ }
73
+ else if (stat.isDirectory()) {
74
+ // `ls-files --others` reports an embedded repository as a directory instead of its files.
75
+ const nestedPath = await realpath(filePath);
76
+ const relative = path.relative(cwd, nestedPath);
77
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
78
+ throw new Error('Nested repository escapes the working tree');
79
+ hash.update(`\0repository\0${await fingerprintRepository(nestedPath, visited)}\0`);
80
+ }
81
+ else {
82
+ throw new Error('Cannot fingerprint a non-regular untracked file');
83
+ }
84
+ }
85
+ return hash.digest('hex');
86
+ }
@@ -66,3 +66,24 @@ function assertLeafIsNotSymlink(candidate) {
66
66
  throw err;
67
67
  }
68
68
  }
69
+ /** Create storage directories without accepting symlinked path components. */
70
+ export function ensureDirectoryInside(rootPath, relativePath) {
71
+ assertPathInside(rootPath, relativePath);
72
+ const root = fs.realpathSync(rootPath);
73
+ let current = root;
74
+ for (const segment of relativePath.split(path.sep).filter(Boolean)) {
75
+ current = path.join(current, segment);
76
+ try {
77
+ fs.mkdirSync(current);
78
+ }
79
+ catch (err) {
80
+ if (err.code !== 'EEXIST')
81
+ throw err;
82
+ }
83
+ const stat = fs.lstatSync(current);
84
+ if (stat.isSymbolicLink() || !stat.isDirectory())
85
+ throw new Error('Symbolic links are not allowed in storage directories');
86
+ current = resolveExistingPathInside(root, current);
87
+ }
88
+ return current;
89
+ }
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import * as wsService from '../services/websocket-service.js';
5
5
  import { getIndexLockPath } from './git-ops.js';
6
+ import { ensureDirectoryInside } from './safe-path.js';
6
7
  /** Default wall-clock budget for a user script before it is force-killed. */
7
8
  export const SCRIPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
8
9
  /**
@@ -13,10 +14,31 @@ export const SCRIPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
13
14
  export function runScript(opts) {
14
15
  const { workspaceId, worktreePath, script, eventPrefix, tmpFileName, env, extraEnv } = opts;
15
16
  const timeoutMs = opts.timeoutMs ?? SCRIPT_TIMEOUT_MS;
17
+ let scriptPath;
18
+ let scriptDirectory;
19
+ const cleanup = () => {
20
+ if (scriptPath) {
21
+ try {
22
+ fs.unlinkSync(scriptPath);
23
+ }
24
+ catch {
25
+ /* best effort */
26
+ }
27
+ }
28
+ if (scriptDirectory) {
29
+ try {
30
+ fs.rmdirSync(scriptDirectory);
31
+ }
32
+ catch {
33
+ /* best effort */
34
+ }
35
+ }
36
+ };
16
37
  return new Promise((resolve) => {
17
- const scriptPath = path.join(worktreePath, '.ai', tmpFileName);
18
- fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
19
- fs.writeFileSync(scriptPath, script, { mode: 0o755 });
38
+ const storage = ensureDirectoryInside(worktreePath, '.ai');
39
+ scriptDirectory = fs.mkdtempSync(path.join(storage, '.script-'));
40
+ scriptPath = path.join(scriptDirectory, path.basename(tmpFileName));
41
+ fs.writeFileSync(scriptPath, script, { mode: 0o700, flag: 'wx' });
20
42
  const proc = spawn('bash', [scriptPath], {
21
43
  cwd: worktreePath,
22
44
  env: {
@@ -93,12 +115,7 @@ export function runScript(opts) {
93
115
  return;
94
116
  settled = true;
95
117
  clearTimeout(timeout);
96
- try {
97
- fs.unlinkSync(scriptPath);
98
- }
99
- catch {
100
- /* best-effort */
101
- }
118
+ cleanup();
102
119
  if (exitCode === 0) {
103
120
  wsService.emitEphemeral(workspaceId, `${eventPrefix}:complete`, { hadOutput: outputEmitted });
104
121
  }
@@ -119,5 +136,11 @@ export function runScript(opts) {
119
136
  proc.on('close', (code) => {
120
137
  finish(code ?? 1);
121
138
  });
139
+ }).catch((err) => {
140
+ cleanup();
141
+ const message = err instanceof Error ? err.message : String(err);
142
+ wsService.emit(workspaceId, `${eventPrefix}:output`, { text: `[kobo] Script failed to start: ${message}` });
143
+ wsService.emitEphemeral(workspaceId, `${eventPrefix}:error`, { exitCode: 1, message });
144
+ return { exitCode: 1 };
122
145
  });
123
146
  }
@@ -1,4 +1,5 @@
1
1
  const busyWorkspaces = new Set();
2
+ const deferred = new Map();
2
3
  export class WorkspaceLifecycleBusyError extends Error {
3
4
  code = 'workspace-busy';
4
5
  constructor() {
@@ -8,6 +9,19 @@ export class WorkspaceLifecycleBusyError extends Error {
8
9
  export function isWorkspaceLifecycleBusy(id) {
9
10
  return busyWorkspaces.has(id);
10
11
  }
12
+ export function assertWorkspaceLifecycleAvailable(id) {
13
+ if (busyWorkspaces.has(id))
14
+ throw new WorkspaceLifecycleBusyError();
15
+ }
16
+ /** Keep automatic work pending without polling or acquiring the guard recursively. */
17
+ export function deferUntilWorkspaceAvailable(id, retry) {
18
+ if (!busyWorkspaces.has(id))
19
+ return false;
20
+ const callbacks = deferred.get(id) ?? new Set();
21
+ callbacks.add(retry);
22
+ deferred.set(id, callbacks);
23
+ return true;
24
+ }
11
25
  /** Exclude competing restore/purge/delete operations, including their awaits. */
12
26
  export async function withWorkspaceLifecycleGuard(id, action) {
13
27
  if (busyWorkspaces.has(id))
@@ -18,5 +32,18 @@ export async function withWorkspaceLifecycleGuard(id, action) {
18
32
  }
19
33
  finally {
20
34
  busyWorkspaces.delete(id);
35
+ const callbacks = deferred.get(id);
36
+ deferred.delete(id);
37
+ if (callbacks)
38
+ queueMicrotask(() => {
39
+ for (const retry of callbacks) {
40
+ try {
41
+ retry();
42
+ }
43
+ catch (err) {
44
+ console.error('[workspace-lifecycle] deferred operation failed:', err);
45
+ }
46
+ }
47
+ });
21
48
  }
22
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.11.36",
3
+ "version": "1.11.37",
4
4
  "description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -1 +1 @@
1
- .agent-error-item[data-v-e98c3c02]{gap:var(--kobo-space-sm);padding:var(--kobo-space-sm) var(--kobo-space-md);border-left:2px solid var(--kobo-danger);background:var(--kobo-surface-2);border-radius:var(--kobo-radius-sm);align-items:flex-start;display:flex}.agent-error-item__icon[data-v-e98c3c02]{color:var(--kobo-danger);flex:none;margin-top:2px}.agent-error-item__body[data-v-e98c3c02]{min-width:0}.agent-error-item__title[data-v-e98c3c02]{color:var(--kobo-text);font-weight:600}.agent-error-item__message[data-v-e98c3c02]{color:var(--kobo-text-2);font-family:var(--kobo-font-mono);white-space:pre-wrap;word-break:break-word;font-size:12px}.markdown-message[data-v-99371035]{color:var(--kobo-text-2);word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-99371035] *{max-width:100%}.markdown-message[data-v-99371035] p{margin:0 0 .5em}.markdown-message[data-v-99371035] p:last-child{margin-bottom:0}.markdown-message[data-v-99371035] pre{background:#00000059;border-radius:4px;margin:.5em 0;padding:.5em .75em;overflow-x:auto}.markdown-message[data-v-99371035] code{word-break:break-all;background:#0000004d;border-radius:3px;padding:.1em .3em;font-size:.9em}.markdown-message[data-v-99371035] pre code{background:0 0;padding:0}.markdown-message[data-v-99371035] ul,.markdown-message[data-v-99371035] ol{margin:.25em 0 .5em;padding-left:1.5em}.markdown-message[data-v-99371035] li{margin:.15em 0}.markdown-message[data-v-99371035] a{color:var(--kobo-text-2);text-decoration:underline}.markdown-message[data-v-99371035] .document-link{color:var(--kobo-text-2);cursor:pointer;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.markdown-message[data-v-99371035] .document-link:hover{color:var(--kobo-text);-webkit-text-decoration:underline;text-decoration:underline}.markdown-message[data-v-99371035] h1,.markdown-message[data-v-99371035] h2,.markdown-message[data-v-99371035] h3,.markdown-message[data-v-99371035] h4,.markdown-message[data-v-99371035] h5,.markdown-message[data-v-99371035] h6{margin:.5em 0 .3em;font-weight:600;line-height:1.3}.markdown-message[data-v-99371035] h1{font-size:1.25em}.markdown-message[data-v-99371035] h2{font-size:1.15em}.markdown-message[data-v-99371035] h3{font-size:1.08em}.markdown-message[data-v-99371035] h4,.markdown-message[data-v-99371035] h5,.markdown-message[data-v-99371035] h6{font-size:1em}.markdown-message[data-v-99371035] blockquote{color:#ffffffb3;border-left:3px solid #fff3;margin:.5em 0;padding-left:.75em}.markdown-message[data-v-99371035] table{border-collapse:collapse;margin:.5em 0}.markdown-message[data-v-99371035] th,.markdown-message[data-v-99371035] td{border:1px solid #ffffff26;padding:.25em .5em}.tool-row[data-v-2482adfe]{border-radius:4px;margin:0;font-size:12px}.tool-header[data-v-2482adfe]{color:var(--kobo-text-2);cursor:default;align-items:center;gap:10px;min-width:0;padding:5px 10px;display:flex}.tool-row:not(.tool-row-generic) .tool-header[data-v-2482adfe],.tool-row--toggleable .tool-header[data-v-2482adfe]{cursor:pointer}.tool-row:has(.tool-diff) .tool-header[data-v-2482adfe]{cursor:pointer}.tool-row:not(.tool-row-generic) .tool-header[data-v-2482adfe]:hover,.tool-row--toggleable .tool-header[data-v-2482adfe]:hover{background:#ffffff08}.tool-icon[data-v-2482adfe]{color:var(--kobo-text-2);flex-shrink:0}.tool-name[data-v-2482adfe]{color:var(--kobo-text-2);flex-shrink:0;font-weight:600}.tool-arg[data-v-2482adfe],.tool-path[data-v-2482adfe]{color:var(--kobo-text-3);text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;font-family:var(--kobo-font-mono);font-size:11.5px;overflow:hidden}.tool-open-diff[data-v-2482adfe]{flex-shrink:0}.tool-path[data-v-2482adfe],.tool-arg[data-v-2482adfe]{flex:1}.tool-stat-add[data-v-2482adfe]{color:var(--kobo-success);flex-shrink:0;font-size:11px;font-weight:600}.tool-stat-del[data-v-2482adfe]{color:var(--kobo-danger);flex-shrink:0;font-size:11px;font-weight:600}.tool-diff[data-v-2482adfe]{max-height:400px;font-family:var(--kobo-font-mono);background:#0003;border-radius:4px;margin-top:4px;padding:8px 0;font-size:11px;line-height:1.5;overflow:auto}.diff-line[data-v-2482adfe]{white-space:pre;color:var(--kobo-text-2);padding:0 12px}.diff-sign[data-v-2482adfe]{width:14px;color:var(--kobo-text-disabled);-webkit-user-select:none;user-select:none;display:inline-block}.diff-add[data-v-2482adfe]{color:var(--kobo-success);background:#66bb6a1a}.diff-add .diff-sign[data-v-2482adfe]{color:var(--kobo-success)}.diff-del[data-v-2482adfe]{color:var(--kobo-danger);background:#ef53501a}.diff-del .diff-sign[data-v-2482adfe]{color:var(--kobo-danger)}.tool-output[data-v-2482adfe]{color:var(--kobo-text-2);font-family:var(--kobo-font-mono);white-space:pre-wrap;background:#00000026;border-radius:4px;max-height:8em;margin-top:4px;padding:6px 10px;font-size:11px;overflow:auto}.markdown-message[data-v-7687eb7e]{color:var(--kobo-text-2);word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-7687eb7e] *{max-width:100%}.markdown-message[data-v-7687eb7e] img{-o-object-fit:contain;object-fit:contain;cursor:zoom-in;background:#0003;border-radius:4px;max-width:180px;max-height:100px;margin:.3em 0;display:block}.markdown-message[data-v-7687eb7e] code{word-break:break-all}.markdown-message[data-v-7687eb7e] p{margin:0 0 .4em}.markdown-message[data-v-7687eb7e] p:last-child{margin-bottom:0}.markdown-message[data-v-7687eb7e] code{background:#00000040;border-radius:3px;padding:.1em .3em}.markdown-message[data-v-7687eb7e] h1,.markdown-message[data-v-7687eb7e] h2,.markdown-message[data-v-7687eb7e] h3,.markdown-message[data-v-7687eb7e] h4,.markdown-message[data-v-7687eb7e] h5,.markdown-message[data-v-7687eb7e] h6{margin:.4em 0 .25em;font-weight:600;line-height:1.3}.markdown-message[data-v-7687eb7e] h1{font-size:1.25em}.markdown-message[data-v-7687eb7e] h2{font-size:1.15em}.markdown-message[data-v-7687eb7e] h3{font-size:1.08em}.markdown-message[data-v-7687eb7e] h4,.markdown-message[data-v-7687eb7e] h5,.markdown-message[data-v-7687eb7e] h6{font-size:1em}.markdown-user-prompt[data-v-7687eb7e]{color:var(--kobo-text-2);font-size:12px;font-style:italic}.markdown-user-prompt[data-v-7687eb7e] p{margin:0 0 .4em}.markdown-user-prompt[data-v-7687eb7e] code{background:#ffffff14;border-radius:3px;padding:.1em .3em;font-style:normal}.image-lightbox-img{-o-object-fit:contain;object-fit:contain;cursor:zoom-out;background:#0000004d;border-radius:4px;max-width:92vw;max-height:92vh;display:block}.turn-card[data-v-6e61835e]{border:1px solid #ffffff14;border-left:3px solid var(--turn-accent);background:#ffffff05;border-radius:6px;min-width:0;max-width:100%;margin:14px 0;overflow:hidden}.turn-card--highlighted[data-v-6e61835e]{animation:1.8s ease-out search-highlight-6e61835e}@keyframes search-highlight-6e61835e{0%,45%{box-shadow:0 0 0 2px #818cf8f2,0 0 22px #818cf873}to{box-shadow:none}}.turn-header[data-v-6e61835e]{color:var(--kobo-text-3);background:#ffffff08;border-bottom:1px solid #ffffff0d;align-items:center;gap:8px;padding:8px 14px;font-size:11px;display:flex}.turn-badge[data-v-6e61835e]{letter-spacing:.3px;border-radius:3px;padding:2px 8px;font-size:11px;font-weight:700}.turn-badge-user[data-v-6e61835e]{color:var(--kobo-turn-user);background:#ce93d826}.turn-badge-agent[data-v-6e61835e]{color:var(--kobo-turn-agent);background:#7986cb26}.turn-badge-system[data-v-6e61835e]{color:var(--kobo-text-2);background:#75757533;font-style:italic}.turn-badge-session[data-v-6e61835e]{color:var(--kobo-text-3);background:#61616133}.turn-badge-script[data-v-6e61835e]{color:var(--kobo-success);background:#4db6ac26}.turn-time[data-v-6e61835e]{color:var(--kobo-text-disabled);font-family:var(--kobo-font-mono);font-size:11px}.turn-time-arrow[data-v-6e61835e]{opacity:.7;margin:0 -2px}.turn-time-updated[data-v-6e61835e]{color:var(--kobo-text-3)}.turn-actions[data-v-6e61835e]{color:var(--kobo-text-3);font-size:11px}.turn-body[data-v-6e61835e]{flex-direction:column;gap:12px;min-width:0;padding:14px 18px;display:flex}.turn-body[data-v-6e61835e]>*{min-width:0;max-width:100%}.turn-body[data-v-6e61835e] .tool-row+.tool-row{margin-top:-8px}.turn-scroll-top[data-v-6e61835e]{justify-content:flex-start;padding:0 8px 6px;display:flex}.turn-scroll-top-btn[data-v-6e61835e]{opacity:.5;transition:opacity .15s}.turn-scroll-top-btn[data-v-6e61835e]:hover{opacity:1}.activity-feed-wrap[data-v-01cd9f2a]{width:100%;height:100%;position:relative}.activity-feed-scroll[data-v-01cd9f2a]{width:100%;height:100%}.activity-feed-nav-cluster[data-v-01cd9f2a]{z-index:2;align-items:center;gap:8px;display:flex;position:absolute;bottom:14px;right:14px}.activity-feed-compacting[data-v-01cd9f2a]{z-index:3;background:var(--kobo-surface);border:1px solid var(--kobo-border);border-radius:6px;align-items:center;gap:8px;padding:6px 14px;display:flex;position:absolute;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 16px #00000059}.fade-enter-active[data-v-01cd9f2a],.fade-leave-active[data-v-01cd9f2a]{transition:opacity .16s}.fade-enter-from[data-v-01cd9f2a],.fade-leave-to[data-v-01cd9f2a]{opacity:0}.activity-feed-nav-btn[data-v-01cd9f2a]{opacity:.8;transition:opacity .12s}.activity-feed-nav-btn[data-v-01cd9f2a]:hover{opacity:1}.activity-feed-scroll[data-v-01cd9f2a] .q-scrollarea__content{max-width:100%;overflow-x:hidden}.activity-feed-switching[data-v-01cd9f2a]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.activity-feed-empty[data-v-01cd9f2a]{align-items:center;gap:var(--kobo-space-sm);padding:var(--kobo-space-4xl) var(--kobo-space-xl);color:var(--kobo-text-3);text-align:center;flex-direction:column;display:flex}.activity-feed-empty__title[data-v-01cd9f2a]{font-family:var(--kobo-font-sans);color:var(--kobo-text-2);font-size:14px;font-weight:500}.activity-feed-empty__hint[data-v-01cd9f2a]{font-family:var(--kobo-font-sans);color:var(--kobo-text-3);max-width:380px;font-size:13px}
1
+ .agent-error-item[data-v-e98c3c02]{gap:var(--kobo-space-sm);padding:var(--kobo-space-sm) var(--kobo-space-md);border-left:2px solid var(--kobo-danger);background:var(--kobo-surface-2);border-radius:var(--kobo-radius-sm);align-items:flex-start;display:flex}.agent-error-item__icon[data-v-e98c3c02]{color:var(--kobo-danger);flex:none;margin-top:2px}.agent-error-item__body[data-v-e98c3c02]{min-width:0}.agent-error-item__title[data-v-e98c3c02]{color:var(--kobo-text);font-weight:600}.agent-error-item__message[data-v-e98c3c02]{color:var(--kobo-text-2);font-family:var(--kobo-font-mono);white-space:pre-wrap;word-break:break-word;font-size:12px}.markdown-message[data-v-99371035]{color:var(--kobo-text-2);word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-99371035] *{max-width:100%}.markdown-message[data-v-99371035] p{margin:0 0 .5em}.markdown-message[data-v-99371035] p:last-child{margin-bottom:0}.markdown-message[data-v-99371035] pre{background:#00000059;border-radius:4px;margin:.5em 0;padding:.5em .75em;overflow-x:auto}.markdown-message[data-v-99371035] code{word-break:break-all;background:#0000004d;border-radius:3px;padding:.1em .3em;font-size:.9em}.markdown-message[data-v-99371035] pre code{background:0 0;padding:0}.markdown-message[data-v-99371035] ul,.markdown-message[data-v-99371035] ol{margin:.25em 0 .5em;padding-left:1.5em}.markdown-message[data-v-99371035] li{margin:.15em 0}.markdown-message[data-v-99371035] a{color:var(--kobo-text-2);text-decoration:underline}.markdown-message[data-v-99371035] .document-link{color:var(--kobo-text-2);cursor:pointer;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.markdown-message[data-v-99371035] .document-link:hover{color:var(--kobo-text);-webkit-text-decoration:underline;text-decoration:underline}.markdown-message[data-v-99371035] h1,.markdown-message[data-v-99371035] h2,.markdown-message[data-v-99371035] h3,.markdown-message[data-v-99371035] h4,.markdown-message[data-v-99371035] h5,.markdown-message[data-v-99371035] h6{margin:.5em 0 .3em;font-weight:600;line-height:1.3}.markdown-message[data-v-99371035] h1{font-size:1.25em}.markdown-message[data-v-99371035] h2{font-size:1.15em}.markdown-message[data-v-99371035] h3{font-size:1.08em}.markdown-message[data-v-99371035] h4,.markdown-message[data-v-99371035] h5,.markdown-message[data-v-99371035] h6{font-size:1em}.markdown-message[data-v-99371035] blockquote{color:#ffffffb3;border-left:3px solid #fff3;margin:.5em 0;padding-left:.75em}.markdown-message[data-v-99371035] table{border-collapse:collapse;margin:.5em 0}.markdown-message[data-v-99371035] th,.markdown-message[data-v-99371035] td{border:1px solid #ffffff26;padding:.25em .5em}.tool-row[data-v-2482adfe]{border-radius:4px;margin:0;font-size:12px}.tool-header[data-v-2482adfe]{color:var(--kobo-text-2);cursor:default;align-items:center;gap:10px;min-width:0;padding:5px 10px;display:flex}.tool-row:not(.tool-row-generic) .tool-header[data-v-2482adfe],.tool-row--toggleable .tool-header[data-v-2482adfe]{cursor:pointer}.tool-row:has(.tool-diff) .tool-header[data-v-2482adfe]{cursor:pointer}.tool-row:not(.tool-row-generic) .tool-header[data-v-2482adfe]:hover,.tool-row--toggleable .tool-header[data-v-2482adfe]:hover{background:#ffffff08}.tool-icon[data-v-2482adfe]{color:var(--kobo-text-2);flex-shrink:0}.tool-name[data-v-2482adfe]{color:var(--kobo-text-2);flex-shrink:0;font-weight:600}.tool-arg[data-v-2482adfe],.tool-path[data-v-2482adfe]{color:var(--kobo-text-3);text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;font-family:var(--kobo-font-mono);font-size:11.5px;overflow:hidden}.tool-open-diff[data-v-2482adfe]{flex-shrink:0}.tool-path[data-v-2482adfe],.tool-arg[data-v-2482adfe]{flex:1}.tool-stat-add[data-v-2482adfe]{color:var(--kobo-success);flex-shrink:0;font-size:11px;font-weight:600}.tool-stat-del[data-v-2482adfe]{color:var(--kobo-danger);flex-shrink:0;font-size:11px;font-weight:600}.tool-diff[data-v-2482adfe]{max-height:400px;font-family:var(--kobo-font-mono);background:#0003;border-radius:4px;margin-top:4px;padding:8px 0;font-size:11px;line-height:1.5;overflow:auto}.diff-line[data-v-2482adfe]{white-space:pre;color:var(--kobo-text-2);padding:0 12px}.diff-sign[data-v-2482adfe]{width:14px;color:var(--kobo-text-disabled);-webkit-user-select:none;user-select:none;display:inline-block}.diff-add[data-v-2482adfe]{color:var(--kobo-success);background:#66bb6a1a}.diff-add .diff-sign[data-v-2482adfe]{color:var(--kobo-success)}.diff-del[data-v-2482adfe]{color:var(--kobo-danger);background:#ef53501a}.diff-del .diff-sign[data-v-2482adfe]{color:var(--kobo-danger)}.tool-output[data-v-2482adfe]{color:var(--kobo-text-2);font-family:var(--kobo-font-mono);white-space:pre-wrap;background:#00000026;border-radius:4px;max-height:8em;margin-top:4px;padding:6px 10px;font-size:11px;overflow:auto}.markdown-message[data-v-7687eb7e]{color:var(--kobo-text-2);word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-7687eb7e] *{max-width:100%}.markdown-message[data-v-7687eb7e] img{-o-object-fit:contain;object-fit:contain;cursor:zoom-in;background:#0003;border-radius:4px;max-width:180px;max-height:100px;margin:.3em 0;display:block}.markdown-message[data-v-7687eb7e] code{word-break:break-all}.markdown-message[data-v-7687eb7e] p{margin:0 0 .4em}.markdown-message[data-v-7687eb7e] p:last-child{margin-bottom:0}.markdown-message[data-v-7687eb7e] code{background:#00000040;border-radius:3px;padding:.1em .3em}.markdown-message[data-v-7687eb7e] h1,.markdown-message[data-v-7687eb7e] h2,.markdown-message[data-v-7687eb7e] h3,.markdown-message[data-v-7687eb7e] h4,.markdown-message[data-v-7687eb7e] h5,.markdown-message[data-v-7687eb7e] h6{margin:.4em 0 .25em;font-weight:600;line-height:1.3}.markdown-message[data-v-7687eb7e] h1{font-size:1.25em}.markdown-message[data-v-7687eb7e] h2{font-size:1.15em}.markdown-message[data-v-7687eb7e] h3{font-size:1.08em}.markdown-message[data-v-7687eb7e] h4,.markdown-message[data-v-7687eb7e] h5,.markdown-message[data-v-7687eb7e] h6{font-size:1em}.markdown-user-prompt[data-v-7687eb7e]{color:var(--kobo-text-2);font-size:12px;font-style:italic}.markdown-user-prompt[data-v-7687eb7e] p{margin:0 0 .4em}.markdown-user-prompt[data-v-7687eb7e] code{background:#ffffff14;border-radius:3px;padding:.1em .3em;font-style:normal}.image-lightbox-img{-o-object-fit:contain;object-fit:contain;cursor:zoom-out;background:#0000004d;border-radius:4px;max-width:92vw;max-height:92vh;display:block}.turn-card[data-v-6e61835e]{border:1px solid #ffffff14;border-left:3px solid var(--turn-accent);background:#ffffff05;border-radius:6px;min-width:0;max-width:100%;margin:14px 0;overflow:hidden}.turn-card--highlighted[data-v-6e61835e]{animation:1.8s ease-out search-highlight-6e61835e}@keyframes search-highlight-6e61835e{0%,45%{box-shadow:0 0 0 2px #818cf8f2,0 0 22px #818cf873}to{box-shadow:none}}.turn-header[data-v-6e61835e]{color:var(--kobo-text-3);background:#ffffff08;border-bottom:1px solid #ffffff0d;align-items:center;gap:8px;padding:8px 14px;font-size:11px;display:flex}.turn-badge[data-v-6e61835e]{letter-spacing:.3px;border-radius:3px;padding:2px 8px;font-size:11px;font-weight:700}.turn-badge-user[data-v-6e61835e]{color:var(--kobo-turn-user);background:#ce93d826}.turn-badge-agent[data-v-6e61835e]{color:var(--kobo-turn-agent);background:#7986cb26}.turn-badge-system[data-v-6e61835e]{color:var(--kobo-text-2);background:#75757533;font-style:italic}.turn-badge-session[data-v-6e61835e]{color:var(--kobo-text-3);background:#61616133}.turn-badge-script[data-v-6e61835e]{color:var(--kobo-success);background:#4db6ac26}.turn-time[data-v-6e61835e]{color:var(--kobo-text-disabled);font-family:var(--kobo-font-mono);font-size:11px}.turn-time-arrow[data-v-6e61835e]{opacity:.7;margin:0 -2px}.turn-time-updated[data-v-6e61835e]{color:var(--kobo-text-3)}.turn-actions[data-v-6e61835e]{color:var(--kobo-text-3);font-size:11px}.turn-body[data-v-6e61835e]{flex-direction:column;gap:12px;min-width:0;padding:14px 18px;display:flex}.turn-body[data-v-6e61835e]>*{min-width:0;max-width:100%}.turn-body[data-v-6e61835e] .tool-row+.tool-row{margin-top:-8px}.turn-scroll-top[data-v-6e61835e]{justify-content:flex-start;padding:0 8px 6px;display:flex}.turn-scroll-top-btn[data-v-6e61835e]{opacity:.5;transition:opacity .15s}.turn-scroll-top-btn[data-v-6e61835e]:hover{opacity:1}.activity-feed-wrap[data-v-d83f0940]{width:100%;height:100%;position:relative}.activity-feed-scroll[data-v-d83f0940]{width:100%;height:100%}.activity-feed-nav-cluster[data-v-d83f0940]{z-index:2;align-items:center;gap:8px;display:flex;position:absolute;bottom:14px;right:14px}.activity-feed-compacting[data-v-d83f0940]{z-index:3;background:var(--kobo-surface);border:1px solid var(--kobo-border);border-radius:6px;align-items:center;gap:8px;padding:6px 14px;display:flex;position:absolute;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 16px #00000059}.fade-enter-active[data-v-d83f0940],.fade-leave-active[data-v-d83f0940]{transition:opacity .16s}.fade-enter-from[data-v-d83f0940],.fade-leave-to[data-v-d83f0940]{opacity:0}.activity-feed-nav-btn[data-v-d83f0940]{opacity:.8;transition:opacity .12s}.activity-feed-nav-btn[data-v-d83f0940]:hover{opacity:1}.activity-feed-scroll[data-v-d83f0940] .q-scrollarea__content{max-width:100%;overflow-x:hidden}.activity-feed-switching[data-v-d83f0940]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.activity-feed-empty[data-v-d83f0940]{align-items:center;gap:var(--kobo-space-sm);padding:var(--kobo-space-4xl) var(--kobo-space-xl);color:var(--kobo-text-3);text-align:center;flex-direction:column;display:flex}.activity-feed-empty__title[data-v-d83f0940]{font-family:var(--kobo-font-sans);color:var(--kobo-text-2);font-size:14px;font-weight:500}.activity-feed-empty__hint[data-v-d83f0940]{font-family:var(--kobo-font-sans);color:var(--kobo-text-3);max-width:380px;font-size:13px}