@vibedeckx/linux-x64 0.3.27 → 0.3.29

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 +411 -62
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -186844,7 +186844,9 @@ var mapAgentSession = (row) => ({
186844
186844
  last_completed_at: row.last_completed_at,
186845
186845
  favorited_at: row.favorited_at,
186846
186846
  native_session_id: row.native_session_id,
186847
- history_epoch: row.history_epoch
186847
+ history_epoch: row.history_epoch,
186848
+ branched_from_session_id: row.branched_from_session_id,
186849
+ branched_from_entry_index: row.branched_from_entry_index
186848
186850
  });
186849
186851
  var parseActivityTimestamp = (value) => {
186850
186852
  const explicitZone = /(?:Z|[+-]\d\d:\d\d)$/i.test(value);
@@ -187007,6 +187009,7 @@ var mapRemoteCreationIntent = (row) => ({
187007
187009
  var mapRemoteReviewerCreationIntent = (row) => ({
187008
187010
  ...row,
187009
187011
  review_span: row.review_span,
187012
+ review_context_mode: row.review_context_mode ?? null,
187010
187013
  status: row.status
187011
187014
  });
187012
187015
  var nowActivityAt = () => sql`cast((julianday('now') - 2440587.5) * 86400000 as integer)`;
@@ -187231,6 +187234,9 @@ var createAgentSessionRepos = (kdb, h) => ({
187231
187234
  // Toggle favorite without touching updated_at — favoriting is a passive
187232
187235
  // bookmark, not a "this session was active" signal, so it must not
187233
187236
  // disturb the dropdown's recency ordering.
187237
+ setBranchedFrom: async (id, sourceSessionId, entryIndex) => {
187238
+ await kdb.updateTable("agent_sessions").set({ branched_from_session_id: sourceSessionId, branched_from_entry_index: entryIndex }).where("id", "=", id).execute();
187239
+ },
187234
187240
  setFavorited: async (id, favorited) => {
187235
187241
  await kdb.updateTable("agent_sessions").set({ favorited_at: favorited ? Date.now() : null }).where("id", "=", id).execute();
187236
187242
  },
@@ -187549,6 +187555,7 @@ var createAgentSessionRepos = (kdb, h) => ({
187549
187555
  review_focus: intent.reviewFocus ?? null,
187550
187556
  source_turn_end_index: intent.sourceTurnEndIndex ?? null,
187551
187557
  review_span: intent.reviewSpan,
187558
+ review_context_mode: intent.reviewContextMode ?? null,
187552
187559
  agent_type: intent.agentType,
187553
187560
  intent_brief: intent.intentBrief ?? null,
187554
187561
  user_id: intent.userId ?? null,
@@ -187558,7 +187565,7 @@ var createAgentSessionRepos = (kdb, h) => ({
187558
187565
  updated_at: h.nowMs()
187559
187566
  }).onConflict((oc) => oc.column("local_reviewer_session_id").doNothing()).execute();
187560
187567
  const row = await trx.selectFrom("remote_reviewer_creation_intents").selectAll().where("local_reviewer_session_id", "=", intent.localReviewerSessionId).executeTakeFirstOrThrow();
187561
- const sameIdentity = row.remote_reviewer_session_id === intent.remoteReviewerSessionId && row.remote_run_id === intent.remoteRunId && row.project_id === intent.projectId && row.remote_server_id === intent.remoteServerId && (row.branch ?? "") === (intent.branch ?? "") && row.remote_path === intent.remotePath && row.source_remote_session_id === intent.sourceRemoteSessionId && row.review_focus === (intent.reviewFocus ?? null) && row.source_turn_end_index === (intent.sourceTurnEndIndex ?? null) && row.review_span === intent.reviewSpan && row.agent_type === intent.agentType && row.intent_brief === (intent.intentBrief ?? null) && row.user_id === (intent.userId ?? null);
187568
+ const sameIdentity = row.remote_reviewer_session_id === intent.remoteReviewerSessionId && row.remote_run_id === intent.remoteRunId && row.project_id === intent.projectId && row.remote_server_id === intent.remoteServerId && (row.branch ?? "") === (intent.branch ?? "") && row.remote_path === intent.remotePath && row.source_remote_session_id === intent.sourceRemoteSessionId && row.review_focus === (intent.reviewFocus ?? null) && row.source_turn_end_index === (intent.sourceTurnEndIndex ?? null) && row.review_span === intent.reviewSpan && (row.review_context_mode ?? null) === (intent.reviewContextMode ?? null) && row.agent_type === intent.agentType && row.intent_brief === (intent.intentBrief ?? null) && row.user_id === (intent.userId ?? null);
187562
187569
  if (!sameIdentity) {
187563
187570
  throw new Error(`Remote reviewer creation intent ${intent.localReviewerSessionId} has conflicting identity`);
187564
187571
  }
@@ -203758,6 +203765,8 @@ var tightenWorkspaceCheckoutForeignKeys = (db) => {
203758
203765
  favorited_at INTEGER DEFAULT NULL,
203759
203766
  native_session_id TEXT DEFAULT NULL,
203760
203767
  history_epoch INTEGER NOT NULL DEFAULT 0,
203768
+ branched_from_session_id TEXT DEFAULT NULL,
203769
+ branched_from_entry_index INTEGER DEFAULT NULL,
203761
203770
  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
203762
203771
  FOREIGN KEY (workspace_checkout_id) REFERENCES workspace_checkouts(id)
203763
203772
  DEFERRABLE INITIALLY DEFERRED
@@ -203765,10 +203774,12 @@ var tightenWorkspaceCheckoutForeignKeys = (db) => {
203765
203774
  INSERT INTO agent_sessions_fk_new
203766
203775
  (id, project_id, branch, workspace_checkout_id, status, permission_mode, agent_type,
203767
203776
  title, model, created_at, updated_at, activity_at, last_user_message_at,
203768
- last_completed_at, favorited_at, native_session_id, history_epoch)
203777
+ last_completed_at, favorited_at, native_session_id, history_epoch,
203778
+ branched_from_session_id, branched_from_entry_index)
203769
203779
  SELECT id, project_id, branch, workspace_checkout_id, status, permission_mode, agent_type,
203770
203780
  title, model, created_at, updated_at, activity_at, last_user_message_at,
203771
- last_completed_at, favorited_at, native_session_id, history_epoch
203781
+ last_completed_at, favorited_at, native_session_id, history_epoch,
203782
+ branched_from_session_id, branched_from_entry_index
203772
203783
  FROM agent_sessions;
203773
203784
  DROP TABLE agent_sessions;
203774
203785
  ALTER TABLE agent_sessions_fk_new RENAME TO agent_sessions;
@@ -204179,6 +204190,7 @@ var initializeSchema = (db) => {
204179
204190
  review_focus TEXT,
204180
204191
  source_turn_end_index INTEGER,
204181
204192
  review_span TEXT NOT NULL CHECK (review_span IN ('this_turn', 'session_start')),
204193
+ review_context_mode TEXT CHECK (review_context_mode IN ('briefed', 'blind')),
204182
204194
  agent_type TEXT NOT NULL,
204183
204195
  intent_brief TEXT,
204184
204196
  user_id TEXT,
@@ -204595,6 +204607,11 @@ var initializeSchema = (db) => {
204595
204607
  coalesce(cast((julianday(created_at) - 2440587.5) * 86400000 as integer), 0)
204596
204608
  )`);
204597
204609
  }
204610
+ const sessionBranchedFromInfo = db.prepare("PRAGMA table_info(agent_sessions)").all();
204611
+ if (!sessionBranchedFromInfo.some((col) => col.name === "branched_from_session_id")) {
204612
+ db.exec("ALTER TABLE agent_sessions ADD COLUMN branched_from_session_id TEXT DEFAULT NULL");
204613
+ db.exec("ALTER TABLE agent_sessions ADD COLUMN branched_from_entry_index INTEGER DEFAULT NULL");
204614
+ }
204598
204615
  db.exec(`
204599
204616
  CREATE INDEX IF NOT EXISTS idx_agent_sessions_project_branch
204600
204617
  ON agent_sessions(project_id, branch);
@@ -205344,6 +205361,10 @@ var initializeSchema = (db) => {
205344
205361
  if (!workflowRunsInfo.some((col) => col.name === "review_span")) {
205345
205362
  db.exec("ALTER TABLE workflow_runs ADD COLUMN review_span TEXT NOT NULL DEFAULT 'this_turn'");
205346
205363
  }
205364
+ const reviewerIntentsInfo = db.prepare("PRAGMA table_info(remote_reviewer_creation_intents)").all();
205365
+ if (!reviewerIntentsInfo.some((col) => col.name === "review_context_mode")) {
205366
+ db.exec("ALTER TABLE remote_reviewer_creation_intents ADD COLUMN review_context_mode TEXT CHECK (review_context_mode IN ('briefed', 'blind'))");
205367
+ }
205347
205368
  db.exec(`
205348
205369
  CREATE TABLE IF NOT EXISTS user_settings (
205349
205370
  user_id TEXT NOT NULL,
@@ -207173,7 +207194,7 @@ function sessionMilestoneForTurnEnd(opts) {
207173
207194
  workflow_run_id: null,
207174
207195
  created_at: opts.createdAt
207175
207196
  };
207176
- if (opts.outcome === "completed") {
207197
+ if (opts.outcome === "completed" || opts.outcome === "completed_with_pending_tasks") {
207177
207198
  return {
207178
207199
  ...base,
207179
207200
  id: sessionResultReadyId(opts.sessionId, opts.entryIndex),
@@ -207312,6 +207333,25 @@ var ClaudeCodeProvider = class {
207312
207333
  formatUserInput(content, _sessionId) {
207313
207334
  return serializeUserInput(content);
207314
207335
  }
207336
+ /**
207337
+ * `stop_task` control_request — the documented Agent SDK primitive for
207338
+ * killing one background task, verified live against claude 2.1.238: the CLI
207339
+ * answers `control_response { subtype: "success" }`, empties its task list
207340
+ * and the process really dies.
207341
+ *
207342
+ * `request_id` is namespaced by task id so a response could be correlated;
207343
+ * nothing reads control_response today, and it does not need to — the
207344
+ * authoritative `background_tasks_changed` snapshot that follows is what
207345
+ * updates the ledger. An older CLI that does not know the subtype answers an
207346
+ * error and simply changes nothing.
207347
+ */
207348
+ formatStopBackgroundTask(taskId, _sessionId) {
207349
+ return JSON.stringify({
207350
+ type: "control_request",
207351
+ request_id: `stop_task-${taskId}`,
207352
+ request: { subtype: "stop_task", task_id: taskId }
207353
+ }) + "\n";
207354
+ }
207315
207355
  // Lifecycle hooks are no-ops for Claude (stateless per-session)
207316
207356
  onSessionCreated(_sessionId, _permissionMode) {
207317
207357
  }
@@ -229427,7 +229467,7 @@ function isResidentProcessInScope(candidate, scope) {
229427
229467
  return candidate.projectId === scope.projectId && candidate.branch === scope.branch;
229428
229468
  }
229429
229469
  function isIdleResidentProcess(candidate) {
229430
- return candidate.processAlive && !candidate.dormant && candidate.status !== "running" && candidate.backgroundTaskCount === 0;
229470
+ return candidate.processAlive && !candidate.dormant && candidate.status !== "running" && !candidate.backgroundTasksProtect;
229431
229471
  }
229432
229472
  function pickIdleResidentEvictionCandidate(candidates, scope) {
229433
229473
  return candidates.filter((candidate) => !scope || isResidentProcessInScope(candidate, scope)).filter(isIdleResidentProcess).sort((a, b2) => a.lastActiveAt - b2.lastActiveAt)[0] ?? null;
@@ -229449,7 +229489,12 @@ var ResidentProcessLimitError = class extends Error {
229449
229489
 
229450
229490
  // src/turn-completion.ts
229451
229491
  var COMPLETION_GRACE_MS = 1500;
229492
+ var PARK_TIMEOUT_MS = 20 * 60 * 1e3;
229452
229493
  var TurnCompletionLedger = class {
229494
+ constructor(parkTimeoutMs = PARK_TIMEOUT_MS) {
229495
+ this.parkTimeoutMs = parkTimeoutMs;
229496
+ }
229497
+ parkTimeoutMs;
229453
229498
  /** Live background tasks by harness task_id (same id may restart). */
229454
229499
  tasks = /* @__PURE__ */ new Map();
229455
229500
  /** Held completion candidate — the latest success result, if any. */
@@ -229462,6 +229507,23 @@ var TurnCompletionLedger = class {
229462
229507
  * grace delay (the common case).
229463
229508
  */
229464
229509
  sawBackgroundActivity = false;
229510
+ /**
229511
+ * When the held candidate was parked behind live background tasks — the
229512
+ * moment the agent stopped working and only tasks kept the turn open. The
229513
+ * park deadline counts from here, not from when a task started: the number
229514
+ * that matters to the user is "how long since the agent answered".
229515
+ */
229516
+ parkedSince = null;
229517
+ /** Task ids the user vouched for; they stop counting toward the deadline. */
229518
+ sanctioned = /* @__PURE__ */ new Set();
229519
+ /**
229520
+ * Whether a park deadline already expired and closed the turn. Live tasks
229521
+ * normally shield the session from being reclaimed — hibernating would kill
229522
+ * a real build and the auto-resume that reads it — but that shield must not
229523
+ * outlast the deadline, or one stuck task pins a resident process slot
229524
+ * forever and new sessions on the branch get turned away.
229525
+ */
229526
+ parkDeadlineExpired = false;
229465
229527
  get pendingTaskCount() {
229466
229528
  return this.tasks.size;
229467
229529
  }
@@ -229470,17 +229532,61 @@ var TurnCompletionLedger = class {
229470
229532
  }
229471
229533
  /** Live tasks in first-seen order — the payload the UI renders. */
229472
229534
  get backgroundTasks() {
229473
- return [...this.tasks.values()];
229535
+ return [...this.tasks.values()].map(
229536
+ (task) => this.sanctioned.has(task.taskId) ? { ...task, sanctioned: true } : task
229537
+ );
229538
+ }
229539
+ /**
229540
+ * When the parked turn will be committed anyway, or null if nothing is
229541
+ * parked or every live task has been vouched for. Timer-free by design: the
229542
+ * caller re-reads this after each mutation and syncs its own timer, so the
229543
+ * ledger stays a pure state machine.
229544
+ */
229545
+ get parkDeadlineAt() {
229546
+ if (this.pending === null || this.parkedSince === null) return null;
229547
+ const allVouchedFor = [...this.tasks.keys()].every((id) => this.sanctioned.has(id));
229548
+ return allVouchedFor ? null : this.parkedSince + this.parkTimeoutMs;
229549
+ }
229550
+ /**
229551
+ * The user vouched for a task: stop counting it toward the deadline. This
229552
+ * restores the original behavior for that task — wait for it, let the
229553
+ * auto-resume close the turn — but now as an explicit choice.
229554
+ */
229555
+ sanction(taskId) {
229556
+ if (this.tasks.has(taskId)) this.sanctioned.add(taskId);
229557
+ }
229558
+ /**
229559
+ * The deadline expired: commit the parked candidate. Deliberately uses the
229560
+ * ORIGINAL payload — its duration/cost/tokens describe the turn the agent
229561
+ * actually ran, not the time spent waiting on a stuck task.
229562
+ */
229563
+ parkDeadlineElapsed() {
229564
+ if (this.pending === null) return { kind: "none" };
229565
+ this.parkDeadlineExpired = true;
229566
+ return this.commitHeld();
229567
+ }
229568
+ /**
229569
+ * Whether live background tasks should still shield this session from
229570
+ * resident-process reclamation. True while they are plausibly doing real
229571
+ * work; false once the deadline judged them anomalous — unless the user
229572
+ * vouched for every one of them, which restores the shield along with the
229573
+ * waiting behavior it protects.
229574
+ */
229575
+ get backgroundTasksProtectSession() {
229576
+ if (this.tasks.size === 0) return false;
229577
+ if (!this.parkDeadlineExpired) return true;
229578
+ return [...this.tasks.keys()].every((id) => this.sanctioned.has(id));
229474
229579
  }
229475
229580
  taskStarted(task, now3) {
229476
229581
  this.upsert(task, this.tasks.get(task.taskId), now3);
229477
229582
  this.sawBackgroundActivity = true;
229478
- return this.rearmIfHeld();
229583
+ return this.rearmIfHeld(now3);
229479
229584
  }
229480
- taskFinished(taskId) {
229585
+ taskFinished(taskId, now3) {
229481
229586
  this.tasks.delete(taskId);
229587
+ this.sanctioned.delete(taskId);
229482
229588
  this.sawBackgroundActivity = true;
229483
- return this.rearmIfHeld();
229589
+ return this.rearmIfHeld(now3);
229484
229590
  }
229485
229591
  /** Authoritative snapshot from `system/background_tasks_changed`. */
229486
229592
  taskListChanged(tasks, now3) {
@@ -229489,8 +229595,11 @@ var TurnCompletionLedger = class {
229489
229595
  for (const task of tasks) {
229490
229596
  this.upsert(task, previous.get(task.taskId), now3);
229491
229597
  }
229598
+ for (const id of this.sanctioned) {
229599
+ if (!this.tasks.has(id)) this.sanctioned.delete(id);
229600
+ }
229492
229601
  if (tasks.length > 0) this.sawBackgroundActivity = true;
229493
- return this.rearmIfHeld();
229602
+ return this.rearmIfHeld(now3);
229494
229603
  }
229495
229604
  /**
229496
229605
  * The process emitted turn activity: if a completion was held, it was an
@@ -229502,8 +229611,10 @@ var TurnCompletionLedger = class {
229502
229611
  * the race against the grace window.
229503
229612
  */
229504
229613
  noteTurnActivity() {
229614
+ this.parkDeadlineExpired = false;
229505
229615
  if (this.pending === null) return { kind: "none" };
229506
229616
  this.pending = null;
229617
+ this.parkedSince = null;
229507
229618
  this.generation++;
229508
229619
  return { kind: "cancel" };
229509
229620
  }
@@ -229519,9 +229630,11 @@ var TurnCompletionLedger = class {
229519
229630
  this.sawBackgroundActivity = false;
229520
229631
  return this.noteTurnActivity();
229521
229632
  }
229522
- successResult(payload) {
229633
+ successResult(payload, now3) {
229523
229634
  this.generation++;
229524
229635
  if (this.tasks.size > 0) {
229636
+ this.parkedSince = now3;
229637
+ this.parkDeadlineExpired = false;
229525
229638
  this.pending = payload;
229526
229639
  return { kind: "cancel" };
229527
229640
  }
@@ -229535,6 +229648,7 @@ var TurnCompletionLedger = class {
229535
229648
  errorResult() {
229536
229649
  if (this.pending === null) return { kind: "none" };
229537
229650
  this.pending = null;
229651
+ this.parkedSince = null;
229538
229652
  this.generation++;
229539
229653
  return { kind: "cancel" };
229540
229654
  }
@@ -229560,7 +229674,10 @@ var TurnCompletionLedger = class {
229560
229674
  /** Full reset (fresh spawn / stop / hibernate / agent switch). */
229561
229675
  reset() {
229562
229676
  this.tasks.clear();
229677
+ this.sanctioned.clear();
229563
229678
  this.pending = null;
229679
+ this.parkedSince = null;
229680
+ this.parkDeadlineExpired = false;
229564
229681
  this.generation++;
229565
229682
  this.sawBackgroundActivity = false;
229566
229683
  }
@@ -229569,10 +229686,13 @@ var TurnCompletionLedger = class {
229569
229686
  * have no resume behind it), so they delay the commit rather than cancel
229570
229687
  * it — and while tasks are still live the candidate stays parked with no
229571
229688
  * timer at all (only an empty set can complete a turn). */
229572
- rearmIfHeld() {
229689
+ rearmIfHeld(now3) {
229573
229690
  if (this.pending === null) return { kind: "none" };
229574
229691
  this.generation++;
229575
- if (this.tasks.size > 0) return { kind: "cancel" };
229692
+ if (this.tasks.size > 0) {
229693
+ this.parkedSince ??= now3;
229694
+ return { kind: "cancel" };
229695
+ }
229576
229696
  return { kind: "schedule", generation: this.generation };
229577
229697
  }
229578
229698
  /**
@@ -229593,6 +229713,7 @@ var TurnCompletionLedger = class {
229593
229713
  commitHeld() {
229594
229714
  const payload = this.pending;
229595
229715
  this.pending = null;
229716
+ this.parkedSince = null;
229596
229717
  this.generation++;
229597
229718
  return { kind: "commit", payload };
229598
229719
  }
@@ -229673,10 +229794,13 @@ var AgentSessionManager = class {
229673
229794
  retentionDeleting = /* @__PURE__ */ new Set();
229674
229795
  /** Grace window before committing a held completion (injectable for tests). */
229675
229796
  completionGraceMs;
229797
+ /** Bound on a parked completion (injectable for tests). */
229798
+ parkTimeoutMs;
229676
229799
  workflowSuppressionCheck = null;
229677
229800
  constructor(storage2, opts) {
229678
229801
  this.storage = storage2;
229679
229802
  this.completionGraceMs = opts?.completionGraceMs ?? COMPLETION_GRACE_MS;
229803
+ this.parkTimeoutMs = opts?.parkTimeoutMs ?? PARK_TIMEOUT_MS;
229680
229804
  }
229681
229805
  async resolveSessionWorktreePath(session, legacyProjectPath) {
229682
229806
  if (!session.workspaceCheckoutId) {
@@ -229951,7 +230075,7 @@ var AgentSessionManager = class {
229951
230075
  processAlive: this.isProcessAlive(session),
229952
230076
  status: session.status,
229953
230077
  dormant: session.dormant,
229954
- backgroundTaskCount: session.completion.pendingTaskCount,
230078
+ backgroundTasksProtect: session.completion.backgroundTasksProtectSession,
229955
230079
  lastActiveAt: session.lastActiveAt,
229956
230080
  projectId: session.projectId,
229957
230081
  branch: session.branch
@@ -230099,8 +230223,9 @@ var AgentSessionManager = class {
230099
230223
  crossRemoteMcp: opts.crossRemoteMcp,
230100
230224
  agentType,
230101
230225
  model,
230102
- completion: new TurnCompletionLedger(),
230226
+ completion: new TurnCompletionLedger(this.parkTimeoutMs),
230103
230227
  graceTimer: null,
230228
+ parkTimer: null,
230104
230229
  eventChain: Promise.resolve(),
230105
230230
  bgSpawnHintsThisTurn: 0,
230106
230231
  taskStartedThisTurn: 0,
@@ -230357,11 +230482,78 @@ var AgentSessionManager = class {
230357
230482
  } else if (action.kind === "schedule") {
230358
230483
  this.armGraceTimer(session, action.generation);
230359
230484
  }
230485
+ this.syncParkTimer(session);
230486
+ }
230487
+ /**
230488
+ * Keep the park timer in step with the ledger's deadline.
230489
+ *
230490
+ * Driven by ledger STATE rather than by an action kind, because the deadline
230491
+ * survives across many actions (every task event returns `cancel` while a
230492
+ * completion stays parked) and can also be lifted without any action at all
230493
+ * when the user vouches for the last unvouched task. Re-reading the state
230494
+ * after each mutation is the only way the two can't drift.
230495
+ */
230496
+ syncParkTimer(session) {
230497
+ const deadlineAt = session.completion.parkDeadlineAt;
230498
+ if (deadlineAt === null) {
230499
+ if (session.parkTimer) {
230500
+ clearTimeout(session.parkTimer);
230501
+ session.parkTimer = null;
230502
+ }
230503
+ return;
230504
+ }
230505
+ if (session.parkTimer) return;
230506
+ const timer = setTimeout(() => {
230507
+ session.parkTimer = null;
230508
+ this.enqueueSessionWork(session, async () => {
230509
+ const action = session.completion.parkDeadlineElapsed();
230510
+ if (action.kind !== "commit") return;
230511
+ console.log(
230512
+ `[AgentSession] parked completion exceeded ${this.parkTimeoutMs}ms with ${session.completion.pendingTaskCount} background task(s) still running \u2014 committing the turn anyway (session=${session.id})`
230513
+ );
230514
+ await this.commitCompletion(session, action.payload, "completed_with_pending_tasks");
230515
+ this.broadcastBackgroundTasks(session);
230516
+ }, "completion-park-deadline");
230517
+ }, Math.max(0, deadlineAt - Date.now()));
230518
+ timer.unref?.();
230519
+ session.parkTimer = timer;
230520
+ }
230521
+ /**
230522
+ * The user vouched for a background task: it stops counting toward the park
230523
+ * deadline, restoring the original wait-for-auto-resume behavior for that
230524
+ * task alone — now as an explicit choice rather than a silent assumption.
230525
+ */
230526
+ /**
230527
+ * Ask the agent to stop one background task.
230528
+ *
230529
+ * Returns "unsupported" for agents with no such primitive (Codex), so the
230530
+ * caller can say "stop the session instead" rather than showing a dead
230531
+ * button. On success nothing is updated here: the CLI's own
230532
+ * `background_tasks_changed` snapshot drains the ledger, which then commits
230533
+ * the parked turn through the normal path.
230534
+ */
230535
+ stopBackgroundTask(sessionId, taskId) {
230536
+ const session = this.sessions.get(sessionId);
230537
+ if (!session?.process?.stdin) return "not_found";
230538
+ const frame = getProvider(session.agentType).formatStopBackgroundTask?.(taskId, sessionId);
230539
+ if (!frame) return "unsupported";
230540
+ session.process.stdin.write(frame);
230541
+ console.log(`[AgentSession] stop_task sent for ${taskId} (session=${sessionId})`);
230542
+ return "ok";
230543
+ }
230544
+ sanctionBackgroundTask(sessionId, taskId) {
230545
+ const session = this.sessions.get(sessionId);
230546
+ if (!session) return false;
230547
+ session.completion.sanction(taskId);
230548
+ this.syncParkTimer(session);
230549
+ this.broadcastBackgroundTasks(session);
230550
+ return true;
230360
230551
  }
230361
230552
  /** Discard all turn-completion state (fresh spawn / stop / hibernate / agent switch). */
230362
230553
  resetCompletion(session) {
230363
230554
  this.clearGraceTimer(session);
230364
230555
  session.completion.reset();
230556
+ this.syncParkTimer(session);
230365
230557
  this.broadcastBackgroundTasks(session);
230366
230558
  }
230367
230559
  /**
@@ -230376,21 +230568,32 @@ var AgentSessionManager = class {
230376
230568
  * stateless (no patch application, no ordering assumptions).
230377
230569
  */
230378
230570
  broadcastBackgroundTasks(session) {
230379
- this.broadcastRaw(session.id, {
230571
+ this.broadcastRaw(session.id, this.backgroundTasksMessage(session));
230572
+ }
230573
+ /**
230574
+ * Reported rather than inferred client-side: whether a single task can be
230575
+ * stopped is a property of the agent (Claude Code has `stop_task`, Codex has
230576
+ * nothing equivalent), and the server is the only side that knows. A client
230577
+ * guessing from the agent type would drift the day Codex gains one.
230578
+ */
230579
+ backgroundTasksMessage(session) {
230580
+ return {
230380
230581
  backgroundTasks: {
230381
230582
  tasks: session.completion.backgroundTasks,
230382
- turnParked: session.completion.hasPendingCompletion
230583
+ turnParked: session.completion.hasPendingCompletion,
230584
+ parkDeadlineAt: session.completion.parkDeadlineAt,
230585
+ canStopTasks: !!getProvider(session.agentType).formatStopBackgroundTask
230383
230586
  }
230384
- });
230587
+ };
230385
230588
  }
230386
- async commitCompletion(session, payload) {
230589
+ async commitCompletion(session, payload, outcome = "completed") {
230387
230590
  const sessionId = session.id;
230388
230591
  console.log(`[AgentSession] taskCompleted: sessionId=${sessionId}, eventBus=${!!this.eventBus}, projectId=${session.projectId}, branch=${session.branch}`);
230389
230592
  const completedAt = Date.now();
230390
230593
  if (!session.skipDb) {
230391
230594
  await this.storage.agentSessions.markCompleted(sessionId, completedAt);
230392
230595
  }
230393
- const turnEndEntryIndex = await this.endActiveTurn(session, "completed");
230596
+ const turnEndEntryIndex = await this.endActiveTurn(session, outcome);
230394
230597
  this.broadcastBackgroundTasks(session);
230395
230598
  const summaryText = extractLastAssistantText(session.store.entries);
230396
230599
  this.broadcastRaw(sessionId, {
@@ -230582,7 +230785,7 @@ var AgentSessionManager = class {
230582
230785
  console.log(`[AgentSession] Background task started: ${event.taskId} (${event.taskType ?? "?"}) \u2014 ${session.completion.pendingTaskCount} pending in ${sessionId}`);
230583
230786
  break;
230584
230787
  case "task_finished":
230585
- this.applyCompletionTimerAction(session, session.completion.taskFinished(event.taskId));
230788
+ this.applyCompletionTimerAction(session, session.completion.taskFinished(event.taskId, timestamp));
230586
230789
  this.broadcastBackgroundTasks(session);
230587
230790
  console.log(`[AgentSession] Background task finished: ${event.taskId} (${event.status ?? "?"}) \u2014 ${session.completion.pendingTaskCount} pending in ${sessionId}`);
230588
230791
  break;
@@ -230652,7 +230855,7 @@ var AgentSessionManager = class {
230652
230855
  cost_usd: event.cost_usd,
230653
230856
  input_tokens: event.input_tokens,
230654
230857
  output_tokens: event.output_tokens
230655
- });
230858
+ }, timestamp);
230656
230859
  if (action.kind === "commit") {
230657
230860
  await this.commitCompletion(session, action.payload);
230658
230861
  } else {
@@ -230983,6 +231186,7 @@ var AgentSessionManager = class {
230983
231186
  reset: opts.historyEpoch !== void 0 && opts.historyEpoch !== session.historyEpoch
230984
231187
  }
230985
231188
  }));
231189
+ ws.send(JSON.stringify(this.backgroundTasksMessage(session)));
230986
231190
  for (const patch of session.store.patches) {
230987
231191
  const entryIndices = patch.flatMap((op) => {
230988
231192
  const match2 = op.path.match(/^\/entries\/(\d+)$/);
@@ -230995,13 +231199,6 @@ var AgentSessionManager = class {
230995
231199
  ws.send(JSON.stringify({ Ready: true, historyEpoch: session.historyEpoch }));
230996
231200
  const statusPatch = ConversationPatch.updateStatus(session.status);
230997
231201
  ws.send(JSON.stringify({ JsonPatch: statusPatch }));
230998
- const tasksMsg = {
230999
- backgroundTasks: {
231000
- tasks: session.completion.backgroundTasks,
231001
- turnParked: session.completion.hasPendingCompletion
231002
- }
231003
- };
231004
- ws.send(JSON.stringify(tasksMsg));
231005
231202
  return () => {
231006
231203
  session.subscribers.delete(ws);
231007
231204
  };
@@ -231800,8 +231997,11 @@ var AgentSessionManager = class {
231800
231997
  permissionMode,
231801
231998
  agentType: dbSession.agent_type || "claude-code",
231802
231999
  model: dbSession.model ?? null,
231803
- completion: new TurnCompletionLedger(),
232000
+ branchedFromSessionId: dbSession.branched_from_session_id ?? null,
232001
+ branchedFromEntryIndex: dbSession.branched_from_entry_index ?? null,
232002
+ completion: new TurnCompletionLedger(this.parkTimeoutMs),
231804
232003
  graceTimer: null,
232004
+ parkTimer: null,
231805
232005
  eventChain: Promise.resolve(),
231806
232006
  bgSpawnHintsThisTurn: 0,
231807
232007
  taskStartedThisTurn: 0,
@@ -231925,6 +232125,14 @@ var AgentSessionManager = class {
231925
232125
  if (existingRuntime && opts.crossRemoteMcp) {
231926
232126
  existingRuntime.crossRemoteMcp = opts.crossRemoteMcp;
231927
232127
  }
232128
+ if (!existingBranch.branched_from_session_id) {
232129
+ const repairedEntryIndex = entryRows[entryRows.length - 1].entry_index;
232130
+ await this.storage.agentSessions.setBranchedFrom(newId, sourceSessionId, repairedEntryIndex);
232131
+ if (existingRuntime) {
232132
+ existingRuntime.branchedFromSessionId = sourceSessionId;
232133
+ existingRuntime.branchedFromEntryIndex = repairedEntryIndex;
232134
+ }
232135
+ }
231928
232136
  return { ok: true, sessionId: newId };
231929
232137
  }
231930
232138
  }
@@ -231958,6 +232166,8 @@ var AgentSessionManager = class {
231958
232166
  for (const row of entryRows) {
231959
232167
  await this.storage.agentSessions.upsertEntry(newId, row.entry_index, row.data);
231960
232168
  }
232169
+ const branchedFromEntryIndex = opts.upToEntryIndex ?? entryRows[entryRows.length - 1].entry_index;
232170
+ await this.storage.agentSessions.setBranchedFrom(newId, sourceSessionId, branchedFromEntryIndex);
231961
232171
  let baseTitle = sourceRow?.title ?? null;
231962
232172
  if (!baseTitle) {
231963
232173
  for (const row of entryRows) {
@@ -231992,15 +232202,18 @@ var AgentSessionManager = class {
231992
232202
  permissionMode,
231993
232203
  agentType,
231994
232204
  model,
231995
- completion: new TurnCompletionLedger(),
232205
+ completion: new TurnCompletionLedger(this.parkTimeoutMs),
231996
232206
  graceTimer: null,
232207
+ parkTimer: null,
231997
232208
  eventChain: Promise.resolve(),
231998
232209
  bgSpawnHintsThisTurn: 0,
231999
232210
  taskStartedThisTurn: 0,
232000
232211
  lastActiveAt: Date.now(),
232001
232212
  turnOpenSince: null,
232002
232213
  turnDisposition: null,
232003
- crossRemoteMcp: opts.crossRemoteMcp
232214
+ crossRemoteMcp: opts.crossRemoteMcp,
232215
+ branchedFromSessionId: sourceSessionId,
232216
+ branchedFromEntryIndex
232004
232217
  };
232005
232218
  this.sessions.set(newId, branched);
232006
232219
  await this.emitDerivedBranchActivity(projectId, branch);
@@ -233190,6 +233403,7 @@ async function createRemoteWorkflowReviewer(deps, params) {
233190
233403
  reviewFocus: params.reviewFocus ?? null,
233191
233404
  sourceTurnEndIndex: params.sourceTurnEndIndex ?? null,
233192
233405
  reviewSpan: params.reviewSpan,
233406
+ reviewContextMode: params.reviewContextMode ?? null,
233193
233407
  agentType: params.reviewerAgentType,
233194
233408
  intentBrief: params.intentBrief ?? null,
233195
233409
  userId: params.userId ?? null
@@ -233205,6 +233419,7 @@ async function createRemoteWorkflowReviewer(deps, params) {
233205
233419
  reviewFocus: params.reviewFocus,
233206
233420
  sourceTurnEndIndex: params.sourceTurnEndIndex,
233207
233421
  reviewSpan: params.reviewSpan,
233422
+ reviewContextMode: params.reviewContextMode,
233208
233423
  reviewerAgentType: params.reviewerAgentType,
233209
233424
  intentBrief: params.intentBrief,
233210
233425
  runId: remoteRunId,
@@ -233396,6 +233611,7 @@ function recoverPendingRemoteReviewerOnce(deps, intent) {
233396
233611
  reviewFocus: intent.review_focus ?? void 0,
233397
233612
  sourceTurnEndIndex: intent.source_turn_end_index ?? void 0,
233398
233613
  reviewSpan: intent.review_span,
233614
+ reviewContextMode: intent.review_context_mode ?? void 0,
233399
233615
  reviewerAgentType: intent.agent_type,
233400
233616
  intentBrief: intent.intent_brief ?? void 0,
233401
233617
  userId: intent.user_id ?? void 0,
@@ -239487,13 +239703,17 @@ var VERDICT_INSTRUCTIONS = [
239487
239703
  "3. Non-blocking notes \u2014 style and polish, briefly, clearly separated from the blocking list."
239488
239704
  ];
239489
239705
  function buildReviewerPrompt(opts) {
239490
- const intent = opts.originalIntent !== opts.taskContext ? opts.originalIntent : null;
239491
- const brief = opts.intentBrief || null;
239492
- const hasExcerpt = Boolean(intent || opts.taskContext || opts.authorSelfReport);
239706
+ const blind = opts.blind === true;
239707
+ const intent = !blind && opts.originalIntent !== opts.taskContext ? opts.originalIntent : null;
239708
+ const brief = blind ? null : opts.intentBrief || null;
239709
+ const taskContext = blind ? null : opts.taskContext;
239710
+ const selfReport = blind ? null : opts.authorSelfReport;
239711
+ const hasExcerpt = Boolean(intent || taskContext || selfReport);
239493
239712
  const scope = opts.scope && opts.scope.changedFiles.length > 0 ? opts.scope : null;
239494
- const noDiffWithAnalysis = Boolean(opts.authorSelfReport) && opts.authorSelfReport.trim().length >= SELF_REPORT_MIN_CHARS;
239713
+ const noDiffWithAnalysis = Boolean(selfReport) && selfReport.trim().length >= SELF_REPORT_MIN_CHARS;
239495
239714
  return [
239496
239715
  "You are a code reviewer agent. Another agent just completed work in this workspace; review it critically and independently.",
239716
+ blind ? '\n## Independent review\nBy design you have been given no context from the conversation that produced this work \u2014 no task statement, no author summary. Infer the intent from the change itself, the repository, and its history, and open your verdict message by stating that inferred intent in one or two sentences. Do not assume any agreement, exemption, or constraint that is not evidenced in the repository; if a behavior looks wrong but could plausibly be intentional, report it marked "possibly intended \u2014 needs author confirmation" rather than staying silent.' : null,
239497
239717
  brief ? `
239498
239718
  ## Intent brief (distilled from the source conversation)
239499
239719
  ${brief}` : null,
@@ -239503,10 +239723,10 @@ ${intent}` : null,
239503
239723
  // Deliberately not titled "Original task": in confirmation-style
239504
239724
  // conversations the latest message is often just "ok" — informative as
239505
239725
  // the user's last word, misleading as a statement of the task.
239506
- !brief && opts.taskContext ? `
239726
+ !brief && taskContext ? `
239507
239727
  ## Latest user message (verbatim)
239508
- ${opts.taskContext}` : null,
239509
- selfReportSection(opts.authorSelfReport),
239728
+ ${taskContext}` : null,
239729
+ selfReportSection(selfReport),
239510
239730
  opts.reviewFocus ? `
239511
239731
  ## Review focus (from the user)
239512
239732
  ${opts.reviewFocus}` : null,
@@ -239523,14 +239743,17 @@ Confine your review to these files and changes. Other uncommitted or pre-existin
239523
239743
  "- Do NOT modify any files \u2014 you are in read-only review mode.",
239524
239744
  "- Inspect the actual workspace state yourself: read the relevant files, run `git diff`, `git status` and `git log`.",
239525
239745
  reviewTargetPromptLine(opts.target),
239526
- noDiffWithAnalysis ? "- Judge correctness and completeness against the task. For this analysis/plan turn the work under review is the reasoning and the proposal, not code quality of a diff. Be specific: reference files and lines." : "- Judge correctness, completeness against the task, and code quality. Be specific: reference files and lines.",
239746
+ blind ? "- Judge correctness and code quality on the change's own evidence \u2014 there is no task statement to judge completeness against. Be specific: reference files and lines." : noDiffWithAnalysis ? "- Judge correctness and completeness against the task. For this analysis/plan turn the work under review is the reasoning and the proposal, not code quality of a diff. Be specific: reference files and lines." : "- Judge correctness, completeness against the task, and code quality. Be specific: reference files and lines.",
239527
239747
  // These two only make sense against a distilled brief: tier 2 has no
239528
239748
  // [settled]/[tentative] marks and no stated scope, and implying it does
239529
239749
  // would suppress findings on the strength of data that doesn't exist.
239530
239750
  brief ? "- Where the brief marks a decision, non-goal, or accepted limitation as [settled], do not re-raise the choice itself as a finding; DO report concrete consequences it causes \u2014 failure of the core goal, or a correctness, security, or data loss problem. Items marked [tentative] (or unmarked) get normal review. A violated hard constraint is always blocking." : null,
239531
239751
  brief ? "- Do not propose enhancements beyond the brief's stated scope \u2014 scope expansion is a product decision, not a review finding." : null,
239532
239752
  ...VERDICT_INSTRUCTIONS,
239533
- brief ? opts.authorSelfReport ? "\n(review context: distilled intent brief + author self-report + live workspace)" : "\n(review context: distilled intent brief + live workspace)" : hasExcerpt ? "\n(review context: deterministic excerpt of the source conversation + live workspace)" : "\n(review context: live workspace only \u2014 the source conversation was unavailable)"
239753
+ // "deliberately withheld" vs the tier-3 "was unavailable": both are
239754
+ // workspace-only prompts, but post-hoc attribution must be able to tell a
239755
+ // user choice from a degradation.
239756
+ blind ? "\n(review context: independent review \u2014 session context deliberately withheld; live workspace only)" : brief ? opts.authorSelfReport ? "\n(review context: distilled intent brief + author self-report + live workspace)" : "\n(review context: distilled intent brief + live workspace)" : hasExcerpt ? "\n(review context: deterministic excerpt of the source conversation + live workspace)" : "\n(review context: live workspace only \u2014 the source conversation was unavailable)"
239534
239757
  ].filter((l) => l !== null).join("\n");
239535
239758
  }
239536
239759
  function reviewTargetPromptLine(target) {
@@ -239699,6 +239922,7 @@ var WorkflowEngine = class {
239699
239922
  sessionId: null,
239700
239923
  title: null,
239701
239924
  agentType: null,
239925
+ lastActiveAt: null,
239702
239926
  reason
239703
239927
  });
239704
239928
  const source = await this.storage.agentSessions.getById(sourceSessionId);
@@ -239731,6 +239955,7 @@ var WorkflowEngine = class {
239731
239955
  sessionId: reviewer.id,
239732
239956
  title: reviewer.title ?? null,
239733
239957
  agentType: reviewer.agent_type,
239958
+ lastActiveAt: reviewerProjection.lastActiveAt,
239734
239959
  reason: null
239735
239960
  };
239736
239961
  }
@@ -239741,6 +239966,9 @@ var WorkflowEngine = class {
239741
239966
  if (opts.reviewerSessionId && opts.newReviewerSessionId) {
239742
239967
  throw new WorkflowError("reviewer-unavailable", "\u4E0D\u80FD\u540C\u65F6\u590D\u7528\u548C\u65B0\u5EFA reviewer session");
239743
239968
  }
239969
+ if (opts.blind && opts.reviewerSessionId) {
239970
+ throw new WorkflowError("reviewer-unavailable", "blind review \u4E0D\u80FD\u590D\u7528\u5DF2\u6709 reviewer session");
239971
+ }
239744
239972
  const runId = opts.runId ?? randomUUID6();
239745
239973
  const existingRun = opts.runId ? await this.storage.workflowRuns.getById(runId) : void 0;
239746
239974
  if (existingRun) {
@@ -239902,6 +240130,7 @@ var WorkflowEngine = class {
239902
240130
  originalIntent: extractFirstUserMessage(entries),
239903
240131
  authorSelfReport: extractAuthorSelfReport(entries, turnEndIndex),
239904
240132
  intentBrief: opts.intentBrief ?? null,
240133
+ blind: opts.blind,
239905
240134
  reviewFocus: opts.reviewFocus ?? null,
239906
240135
  target,
239907
240136
  scope
@@ -247419,6 +247648,25 @@ var routes11 = async (fastify2) => {
247419
247648
  }
247420
247649
  return reader(session.id, "runtime");
247421
247650
  }
247651
+ function sendBackFields(session) {
247652
+ const parentId = session.branchedFromSessionId;
247653
+ if (!parentId) return {};
247654
+ return {
247655
+ branchedFromSessionId: parentId,
247656
+ branchedFromAvailable: fastify2.agentSessionManager.getSession(parentId) != null,
247657
+ // Entry indices survive the branch copy unchanged, so this tells the UI
247658
+ // which dividers are inherited history (≤) vs the branch's own turns (>)
247659
+ // — send-back only makes sense on the latter.
247660
+ ...session.branchedFromEntryIndex != null ? { branchedFromEntryIndex: session.branchedFromEntryIndex } : {}
247661
+ };
247662
+ }
247663
+ async function mapRemoteSendBackFields(remoteServerId, session) {
247664
+ const { branchedFromSessionId: workerParentId, branchedFromAvailable, branchedFromEntryIndex, ...rest } = session;
247665
+ if (typeof workerParentId !== "string") return rest;
247666
+ const mapping = await fastify2.storage.remoteSessionMappings.getByRemote?.(remoteServerId, workerParentId);
247667
+ if (!mapping) return rest;
247668
+ return { ...rest, branchedFromSessionId: mapping.local_session_id, branchedFromAvailable, branchedFromEntryIndex };
247669
+ }
247422
247670
  async function performLocalBranch(sourceSessionId, userId, opts) {
247423
247671
  const sourceRow = await fastify2.storage.agentSessions.getById(sourceSessionId);
247424
247672
  const sourceProjection = sourceRow ? await projectLocalSessionIdentity(sourceRow) : void 0;
@@ -247454,7 +247702,8 @@ var routes11 = async (fastify2) => {
247454
247702
  permissionMode: session?.permissionMode || "edit",
247455
247703
  agentType: session?.agentType || "claude-code",
247456
247704
  model: session?.model ?? null,
247457
- title: dbRow?.title ?? null
247705
+ title: dbRow?.title ?? null,
247706
+ ...session ? sendBackFields(session) : {}
247458
247707
  },
247459
247708
  messages
247460
247709
  }
@@ -247542,7 +247791,8 @@ var routes11 = async (fastify2) => {
247542
247791
  workspaceCheckoutId: session?.workspaceCheckoutId ?? null,
247543
247792
  worktreePath: projection?.worktreePath ?? session?.checkoutPath ?? null,
247544
247793
  checkoutDeletedAt: projection?.checkoutDeletedAt ?? null,
247545
- processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
247794
+ processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false,
247795
+ ...session ? sendBackFields(session) : {}
247546
247796
  },
247547
247797
  messages: historyWindow ? historyWindow.entries.map((entry) => entry.message) : messages,
247548
247798
  ...historyWindow ? { historyWindow } : {}
@@ -247692,7 +247942,8 @@ var routes11 = async (fastify2) => {
247692
247942
  model: active.model ?? null,
247693
247943
  workspaceCheckoutId: active.workspaceCheckoutId,
247694
247944
  worktreePath: active.checkoutPath,
247695
- processAlive: fastify2.agentSessionManager.getSessionProcessAlive(sessionId)
247945
+ processAlive: fastify2.agentSessionManager.getSessionProcessAlive(sessionId),
247946
+ ...sendBackFields(active)
247696
247947
  },
247697
247948
  messages: fastify2.agentSessionManager.getMessages(sessionId)
247698
247949
  });
@@ -248016,7 +248267,7 @@ var routes11 = async (fastify2) => {
248016
248267
  }
248017
248268
  return reply.code(200).send({
248018
248269
  session: {
248019
- ...remoteData.session,
248270
+ ...await mapRemoteSendBackFields(agentMode, remoteData.session),
248020
248271
  id: localSessionId,
248021
248272
  projectId: req.params.projectId
248022
248273
  },
@@ -248058,7 +248309,8 @@ var routes11 = async (fastify2) => {
248058
248309
  permissionMode: session?.permissionMode || "edit",
248059
248310
  agentType: session?.agentType || "claude-code",
248060
248311
  model: session?.model ?? null,
248061
- processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
248312
+ processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false,
248313
+ ...session ? sendBackFields(session) : {}
248062
248314
  },
248063
248315
  messages: historyWindow ? historyWindow.entries.map((entry) => entry.message) : messages,
248064
248316
  ...historyWindow ? { historyWindow } : {}
@@ -248195,7 +248447,7 @@ var routes11 = async (fastify2) => {
248195
248447
  return reply.code(200).send({
248196
248448
  ...remoteData,
248197
248449
  session: {
248198
- ...remoteData.session,
248450
+ ...await mapRemoteSendBackFields(remoteInfo.remoteServerId, remoteData.session),
248199
248451
  id: req.params.sessionId,
248200
248452
  projectId: registered?.workspace.project_id ?? mapping?.project_id,
248201
248453
  branch: registered ? registered.workspace.branch === "" ? null : registered.workspace.branch : mapping?.branch,
@@ -248240,7 +248492,8 @@ var routes11 = async (fastify2) => {
248240
248492
  permissionMode: session.permissionMode,
248241
248493
  agentType: session.agentType || "claude-code",
248242
248494
  model: session.model ?? null,
248243
- processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id)
248495
+ processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id),
248496
+ ...sendBackFields(session)
248244
248497
  },
248245
248498
  messages
248246
248499
  });
@@ -248271,7 +248524,7 @@ var routes11 = async (fastify2) => {
248271
248524
  return reply.code(200).send({
248272
248525
  ...data,
248273
248526
  session: data.session ? {
248274
- ...data.session,
248527
+ ...await mapRemoteSendBackFields(remoteInfo.remoteServerId, data.session),
248275
248528
  id: req.params.sessionId,
248276
248529
  projectId: projectIdFromRemoteSessionId(req.params.sessionId, remoteInfo),
248277
248530
  branch: remoteInfo.branch ?? null
@@ -248309,7 +248562,8 @@ var routes11 = async (fastify2) => {
248309
248562
  permissionMode: session.permissionMode,
248310
248563
  agentType: session.agentType,
248311
248564
  model: session.model,
248312
- processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id)
248565
+ processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id),
248566
+ ...sendBackFields(session)
248313
248567
  }
248314
248568
  });
248315
248569
  });
@@ -248753,6 +249007,68 @@ var routes11 = async (fastify2) => {
248753
249007
  });
248754
249008
  }
248755
249009
  );
249010
+ fastify2.post(
249011
+ "/api/agent-sessions/:sessionId/background-tasks/:taskId/keep",
249012
+ async (req, reply) => {
249013
+ const userId = requireUserFacingUserId(req, reply);
249014
+ if (userId === null) return;
249015
+ const { sessionId, taskId } = req.params;
249016
+ if (sessionId.startsWith("remote-")) {
249017
+ const remoteInfo = await getAuthorizedRemoteSessionInfo(sessionId, userId);
249018
+ if (!remoteInfo) {
249019
+ return reply.code(404).send({ error: "Remote session not found" });
249020
+ }
249021
+ const result = await proxyAuto(
249022
+ remoteInfo.remoteServerId,
249023
+ "POST",
249024
+ `/api/agent-sessions/${remoteInfo.remoteSessionId}/background-tasks/${encodeURIComponent(taskId)}/keep`,
249025
+ {}
249026
+ );
249027
+ return reply.code(proxyStatus(result)).send(result.data);
249028
+ }
249029
+ const row = await fastify2.storage.agentSessions.getById(sessionId);
249030
+ if (!row || !await fastify2.storage.projects.getById(row.project_id, userId)) {
249031
+ return reply.code(404).send({ error: "Session not found" });
249032
+ }
249033
+ if (!fastify2.agentSessionManager.sanctionBackgroundTask(sessionId, taskId)) {
249034
+ return reply.code(404).send({ error: "Session not running" });
249035
+ }
249036
+ return reply.code(200).send({ success: true });
249037
+ }
249038
+ );
249039
+ fastify2.post(
249040
+ "/api/agent-sessions/:sessionId/background-tasks/:taskId/stop",
249041
+ async (req, reply) => {
249042
+ const userId = requireUserFacingUserId(req, reply);
249043
+ if (userId === null) return;
249044
+ const { sessionId, taskId } = req.params;
249045
+ if (sessionId.startsWith("remote-")) {
249046
+ const remoteInfo = await getAuthorizedRemoteSessionInfo(sessionId, userId);
249047
+ if (!remoteInfo) {
249048
+ return reply.code(404).send({ error: "Remote session not found" });
249049
+ }
249050
+ const result = await proxyAuto(
249051
+ remoteInfo.remoteServerId,
249052
+ "POST",
249053
+ `/api/agent-sessions/${remoteInfo.remoteSessionId}/background-tasks/${encodeURIComponent(taskId)}/stop`,
249054
+ {}
249055
+ );
249056
+ return reply.code(proxyStatus(result)).send(result.data);
249057
+ }
249058
+ const row = await fastify2.storage.agentSessions.getById(sessionId);
249059
+ if (!row || !await fastify2.storage.projects.getById(row.project_id, userId)) {
249060
+ return reply.code(404).send({ error: "Session not found" });
249061
+ }
249062
+ const outcome = fastify2.agentSessionManager.stopBackgroundTask(sessionId, taskId);
249063
+ if (outcome === "not_found") {
249064
+ return reply.code(404).send({ error: "Session not running" });
249065
+ }
249066
+ if (outcome === "unsupported") {
249067
+ return reply.code(501).send({ error: "This agent cannot stop a single background task \u2014 stop the session instead" });
249068
+ }
249069
+ return reply.code(200).send({ success: true });
249070
+ }
249071
+ );
248756
249072
  fastify2.post(
248757
249073
  "/api/agent-sessions/:sessionId/branch",
248758
249074
  async (req, reply) => {
@@ -250307,6 +250623,10 @@ function parseReviewSpan(raw) {
250307
250623
  if (raw === void 0) return "this_turn";
250308
250624
  return raw === "this_turn" || raw === "session_start" ? raw : null;
250309
250625
  }
250626
+ function parseReviewContextMode(raw) {
250627
+ if (raw === void 0) return "briefed";
250628
+ return raw === "briefed" || raw === "blind" ? raw : null;
250629
+ }
250310
250630
  function normalizeIntentBrief(raw) {
250311
250631
  if (!raw?.trim()) return void 0;
250312
250632
  return raw.length > 8e3 ? raw.slice(0, 8e3) + "\u2026" : raw;
@@ -250414,12 +250734,18 @@ async function routes20(fastify2) {
250414
250734
  return reply.code(400).send({ error: "reviewerSessionId and reviewerAgentType are mutually exclusive" });
250415
250735
  }
250416
250736
  const reviewerSessionId = reviewerSessionIdRaw?.trim();
250737
+ const reviewContextMode = parseReviewContextMode(req.body?.reviewContextMode);
250738
+ if (reviewContextMode === null) return reply.code(400).send({ error: "reviewContextMode must be one of: briefed, blind" });
250739
+ const blind = reviewContextMode === "blind";
250740
+ if (blind && reviewerSessionId) {
250741
+ return reply.code(400).send({ error: "blind review requires a new reviewer session" });
250742
+ }
250417
250743
  const intentBriefRaw = req.body?.intentBrief;
250418
250744
  if (intentBriefRaw !== void 0 && typeof intentBriefRaw !== "string") {
250419
250745
  return reply.code(400).send({ error: "intentBrief must be a string" });
250420
250746
  }
250421
250747
  const clientProvidedBrief = intentBriefRaw !== void 0;
250422
- const clientBrief = normalizeIntentBrief(intentBriefRaw);
250748
+ const clientBrief = blind ? void 0 : normalizeIntentBrief(intentBriefRaw);
250423
250749
  if (sourceSessionId.startsWith("remote-")) {
250424
250750
  const remoteInfo = fastify2.remoteSessionMap.get(sourceSessionId);
250425
250751
  if (!remoteInfo) return reply.code(404).send({ error: "Session not found" });
@@ -250442,7 +250768,7 @@ async function routes20(fastify2) {
250442
250768
  bareReviewerSessionId = reviewerInfo.remoteSessionId;
250443
250769
  }
250444
250770
  let intentBrief2 = clientBrief;
250445
- if (!clientProvidedBrief && !bareReviewerSessionId) {
250771
+ if (!clientProvidedBrief && !bareReviewerSessionId && !blind) {
250446
250772
  intentBrief2 = await distillIntentBrief(userId, sourceSessionId);
250447
250773
  }
250448
250774
  if (reviewerSessionId && !await fastify2.remoteNotificationSync.prepareForNewTurn(reviewerSessionId)) {
@@ -250486,6 +250812,10 @@ async function routes20(fastify2) {
250486
250812
  reviewFocus,
250487
250813
  sourceTurnEndIndex,
250488
250814
  reviewSpan,
250815
+ // Additive tunnel field: a worker that predates it ignores the flag
250816
+ // and runs a briefed (tier-2) review — the reviewer prompt's
250817
+ // trailing "(review context: …)" line records what actually ran.
250818
+ reviewContextMode,
250489
250819
  reviewerAgentType: reviewerAgentType ?? "claude-code",
250490
250820
  intentBrief: intentBrief2,
250491
250821
  userId
@@ -250586,7 +250916,7 @@ async function routes20(fastify2) {
250586
250916
  return reply.code(400).send({ error: "branch does not match source session" });
250587
250917
  }
250588
250918
  let intentBrief = clientBrief;
250589
- if (!clientProvidedBrief && !reviewerSessionId) {
250919
+ if (!clientProvidedBrief && !reviewerSessionId && !blind) {
250590
250920
  intentBrief = await distillIntentBrief(userId, sourceSessionId);
250591
250921
  }
250592
250922
  try {
@@ -250599,7 +250929,8 @@ async function routes20(fastify2) {
250599
250929
  reviewSpan,
250600
250930
  reviewerAgentType,
250601
250931
  reviewerSessionId,
250602
- intentBrief
250932
+ intentBrief,
250933
+ blind
250603
250934
  });
250604
250935
  return reply.code(201).send({ run: run2 });
250605
250936
  } catch (err) {
@@ -250816,11 +251147,14 @@ async function routes20(fastify2) {
250816
251147
  if (!sourceSessionId) return reply.code(400).send({ error: "sourceSessionId is required" });
250817
251148
  const reviewSpan = parseReviewSpan(req.body?.reviewSpan);
250818
251149
  if (reviewSpan === null) return reply.code(400).send({ error: "reviewSpan must be one of: this_turn, session_start" });
251150
+ const reviewContextMode = parseReviewContextMode(req.body?.reviewContextMode);
251151
+ if (reviewContextMode === null) return reply.code(400).send({ error: "reviewContextMode must be one of: briefed, blind" });
251152
+ const blind = reviewContextMode === "blind";
250819
251153
  const intentBriefRaw = req.body?.intentBrief;
250820
251154
  if (intentBriefRaw !== void 0 && typeof intentBriefRaw !== "string") {
250821
251155
  return reply.code(400).send({ error: "intentBrief must be a string" });
250822
251156
  }
250823
- const intentBrief = normalizeIntentBrief(intentBriefRaw);
251157
+ const intentBrief = blind ? void 0 : normalizeIntentBrief(intentBriefRaw);
250824
251158
  const reviewerAgentType = parseReviewerAgentType(req.body?.reviewerAgentType);
250825
251159
  if (reviewerAgentType === null) return reply.code(400).send({ error: "reviewerAgentType must be one of: claude-code, codex" });
250826
251160
  const reviewerSessionIdRaw = req.body?.reviewerSessionId;
@@ -250858,6 +251192,7 @@ async function routes20(fastify2) {
250858
251192
  reviewerAgentType,
250859
251193
  reviewerSessionId,
250860
251194
  intentBrief,
251195
+ blind,
250861
251196
  runId: runId || void 0,
250862
251197
  newReviewerSessionId: newReviewerSessionId || void 0
250863
251198
  });
@@ -251770,6 +252105,11 @@ var routes23 = async (fastify2) => {
251770
252105
  try {
251771
252106
  const message = JSON.parse(data.toString());
251772
252107
  if (message.type === "input" || message.type === "resize") {
252108
+ if (message.type === "resize") {
252109
+ console.log(
252110
+ `[WebSocket] resize ${processId} \u2192 ${message.cols}x${message.rows} ip=${req.ip} ua=${req.headers["user-agent"] ?? "?"}`
252111
+ );
252112
+ }
251773
252113
  handle.handleInput(message);
251774
252114
  }
251775
252115
  } catch (error48) {
@@ -251845,6 +252185,9 @@ var routes23 = async (fastify2) => {
251845
252185
  } else if (msg.type === "input") {
251846
252186
  handleInputMap.get(msg.processId)?.({ type: "input", data: msg.data });
251847
252187
  } else if (msg.type === "resize") {
252188
+ console.log(
252189
+ `[ExecutorMux] resize ${msg.processId} \u2192 ${msg.cols}x${msg.rows} ip=${req.ip} ua=${req.headers["user-agent"] ?? "?"}`
252190
+ );
251848
252191
  handleInputMap.get(msg.processId)?.({ type: "resize", cols: msg.cols, rows: msg.rows });
251849
252192
  }
251850
252193
  } catch (error48) {
@@ -251936,6 +252279,12 @@ var routes23 = async (fastify2) => {
251936
252279
  }));
251937
252280
  } catch {
251938
252281
  }
252282
+ if (cacheEntry.backgroundTasks !== null) {
252283
+ try {
252284
+ socket.send(cacheEntry.backgroundTasks);
252285
+ } catch {
252286
+ }
252287
+ }
251939
252288
  for (const raw of cacheEntry.messages) {
251940
252289
  if (replayAfter >= 0) {
251941
252290
  try {
@@ -251959,12 +252308,6 @@ var routes23 = async (fastify2) => {
251959
252308
  socket.send(JSON.stringify({ Ready: true, historyEpoch: cacheEntry.historyEpoch ?? void 0 }));
251960
252309
  } catch {
251961
252310
  }
251962
- if (cacheEntry.backgroundTasks !== null) {
251963
- try {
251964
- socket.send(cacheEntry.backgroundTasks);
251965
- } catch {
251966
- }
251967
- }
251968
252311
  if (cacheEntry.finished) {
251969
252312
  try {
251970
252313
  socket.send(JSON.stringify({ finished: true }));
@@ -260532,6 +260875,12 @@ var WORKER_CAPABILITIES = {
260532
260875
  "http:POST /api/agent-sessions/:param/restart": { since: "0.2.0", summary: "\u91CD\u542F\u4F1A\u8BDD\u8FDB\u7A0B" },
260533
260876
  "http:POST /api/agent-sessions/:param/agent-type": { since: "0.2.0", summary: "\u5207\u6362 agent \u7C7B\u578B" },
260534
260877
  "http:POST /api/agent-sessions/:param/model": { since: "0.2.0", summary: "\u5207\u6362\u4F1A\u8BDD\u6A21\u578B" },
260878
+ // Additive: an older worker 404s it, and the UI degrades by hiding the
260879
+ // "keep running" button — that worker has no park deadline to defuse.
260880
+ "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" },
260881
+ // Additive alongside /keep, and degrades the same way: an older worker 404s
260882
+ // and the UI hides the button.
260883
+ "http:POST /api/agent-sessions/:param/background-tasks/:param/stop": { since: "0.3.28", summary: "\u505C\u6B62\u5355\u4E2A\u540E\u53F0\u4EFB\u52A1" },
260535
260884
  "http:POST /api/path/agent-sessions/:param/branch": { since: "0.2.0", summary: "\u4ECE\u5386\u53F2\u5206\u53C9\u4F1A\u8BDD" },
260536
260885
  "http:POST /api/agent-sessions/:param/switch-mode": { since: "0.2.0", summary: "\u6743\u9650\u6A21\u5F0F\u5207\u6362" },
260537
260886
  "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.27",
3
+ "version": "0.3.29",
4
4
  "description": "Vibedeckx platform binaries for Linux x64",
5
5
  "os": [
6
6
  "linux"