@vibedeckx/linux-x64 0.3.26 → 0.3.28

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 (2) hide show
  1. package/dist/bin.js +404 -32
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -186425,6 +186425,10 @@ var createRemoteServerRepos = (kdb, _h) => ({
186425
186425
  ]).where("project_remotes.project_id", "=", projectId).orderBy("project_remotes.sort_order", "asc").orderBy("project_remotes.id", "asc").execute();
186426
186426
  return rows.map(mapProjectRemoteWithServer);
186427
186427
  },
186428
+ listProjectIdsByServer: async (remoteServerId) => {
186429
+ const rows = await kdb.selectFrom("project_remotes").select("project_id").distinct().where("remote_server_id", "=", remoteServerId).execute();
186430
+ return rows.map((r) => r.project_id);
186431
+ },
186428
186432
  getByProjectAndServer: async (projectId, remoteServerId) => {
186429
186433
  const row = await kdb.selectFrom("project_remotes").innerJoin("remote_servers", "remote_servers.id", "project_remotes.remote_server_id").select([
186430
186434
  "project_remotes.id",
@@ -207169,7 +207173,7 @@ function sessionMilestoneForTurnEnd(opts) {
207169
207173
  workflow_run_id: null,
207170
207174
  created_at: opts.createdAt
207171
207175
  };
207172
- if (opts.outcome === "completed") {
207176
+ if (opts.outcome === "completed" || opts.outcome === "completed_with_pending_tasks") {
207173
207177
  return {
207174
207178
  ...base,
207175
207179
  id: sessionResultReadyId(opts.sessionId, opts.entryIndex),
@@ -207274,8 +207278,10 @@ var ClaudeCodeProvider = class {
207274
207278
  if (systemMsg.subtype === BACKGROUND_TASKS_CHANGED_SUBTYPE) {
207275
207279
  const tasks = msg.tasks;
207276
207280
  if (!Array.isArray(tasks)) return [];
207277
- const taskIds = tasks.map((t) => t.task_id).filter((id) => typeof id === "string");
207278
- return [{ type: "task_list_changed", taskIds }];
207281
+ const parsed = tasks.flatMap(
207282
+ (t) => typeof t.task_id === "string" ? [{ taskId: t.task_id, taskType: t.task_type, description: t.description }] : []
207283
+ );
207284
+ return [{ type: "task_list_changed", tasks: parsed }];
207279
207285
  }
207280
207286
  if (systemMsg.subtype === INIT_SUBTYPE) {
207281
207287
  const nativeId = msg.session_id;
@@ -207306,6 +207312,25 @@ var ClaudeCodeProvider = class {
207306
207312
  formatUserInput(content, _sessionId) {
207307
207313
  return serializeUserInput(content);
207308
207314
  }
207315
+ /**
207316
+ * `stop_task` control_request — the documented Agent SDK primitive for
207317
+ * killing one background task, verified live against claude 2.1.238: the CLI
207318
+ * answers `control_response { subtype: "success" }`, empties its task list
207319
+ * and the process really dies.
207320
+ *
207321
+ * `request_id` is namespaced by task id so a response could be correlated;
207322
+ * nothing reads control_response today, and it does not need to — the
207323
+ * authoritative `background_tasks_changed` snapshot that follows is what
207324
+ * updates the ledger. An older CLI that does not know the subtype answers an
207325
+ * error and simply changes nothing.
207326
+ */
207327
+ formatStopBackgroundTask(taskId, _sessionId) {
207328
+ return JSON.stringify({
207329
+ type: "control_request",
207330
+ request_id: `stop_task-${taskId}`,
207331
+ request: { subtype: "stop_task", task_id: taskId }
207332
+ }) + "\n";
207333
+ }
207309
207334
  // Lifecycle hooks are no-ops for Claude (stateless per-session)
207310
207335
  onSessionCreated(_sessionId, _permissionMode) {
207311
207336
  }
@@ -229421,7 +229446,7 @@ function isResidentProcessInScope(candidate, scope) {
229421
229446
  return candidate.projectId === scope.projectId && candidate.branch === scope.branch;
229422
229447
  }
229423
229448
  function isIdleResidentProcess(candidate) {
229424
- return candidate.processAlive && !candidate.dormant && candidate.status !== "running" && candidate.backgroundTaskCount === 0;
229449
+ return candidate.processAlive && !candidate.dormant && candidate.status !== "running" && !candidate.backgroundTasksProtect;
229425
229450
  }
229426
229451
  function pickIdleResidentEvictionCandidate(candidates, scope) {
229427
229452
  return candidates.filter((candidate) => !scope || isResidentProcessInScope(candidate, scope)).filter(isIdleResidentProcess).sort((a, b2) => a.lastActiveAt - b2.lastActiveAt)[0] ?? null;
@@ -229443,9 +229468,14 @@ var ResidentProcessLimitError = class extends Error {
229443
229468
 
229444
229469
  // src/turn-completion.ts
229445
229470
  var COMPLETION_GRACE_MS = 1500;
229471
+ var PARK_TIMEOUT_MS = 20 * 60 * 1e3;
229446
229472
  var TurnCompletionLedger = class {
229473
+ constructor(parkTimeoutMs = PARK_TIMEOUT_MS) {
229474
+ this.parkTimeoutMs = parkTimeoutMs;
229475
+ }
229476
+ parkTimeoutMs;
229447
229477
  /** Live background tasks by harness task_id (same id may restart). */
229448
- tasks = /* @__PURE__ */ new Set();
229478
+ tasks = /* @__PURE__ */ new Map();
229449
229479
  /** Held completion candidate — the latest success result, if any. */
229450
229480
  pending = null;
229451
229481
  /** Bumped whenever the candidate changes; stale grace timers no-op. */
@@ -229456,27 +229486,99 @@ var TurnCompletionLedger = class {
229456
229486
  * grace delay (the common case).
229457
229487
  */
229458
229488
  sawBackgroundActivity = false;
229489
+ /**
229490
+ * When the held candidate was parked behind live background tasks — the
229491
+ * moment the agent stopped working and only tasks kept the turn open. The
229492
+ * park deadline counts from here, not from when a task started: the number
229493
+ * that matters to the user is "how long since the agent answered".
229494
+ */
229495
+ parkedSince = null;
229496
+ /** Task ids the user vouched for; they stop counting toward the deadline. */
229497
+ sanctioned = /* @__PURE__ */ new Set();
229498
+ /**
229499
+ * Whether a park deadline already expired and closed the turn. Live tasks
229500
+ * normally shield the session from being reclaimed — hibernating would kill
229501
+ * a real build and the auto-resume that reads it — but that shield must not
229502
+ * outlast the deadline, or one stuck task pins a resident process slot
229503
+ * forever and new sessions on the branch get turned away.
229504
+ */
229505
+ parkDeadlineExpired = false;
229459
229506
  get pendingTaskCount() {
229460
229507
  return this.tasks.size;
229461
229508
  }
229462
229509
  get hasPendingCompletion() {
229463
229510
  return this.pending !== null;
229464
229511
  }
229465
- taskStarted(taskId) {
229466
- this.tasks.add(taskId);
229512
+ /** Live tasks in first-seen order — the payload the UI renders. */
229513
+ get backgroundTasks() {
229514
+ return [...this.tasks.values()].map(
229515
+ (task) => this.sanctioned.has(task.taskId) ? { ...task, sanctioned: true } : task
229516
+ );
229517
+ }
229518
+ /**
229519
+ * When the parked turn will be committed anyway, or null if nothing is
229520
+ * parked or every live task has been vouched for. Timer-free by design: the
229521
+ * caller re-reads this after each mutation and syncs its own timer, so the
229522
+ * ledger stays a pure state machine.
229523
+ */
229524
+ get parkDeadlineAt() {
229525
+ if (this.pending === null || this.parkedSince === null) return null;
229526
+ const allVouchedFor = [...this.tasks.keys()].every((id) => this.sanctioned.has(id));
229527
+ return allVouchedFor ? null : this.parkedSince + this.parkTimeoutMs;
229528
+ }
229529
+ /**
229530
+ * The user vouched for a task: stop counting it toward the deadline. This
229531
+ * restores the original behavior for that task — wait for it, let the
229532
+ * auto-resume close the turn — but now as an explicit choice.
229533
+ */
229534
+ sanction(taskId) {
229535
+ if (this.tasks.has(taskId)) this.sanctioned.add(taskId);
229536
+ }
229537
+ /**
229538
+ * The deadline expired: commit the parked candidate. Deliberately uses the
229539
+ * ORIGINAL payload — its duration/cost/tokens describe the turn the agent
229540
+ * actually ran, not the time spent waiting on a stuck task.
229541
+ */
229542
+ parkDeadlineElapsed() {
229543
+ if (this.pending === null) return { kind: "none" };
229544
+ this.parkDeadlineExpired = true;
229545
+ return this.commitHeld();
229546
+ }
229547
+ /**
229548
+ * Whether live background tasks should still shield this session from
229549
+ * resident-process reclamation. True while they are plausibly doing real
229550
+ * work; false once the deadline judged them anomalous — unless the user
229551
+ * vouched for every one of them, which restores the shield along with the
229552
+ * waiting behavior it protects.
229553
+ */
229554
+ get backgroundTasksProtectSession() {
229555
+ if (this.tasks.size === 0) return false;
229556
+ if (!this.parkDeadlineExpired) return true;
229557
+ return [...this.tasks.keys()].every((id) => this.sanctioned.has(id));
229558
+ }
229559
+ taskStarted(task, now3) {
229560
+ this.upsert(task, this.tasks.get(task.taskId), now3);
229467
229561
  this.sawBackgroundActivity = true;
229468
- return this.rearmIfHeld();
229562
+ return this.rearmIfHeld(now3);
229469
229563
  }
229470
- taskFinished(taskId) {
229564
+ taskFinished(taskId, now3) {
229471
229565
  this.tasks.delete(taskId);
229566
+ this.sanctioned.delete(taskId);
229472
229567
  this.sawBackgroundActivity = true;
229473
- return this.rearmIfHeld();
229568
+ return this.rearmIfHeld(now3);
229474
229569
  }
229475
229570
  /** Authoritative snapshot from `system/background_tasks_changed`. */
229476
- taskListChanged(taskIds) {
229477
- this.tasks = new Set(taskIds);
229478
- if (taskIds.length > 0) this.sawBackgroundActivity = true;
229479
- return this.rearmIfHeld();
229571
+ taskListChanged(tasks, now3) {
229572
+ const previous = this.tasks;
229573
+ this.tasks = /* @__PURE__ */ new Map();
229574
+ for (const task of tasks) {
229575
+ this.upsert(task, previous.get(task.taskId), now3);
229576
+ }
229577
+ for (const id of this.sanctioned) {
229578
+ if (!this.tasks.has(id)) this.sanctioned.delete(id);
229579
+ }
229580
+ if (tasks.length > 0) this.sawBackgroundActivity = true;
229581
+ return this.rearmIfHeld(now3);
229480
229582
  }
229481
229583
  /**
229482
229584
  * The process emitted turn activity: if a completion was held, it was an
@@ -229488,8 +229590,10 @@ var TurnCompletionLedger = class {
229488
229590
  * the race against the grace window.
229489
229591
  */
229490
229592
  noteTurnActivity() {
229593
+ this.parkDeadlineExpired = false;
229491
229594
  if (this.pending === null) return { kind: "none" };
229492
229595
  this.pending = null;
229596
+ this.parkedSince = null;
229493
229597
  this.generation++;
229494
229598
  return { kind: "cancel" };
229495
229599
  }
@@ -229505,9 +229609,11 @@ var TurnCompletionLedger = class {
229505
229609
  this.sawBackgroundActivity = false;
229506
229610
  return this.noteTurnActivity();
229507
229611
  }
229508
- successResult(payload) {
229612
+ successResult(payload, now3) {
229509
229613
  this.generation++;
229510
229614
  if (this.tasks.size > 0) {
229615
+ this.parkedSince = now3;
229616
+ this.parkDeadlineExpired = false;
229511
229617
  this.pending = payload;
229512
229618
  return { kind: "cancel" };
229513
229619
  }
@@ -229521,6 +229627,7 @@ var TurnCompletionLedger = class {
229521
229627
  errorResult() {
229522
229628
  if (this.pending === null) return { kind: "none" };
229523
229629
  this.pending = null;
229630
+ this.parkedSince = null;
229524
229631
  this.generation++;
229525
229632
  return { kind: "cancel" };
229526
229633
  }
@@ -229546,7 +229653,10 @@ var TurnCompletionLedger = class {
229546
229653
  /** Full reset (fresh spawn / stop / hibernate / agent switch). */
229547
229654
  reset() {
229548
229655
  this.tasks.clear();
229656
+ this.sanctioned.clear();
229549
229657
  this.pending = null;
229658
+ this.parkedSince = null;
229659
+ this.parkDeadlineExpired = false;
229550
229660
  this.generation++;
229551
229661
  this.sawBackgroundActivity = false;
229552
229662
  }
@@ -229555,15 +229665,34 @@ var TurnCompletionLedger = class {
229555
229665
  * have no resume behind it), so they delay the commit rather than cancel
229556
229666
  * it — and while tasks are still live the candidate stays parked with no
229557
229667
  * timer at all (only an empty set can complete a turn). */
229558
- rearmIfHeld() {
229668
+ rearmIfHeld(now3) {
229559
229669
  if (this.pending === null) return { kind: "none" };
229560
229670
  this.generation++;
229561
- if (this.tasks.size > 0) return { kind: "cancel" };
229671
+ if (this.tasks.size > 0) {
229672
+ this.parkedSince ??= now3;
229673
+ return { kind: "cancel" };
229674
+ }
229562
229675
  return { kind: "schedule", generation: this.generation };
229563
229676
  }
229677
+ /**
229678
+ * Merge a descriptor into the live set, keeping the earliest `startedAt` and
229679
+ * any label already known: `task_started` carries a description that the
229680
+ * snapshot for the same task may omit, and the two arrive in either order.
229681
+ * `known` comes from the caller: a snapshot resync rebuilds the map, so the
229682
+ * prior entry is no longer reachable through `this.tasks`.
229683
+ */
229684
+ upsert(task, known, now3) {
229685
+ this.tasks.set(task.taskId, {
229686
+ taskId: task.taskId,
229687
+ taskType: task.taskType ?? known?.taskType,
229688
+ description: task.description ?? known?.description,
229689
+ startedAt: known?.startedAt ?? now3
229690
+ });
229691
+ }
229564
229692
  commitHeld() {
229565
229693
  const payload = this.pending;
229566
229694
  this.pending = null;
229695
+ this.parkedSince = null;
229567
229696
  this.generation++;
229568
229697
  return { kind: "commit", payload };
229569
229698
  }
@@ -229644,10 +229773,13 @@ var AgentSessionManager = class {
229644
229773
  retentionDeleting = /* @__PURE__ */ new Set();
229645
229774
  /** Grace window before committing a held completion (injectable for tests). */
229646
229775
  completionGraceMs;
229776
+ /** Bound on a parked completion (injectable for tests). */
229777
+ parkTimeoutMs;
229647
229778
  workflowSuppressionCheck = null;
229648
229779
  constructor(storage2, opts) {
229649
229780
  this.storage = storage2;
229650
229781
  this.completionGraceMs = opts?.completionGraceMs ?? COMPLETION_GRACE_MS;
229782
+ this.parkTimeoutMs = opts?.parkTimeoutMs ?? PARK_TIMEOUT_MS;
229651
229783
  }
229652
229784
  async resolveSessionWorktreePath(session, legacyProjectPath) {
229653
229785
  if (!session.workspaceCheckoutId) {
@@ -229922,7 +230054,7 @@ var AgentSessionManager = class {
229922
230054
  processAlive: this.isProcessAlive(session),
229923
230055
  status: session.status,
229924
230056
  dormant: session.dormant,
229925
- backgroundTaskCount: session.completion.pendingTaskCount,
230057
+ backgroundTasksProtect: session.completion.backgroundTasksProtectSession,
229926
230058
  lastActiveAt: session.lastActiveAt,
229927
230059
  projectId: session.projectId,
229928
230060
  branch: session.branch
@@ -230070,8 +230202,9 @@ var AgentSessionManager = class {
230070
230202
  crossRemoteMcp: opts.crossRemoteMcp,
230071
230203
  agentType,
230072
230204
  model,
230073
- completion: new TurnCompletionLedger(),
230205
+ completion: new TurnCompletionLedger(this.parkTimeoutMs),
230074
230206
  graceTimer: null,
230207
+ parkTimer: null,
230075
230208
  eventChain: Promise.resolve(),
230076
230209
  bgSpawnHintsThisTurn: 0,
230077
230210
  taskStartedThisTurn: 0,
@@ -230226,6 +230359,7 @@ var AgentSessionManager = class {
230226
230359
  if (action.kind === "commit") {
230227
230360
  await this.commitCompletion(session, action.payload);
230228
230361
  }
230362
+ this.broadcastBackgroundTasks(session);
230229
230363
  if (code !== 0 && !spawnFailed && !session.producedOutput) {
230230
230364
  try {
230231
230365
  await this.pushEntry(session.id, {
@@ -230327,25 +230461,119 @@ var AgentSessionManager = class {
230327
230461
  } else if (action.kind === "schedule") {
230328
230462
  this.armGraceTimer(session, action.generation);
230329
230463
  }
230464
+ this.syncParkTimer(session);
230465
+ }
230466
+ /**
230467
+ * Keep the park timer in step with the ledger's deadline.
230468
+ *
230469
+ * Driven by ledger STATE rather than by an action kind, because the deadline
230470
+ * survives across many actions (every task event returns `cancel` while a
230471
+ * completion stays parked) and can also be lifted without any action at all
230472
+ * when the user vouches for the last unvouched task. Re-reading the state
230473
+ * after each mutation is the only way the two can't drift.
230474
+ */
230475
+ syncParkTimer(session) {
230476
+ const deadlineAt = session.completion.parkDeadlineAt;
230477
+ if (deadlineAt === null) {
230478
+ if (session.parkTimer) {
230479
+ clearTimeout(session.parkTimer);
230480
+ session.parkTimer = null;
230481
+ }
230482
+ return;
230483
+ }
230484
+ if (session.parkTimer) return;
230485
+ const timer = setTimeout(() => {
230486
+ session.parkTimer = null;
230487
+ this.enqueueSessionWork(session, async () => {
230488
+ const action = session.completion.parkDeadlineElapsed();
230489
+ if (action.kind !== "commit") return;
230490
+ console.log(
230491
+ `[AgentSession] parked completion exceeded ${this.parkTimeoutMs}ms with ${session.completion.pendingTaskCount} background task(s) still running \u2014 committing the turn anyway (session=${session.id})`
230492
+ );
230493
+ await this.commitCompletion(session, action.payload, "completed_with_pending_tasks");
230494
+ this.broadcastBackgroundTasks(session);
230495
+ }, "completion-park-deadline");
230496
+ }, Math.max(0, deadlineAt - Date.now()));
230497
+ timer.unref?.();
230498
+ session.parkTimer = timer;
230499
+ }
230500
+ /**
230501
+ * The user vouched for a background task: it stops counting toward the park
230502
+ * deadline, restoring the original wait-for-auto-resume behavior for that
230503
+ * task alone — now as an explicit choice rather than a silent assumption.
230504
+ */
230505
+ /**
230506
+ * Ask the agent to stop one background task.
230507
+ *
230508
+ * Returns "unsupported" for agents with no such primitive (Codex), so the
230509
+ * caller can say "stop the session instead" rather than showing a dead
230510
+ * button. On success nothing is updated here: the CLI's own
230511
+ * `background_tasks_changed` snapshot drains the ledger, which then commits
230512
+ * the parked turn through the normal path.
230513
+ */
230514
+ stopBackgroundTask(sessionId, taskId) {
230515
+ const session = this.sessions.get(sessionId);
230516
+ if (!session?.process?.stdin) return "not_found";
230517
+ const frame = getProvider(session.agentType).formatStopBackgroundTask?.(taskId, sessionId);
230518
+ if (!frame) return "unsupported";
230519
+ session.process.stdin.write(frame);
230520
+ console.log(`[AgentSession] stop_task sent for ${taskId} (session=${sessionId})`);
230521
+ return "ok";
230522
+ }
230523
+ sanctionBackgroundTask(sessionId, taskId) {
230524
+ const session = this.sessions.get(sessionId);
230525
+ if (!session) return false;
230526
+ session.completion.sanction(taskId);
230527
+ this.syncParkTimer(session);
230528
+ this.broadcastBackgroundTasks(session);
230529
+ return true;
230330
230530
  }
230331
230531
  /** Discard all turn-completion state (fresh spawn / stop / hibernate / agent switch). */
230332
230532
  resetCompletion(session) {
230333
230533
  this.clearGraceTimer(session);
230334
230534
  session.completion.reset();
230535
+ this.syncParkTimer(session);
230536
+ this.broadcastBackgroundTasks(session);
230335
230537
  }
230336
230538
  /**
230337
230539
  * The single place completion side effects run. Fired by processAgentEvent
230338
230540
  * for turns with no background activity (zero delay), by the grace timer
230339
230541
  * for held candidates, and by the close handler on a clean process exit.
230340
230542
  */
230341
- async commitCompletion(session, payload) {
230543
+ /**
230544
+ * Push the live background-task set to every subscriber. Called on each
230545
+ * lifecycle event rather than diffed: the set is tiny and the harness
230546
+ * already only speaks on change, so a plain snapshot keeps the client
230547
+ * stateless (no patch application, no ordering assumptions).
230548
+ */
230549
+ broadcastBackgroundTasks(session) {
230550
+ this.broadcastRaw(session.id, this.backgroundTasksMessage(session));
230551
+ }
230552
+ /**
230553
+ * Reported rather than inferred client-side: whether a single task can be
230554
+ * stopped is a property of the agent (Claude Code has `stop_task`, Codex has
230555
+ * nothing equivalent), and the server is the only side that knows. A client
230556
+ * guessing from the agent type would drift the day Codex gains one.
230557
+ */
230558
+ backgroundTasksMessage(session) {
230559
+ return {
230560
+ backgroundTasks: {
230561
+ tasks: session.completion.backgroundTasks,
230562
+ turnParked: session.completion.hasPendingCompletion,
230563
+ parkDeadlineAt: session.completion.parkDeadlineAt,
230564
+ canStopTasks: !!getProvider(session.agentType).formatStopBackgroundTask
230565
+ }
230566
+ };
230567
+ }
230568
+ async commitCompletion(session, payload, outcome = "completed") {
230342
230569
  const sessionId = session.id;
230343
230570
  console.log(`[AgentSession] taskCompleted: sessionId=${sessionId}, eventBus=${!!this.eventBus}, projectId=${session.projectId}, branch=${session.branch}`);
230344
230571
  const completedAt = Date.now();
230345
230572
  if (!session.skipDb) {
230346
230573
  await this.storage.agentSessions.markCompleted(sessionId, completedAt);
230347
230574
  }
230348
- const turnEndEntryIndex = await this.endActiveTurn(session, "completed");
230575
+ const turnEndEntryIndex = await this.endActiveTurn(session, outcome);
230576
+ this.broadcastBackgroundTasks(session);
230349
230577
  const summaryText = extractLastAssistantText(session.store.entries);
230350
230578
  this.broadcastRaw(sessionId, {
230351
230579
  taskCompleted: {
@@ -230526,19 +230754,26 @@ var AgentSessionManager = class {
230526
230754
  // no auto-resume behind it, and cancelling on it would drop the
230527
230755
  // completion entirely.
230528
230756
  case "task_started":
230529
- this.applyCompletionTimerAction(session, session.completion.taskStarted(event.taskId));
230757
+ this.applyCompletionTimerAction(session, session.completion.taskStarted({
230758
+ taskId: event.taskId,
230759
+ taskType: event.taskType,
230760
+ description: event.description
230761
+ }, timestamp));
230530
230762
  session.taskStartedThisTurn++;
230763
+ this.broadcastBackgroundTasks(session);
230531
230764
  console.log(`[AgentSession] Background task started: ${event.taskId} (${event.taskType ?? "?"}) \u2014 ${session.completion.pendingTaskCount} pending in ${sessionId}`);
230532
230765
  break;
230533
230766
  case "task_finished":
230534
- this.applyCompletionTimerAction(session, session.completion.taskFinished(event.taskId));
230767
+ this.applyCompletionTimerAction(session, session.completion.taskFinished(event.taskId, timestamp));
230768
+ this.broadcastBackgroundTasks(session);
230535
230769
  console.log(`[AgentSession] Background task finished: ${event.taskId} (${event.status ?? "?"}) \u2014 ${session.completion.pendingTaskCount} pending in ${sessionId}`);
230536
230770
  break;
230537
230771
  // Authoritative running-task snapshot from the CLI — resyncs the ledger
230538
230772
  // so add/delete drift in the started/finished pairs can't accumulate.
230539
230773
  case "task_list_changed":
230540
- this.applyCompletionTimerAction(session, session.completion.taskListChanged(event.taskIds));
230541
- console.log(`[AgentSession] Background task snapshot: [${event.taskIds.join(", ")}] in ${sessionId}`);
230774
+ this.applyCompletionTimerAction(session, session.completion.taskListChanged(event.tasks, timestamp));
230775
+ this.broadcastBackgroundTasks(session);
230776
+ console.log(`[AgentSession] Background task snapshot: [${event.tasks.map((t) => t.taskId).join(", ")}] in ${sessionId}`);
230542
230777
  break;
230543
230778
  // Handled above (cancels a grace-held completion); no store entry.
230544
230779
  case "turn_started":
@@ -230599,7 +230834,7 @@ var AgentSessionManager = class {
230599
230834
  cost_usd: event.cost_usd,
230600
230835
  input_tokens: event.input_tokens,
230601
230836
  output_tokens: event.output_tokens
230602
- });
230837
+ }, timestamp);
230603
230838
  if (action.kind === "commit") {
230604
230839
  await this.commitCompletion(session, action.payload);
230605
230840
  } else {
@@ -230609,6 +230844,7 @@ var AgentSessionManager = class {
230609
230844
  console.log(`[AgentSession] result after background-task activity \u2014 holding completion for ${this.completionGraceMs}ms grace (session=${sessionId})`);
230610
230845
  }
230611
230846
  this.applyCompletionTimerAction(session, action);
230847
+ this.broadcastBackgroundTasks(session);
230612
230848
  }
230613
230849
  }
230614
230850
  break;
@@ -230941,6 +231177,8 @@ var AgentSessionManager = class {
230941
231177
  ws.send(JSON.stringify({ Ready: true, historyEpoch: session.historyEpoch }));
230942
231178
  const statusPatch = ConversationPatch.updateStatus(session.status);
230943
231179
  ws.send(JSON.stringify({ JsonPatch: statusPatch }));
231180
+ const tasksMsg = this.backgroundTasksMessage(session);
231181
+ ws.send(JSON.stringify(tasksMsg));
230944
231182
  return () => {
230945
231183
  session.subscribers.delete(ws);
230946
231184
  };
@@ -231228,6 +231466,7 @@ var AgentSessionManager = class {
231228
231466
  session.process = null;
231229
231467
  this.killProcess(proc);
231230
231468
  this.emitProcessAlive(session, false);
231469
+ this.resetCompletion(session);
231231
231470
  if (!session.skipDb) {
231232
231471
  await this.storage.agentSessions.deleteEntries(sessionId);
231233
231472
  session.historyEpoch = await this.storage.agentSessions.incrementHistoryEpoch(sessionId);
@@ -231738,8 +231977,9 @@ var AgentSessionManager = class {
231738
231977
  permissionMode,
231739
231978
  agentType: dbSession.agent_type || "claude-code",
231740
231979
  model: dbSession.model ?? null,
231741
- completion: new TurnCompletionLedger(),
231980
+ completion: new TurnCompletionLedger(this.parkTimeoutMs),
231742
231981
  graceTimer: null,
231982
+ parkTimer: null,
231743
231983
  eventChain: Promise.resolve(),
231744
231984
  bgSpawnHintsThisTurn: 0,
231745
231985
  taskStartedThisTurn: 0,
@@ -231930,8 +232170,9 @@ var AgentSessionManager = class {
231930
232170
  permissionMode,
231931
232171
  agentType,
231932
232172
  model,
231933
- completion: new TurnCompletionLedger(),
232173
+ completion: new TurnCompletionLedger(this.parkTimeoutMs),
231934
232174
  graceTimer: null,
232175
+ parkTimer: null,
231935
232176
  eventChain: Promise.resolve(),
231936
232177
  bgSpawnHintsThisTurn: 0,
231937
232178
  taskStartedThisTurn: 0,
@@ -233597,6 +233838,9 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
233597
233838
  );
233598
233839
  }
233599
233840
  }
233841
+ } else if ("backgroundTasks" in parsed) {
233842
+ cache2.setBackgroundTasks(sessionId, raw);
233843
+ cache2.broadcast(sessionId, raw);
233600
233844
  } else if ("error" in parsed) {
233601
233845
  cache2.setSessionStatus(sessionId, "error");
233602
233846
  cache2.appendMessage(sessionId, raw, false);
@@ -239414,6 +239658,8 @@ function selfReportSection(report) {
239414
239658
  ].join("\n");
239415
239659
  }
239416
239660
  var VERDICT_INSTRUCTIONS = [
239661
+ "\nThe bar for blocking: a real defect that is worth fixing \u2014 wrong behavior, a case a user or caller will actually hit, a security or data-loss risk, or a missing test for logic that matters. Report those plainly; do not soften a real problem because the fix is inconvenient.",
239662
+ "Not blocking: over-engineering \u2014 speculative hardening, defenses against inputs this code cannot receive, abstractions or configurability for cases nobody has asked for, or a rewrite in your preferred style. When the fix would add more complexity than the problem it prevents is worth, it is a non-blocking note at most.",
239417
239663
  "\nEnd your final message with:",
239418
239664
  "1. Verdict \u2014 exactly one of: ship / needs-changes / cannot-verify. Use cannot-verify when you could not gather enough evidence to judge, rather than guessing.",
239419
239665
  "2. Blocking findings \u2014 what must change before shipping, each specific and actionable (say explicitly when there are none).",
@@ -240101,7 +240347,8 @@ var RemotePatchCache = class {
240101
240347
  latestEntryIndex: null,
240102
240348
  lastTurnEndEntryIndex: null,
240103
240349
  coverage: null,
240104
- sessionStatus: null
240350
+ sessionStatus: null,
240351
+ backgroundTasks: null
240105
240352
  };
240106
240353
  this.cache.set(sessionId, entry);
240107
240354
  }
@@ -240149,6 +240396,7 @@ var RemotePatchCache = class {
240149
240396
  if (metadata.lastTurnEnd !== null) lastTurnEndEntryIndex = Math.max(lastTurnEndEntryIndex ?? -1, metadata.lastTurnEnd);
240150
240397
  }
240151
240398
  const sessionStatus = existing?.sessionStatus ?? null;
240399
+ const backgroundTasks = existing?.backgroundTasks ?? null;
240152
240400
  const coverage = existing?.coverage ?? null;
240153
240401
  this.cache.set(sessionId, {
240154
240402
  messages,
@@ -240164,7 +240412,8 @@ var RemotePatchCache = class {
240164
240412
  latestEntryIndex,
240165
240413
  lastTurnEndEntryIndex,
240166
240414
  coverage,
240167
- sessionStatus
240415
+ sessionStatus,
240416
+ backgroundTasks
240168
240417
  });
240169
240418
  }
240170
240419
  /**
@@ -240229,6 +240478,7 @@ var RemotePatchCache = class {
240229
240478
  entry.historyEpoch = epoch;
240230
240479
  entry.latestEntryIndex = null;
240231
240480
  entry.lastTurnEndEntryIndex = null;
240481
+ entry.backgroundTasks = null;
240232
240482
  entry.coverage = { epoch, start: 0 };
240233
240483
  }
240234
240484
  setLastTurnEndEntryIndex(sessionId, index) {
@@ -240237,6 +240487,10 @@ var RemotePatchCache = class {
240237
240487
  setSessionStatus(sessionId, status) {
240238
240488
  this.getOrCreate(sessionId).sessionStatus = status;
240239
240489
  }
240490
+ /** Last-value store for the live background-task snapshot (see CacheEntry). */
240491
+ setBackgroundTasks(sessionId, raw) {
240492
+ this.getOrCreate(sessionId).backgroundTasks = raw;
240493
+ }
240240
240494
  /** Store a persistent remote WebSocket connection. */
240241
240495
  setRemoteWs(sessionId, ws) {
240242
240496
  const entry = this.getOrCreate(sessionId);
@@ -243265,6 +243519,13 @@ var sharedServices = async (fastify2, opts) => {
243265
243519
  reverseConnectManager.setStatusChangeHandler((remoteServerId, status) => {
243266
243520
  void (async () => {
243267
243521
  await opts.storage.remoteServers.updateStatus(remoteServerId, status);
243522
+ try {
243523
+ for (const projectId of await opts.storage.projectRemotes.listProjectIdsByServer(remoteServerId)) {
243524
+ eventBus.emit({ type: "remote-server:status", projectId, remoteServerId, status });
243525
+ }
243526
+ } catch (err) {
243527
+ console.error(`[SharedServices] remote-server:status fan-out failed for ${remoteServerId}:`, err);
243528
+ }
243268
243529
  if (status === "online") {
243269
243530
  const machineId = reverseConnectManager.getMachineId(remoteServerId);
243270
243531
  await restoreRemoteExecutorsForServer(remoteServerId, machineId);
@@ -248671,6 +248932,68 @@ var routes11 = async (fastify2) => {
248671
248932
  });
248672
248933
  }
248673
248934
  );
248935
+ fastify2.post(
248936
+ "/api/agent-sessions/:sessionId/background-tasks/:taskId/keep",
248937
+ async (req, reply) => {
248938
+ const userId = requireUserFacingUserId(req, reply);
248939
+ if (userId === null) return;
248940
+ const { sessionId, taskId } = req.params;
248941
+ if (sessionId.startsWith("remote-")) {
248942
+ const remoteInfo = await getAuthorizedRemoteSessionInfo(sessionId, userId);
248943
+ if (!remoteInfo) {
248944
+ return reply.code(404).send({ error: "Remote session not found" });
248945
+ }
248946
+ const result = await proxyAuto(
248947
+ remoteInfo.remoteServerId,
248948
+ "POST",
248949
+ `/api/agent-sessions/${remoteInfo.remoteSessionId}/background-tasks/${encodeURIComponent(taskId)}/keep`,
248950
+ {}
248951
+ );
248952
+ return reply.code(proxyStatus(result)).send(result.data);
248953
+ }
248954
+ const row = await fastify2.storage.agentSessions.getById(sessionId);
248955
+ if (!row || !await fastify2.storage.projects.getById(row.project_id, userId)) {
248956
+ return reply.code(404).send({ error: "Session not found" });
248957
+ }
248958
+ if (!fastify2.agentSessionManager.sanctionBackgroundTask(sessionId, taskId)) {
248959
+ return reply.code(404).send({ error: "Session not running" });
248960
+ }
248961
+ return reply.code(200).send({ success: true });
248962
+ }
248963
+ );
248964
+ fastify2.post(
248965
+ "/api/agent-sessions/:sessionId/background-tasks/:taskId/stop",
248966
+ async (req, reply) => {
248967
+ const userId = requireUserFacingUserId(req, reply);
248968
+ if (userId === null) return;
248969
+ const { sessionId, taskId } = req.params;
248970
+ if (sessionId.startsWith("remote-")) {
248971
+ const remoteInfo = await getAuthorizedRemoteSessionInfo(sessionId, userId);
248972
+ if (!remoteInfo) {
248973
+ return reply.code(404).send({ error: "Remote session not found" });
248974
+ }
248975
+ const result = await proxyAuto(
248976
+ remoteInfo.remoteServerId,
248977
+ "POST",
248978
+ `/api/agent-sessions/${remoteInfo.remoteSessionId}/background-tasks/${encodeURIComponent(taskId)}/stop`,
248979
+ {}
248980
+ );
248981
+ return reply.code(proxyStatus(result)).send(result.data);
248982
+ }
248983
+ const row = await fastify2.storage.agentSessions.getById(sessionId);
248984
+ if (!row || !await fastify2.storage.projects.getById(row.project_id, userId)) {
248985
+ return reply.code(404).send({ error: "Session not found" });
248986
+ }
248987
+ const outcome = fastify2.agentSessionManager.stopBackgroundTask(sessionId, taskId);
248988
+ if (outcome === "not_found") {
248989
+ return reply.code(404).send({ error: "Session not running" });
248990
+ }
248991
+ if (outcome === "unsupported") {
248992
+ return reply.code(501).send({ error: "This agent cannot stop a single background task \u2014 stop the session instead" });
248993
+ }
248994
+ return reply.code(200).send({ success: true });
248995
+ }
248996
+ );
248674
248997
  fastify2.post(
248675
248998
  "/api/agent-sessions/:sessionId/branch",
248676
248999
  async (req, reply) => {
@@ -251688,6 +252011,11 @@ var routes23 = async (fastify2) => {
251688
252011
  try {
251689
252012
  const message = JSON.parse(data.toString());
251690
252013
  if (message.type === "input" || message.type === "resize") {
252014
+ if (message.type === "resize") {
252015
+ console.log(
252016
+ `[WebSocket] resize ${processId} \u2192 ${message.cols}x${message.rows} ip=${req.ip} ua=${req.headers["user-agent"] ?? "?"}`
252017
+ );
252018
+ }
251691
252019
  handle.handleInput(message);
251692
252020
  }
251693
252021
  } catch (error48) {
@@ -251763,6 +252091,9 @@ var routes23 = async (fastify2) => {
251763
252091
  } else if (msg.type === "input") {
251764
252092
  handleInputMap.get(msg.processId)?.({ type: "input", data: msg.data });
251765
252093
  } else if (msg.type === "resize") {
252094
+ console.log(
252095
+ `[ExecutorMux] resize ${msg.processId} \u2192 ${msg.cols}x${msg.rows} ip=${req.ip} ua=${req.headers["user-agent"] ?? "?"}`
252096
+ );
251766
252097
  handleInputMap.get(msg.processId)?.({ type: "resize", cols: msg.cols, rows: msg.rows });
251767
252098
  }
251768
252099
  } catch (error48) {
@@ -251877,6 +252208,12 @@ var routes23 = async (fastify2) => {
251877
252208
  socket.send(JSON.stringify({ Ready: true, historyEpoch: cacheEntry.historyEpoch ?? void 0 }));
251878
252209
  } catch {
251879
252210
  }
252211
+ if (cacheEntry.backgroundTasks !== null) {
252212
+ try {
252213
+ socket.send(cacheEntry.backgroundTasks);
252214
+ } catch {
252215
+ }
252216
+ }
251880
252217
  if (cacheEntry.finished) {
251881
252218
  try {
251882
252219
  socket.send(JSON.stringify({ finished: true }));
@@ -252264,6 +252601,11 @@ var routes25 = async (fastify2) => {
252264
252601
  "Access-Control-Allow-Origin": "*"
252265
252602
  });
252266
252603
  reply.raw.write(":ok\n\n");
252604
+ reply.raw.write(
252605
+ `data: ${JSON.stringify({ type: "hello", ...fastify2.uiBuildId ? { uiBuildId: fastify2.uiBuildId } : {} })}
252606
+
252607
+ `
252608
+ );
252267
252609
  const unsubscribe = fastify2.eventBus.subscribe((event) => {
252268
252610
  void (async () => {
252269
252611
  if (userId !== null && !await fastify2.storage.projects.getById(event.projectId, userId)) {
@@ -258633,6 +258975,8 @@ var REMOTE_ID_PROP = {
258633
258975
  };
258634
258976
  var CROSS_REMOTE_MCP_INSTRUCTIONS = [
258635
258977
  "Use these tools when the task requires inspecting or operating another remote machine, or using an MCP server reachable from that remote.",
258978
+ "Treat a machine or host name in the user's request (for example, 'look at the ubuntu machine') as an explicit target signal, not as a request to inspect the current local workspace. Call `list_accessible_remotes` and match the user's wording against the returned remote names and ids before reading files or running local commands.",
258979
+ "If exactly one accessible remote matches the named machine, perform the requested work on that remote. If multiple remotes match, or the wording could reasonably refer to either the local machine or a remote, ask the user which target they mean before operating. If no remote matches, say so instead of silently falling back to local.",
258636
258980
  "Cross-remote can discover accessible machines, inspect files, directories, paths, and processes, run commands on exec-tier remotes, and persistently use MCP servers reachable from those remotes. Available operations depend on the remote's access tier, online state, and worker capabilities.",
258637
258981
  "Call `list_accessible_remotes` first to discover the remote id, access tier, online state, and whether its MCP broker is supported.",
258638
258982
  "For a remote MCP server, call `remote_mcp_open` once, use the returned tool schemas and handle for repeated `remote_mcp_call` calls, then call `remote_mcp_close` when the work is complete. Do not reopen the MCP server for every tool call.",
@@ -258643,7 +258987,7 @@ var CROSS_REMOTE_MCP_INSTRUCTIONS = [
258643
258987
  var TOOLS = [
258644
258988
  {
258645
258989
  name: "list_accessible_remotes",
258646
- description: "List the remote machines this agent may access, with their access tier and online status.",
258990
+ description: "List remote machines this agent may access, including their names, ids, access tiers, and online status. Call this before acting whenever the user names or otherwise identifies a machine/host, so the request is routed to the intended target instead of the current local machine.",
258647
258991
  inputSchema: { type: "object", properties: {}, additionalProperties: false }
258648
258992
  },
258649
258993
  {
@@ -259996,6 +260340,12 @@ function registerTraceContext(server) {
259996
260340
 
259997
260341
  // src/server.ts
259998
260342
  var API_KEY2 = process.env.VIBEDECKX_API_KEY || void 0;
260343
+ function staticCacheControl(filePath) {
260344
+ const p2 = filePath.replace(/\\/g, "/");
260345
+ if (p2.includes("/_next/static/")) return "public, max-age=31536000, immutable";
260346
+ if (p2.endsWith(".html") || p2.endsWith(".txt")) return "no-cache";
260347
+ return "public, max-age=86400";
260348
+ }
259999
260349
  function requireAuth(req, reply) {
260000
260350
  const server = req.server;
260001
260351
  if (!server.authEnabled) return void 0;
@@ -260045,6 +260395,14 @@ var createServer = async (opts) => {
260045
260395
  "./ui"
260046
260396
  );
260047
260397
  const UI_ROOT = opts.uiRoot !== void 0 ? opts.uiRoot : fs5.existsSync(bakedUiRoot) ? bakedUiRoot : null;
260398
+ let uiBuildId;
260399
+ if (UI_ROOT) {
260400
+ try {
260401
+ const parsed = JSON.parse(fs5.readFileSync(path19.join(UI_ROOT, "build-id.json"), "utf8"));
260402
+ if (typeof parsed.buildId === "string" && parsed.buildId) uiBuildId = parsed.buildId;
260403
+ } catch {
260404
+ }
260405
+ }
260048
260406
  const server = (0, import_fastify.default)({
260049
260407
  maxParamLength: 500,
260050
260408
  bodyLimit: 16 * 1024 * 1024,
@@ -260071,6 +260429,7 @@ var createServer = async (opts) => {
260071
260429
  console.log(`[WS-RAW] HTTP upgrade event: ${redactUrlForLog(req.url)}`);
260072
260430
  });
260073
260431
  server.decorate("authEnabled", authEnabled);
260432
+ server.decorate("uiBuildId", uiBuildId);
260074
260433
  server.decorate("noLocalProjects", noLocalProjects);
260075
260434
  registerTraceContext(server);
260076
260435
  server.addHook("onRequest", (req, reply, done) => {
@@ -260185,7 +260544,14 @@ var createServer = async (opts) => {
260185
260544
  if (UI_ROOT) {
260186
260545
  server.register(import_static.fastifyStatic, {
260187
260546
  root: UI_ROOT,
260188
- wildcard: false
260547
+ wildcard: false,
260548
+ // Cache policy is ours, not send's default (`public, max-age=0`): with no
260549
+ // origin Cache-Control, Cloudflare was stamping a 4h TTL on every asset
260550
+ // and every reload re-downloaded ~1.2MB of content-hashed chunks.
260551
+ cacheControl: false,
260552
+ setHeaders: (res, filePath) => {
260553
+ res.setHeader("Cache-Control", staticCacheControl(filePath));
260554
+ }
260189
260555
  });
260190
260556
  }
260191
260557
  server.addHook("onError", (req, _reply, error48, done) => {
@@ -260415,6 +260781,12 @@ var WORKER_CAPABILITIES = {
260415
260781
  "http:POST /api/agent-sessions/:param/restart": { since: "0.2.0", summary: "\u91CD\u542F\u4F1A\u8BDD\u8FDB\u7A0B" },
260416
260782
  "http:POST /api/agent-sessions/:param/agent-type": { since: "0.2.0", summary: "\u5207\u6362 agent \u7C7B\u578B" },
260417
260783
  "http:POST /api/agent-sessions/:param/model": { since: "0.2.0", summary: "\u5207\u6362\u4F1A\u8BDD\u6A21\u578B" },
260784
+ // Additive: an older worker 404s it, and the UI degrades by hiding the
260785
+ // "keep running" button — that worker has no park deadline to defuse.
260786
+ "http:POST /api/agent-sessions/:param/background-tasks/:param/keep": { since: "0.3.28", summary: "\u4E3A\u540E\u53F0\u4EFB\u52A1\u80CC\u4E66,\u514D\u4E8E\u8D85\u65F6\u6536\u5C3E" },
260787
+ // Additive alongside /keep, and degrades the same way: an older worker 404s
260788
+ // and the UI hides the button.
260789
+ "http:POST /api/agent-sessions/:param/background-tasks/:param/stop": { since: "0.3.28", summary: "\u505C\u6B62\u5355\u4E2A\u540E\u53F0\u4EFB\u52A1" },
260418
260790
  "http:POST /api/path/agent-sessions/:param/branch": { since: "0.2.0", summary: "\u4ECE\u5386\u53F2\u5206\u53C9\u4F1A\u8BDD" },
260419
260791
  "http:POST /api/agent-sessions/:param/switch-mode": { since: "0.2.0", summary: "\u6743\u9650\u6A21\u5F0F\u5207\u6362" },
260420
260792
  "http:POST /api/agent-sessions/:param/accept-plan": { since: "0.2.0", summary: "\u63A5\u53D7\u8BA1\u5212(\u9000\u51FA plan \u6A21\u5F0F)" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedeckx/linux-x64",
3
- "version": "0.3.26",
3
+ "version": "0.3.28",
4
4
  "description": "Vibedeckx platform binaries for Linux x64",
5
5
  "os": [
6
6
  "linux"