@vibedeckx/linux-x64 0.3.28 → 0.3.30

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 +512 -90
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -186767,6 +186767,7 @@ function getWorkspaceBindingReadMetrics() {
186767
186767
 
186768
186768
  // src/storage/workflow-run-status.ts
186769
186769
  var WORKFLOW_ACTIVE_STATUSES = [
186770
+ "preparing",
186770
186771
  "waiting_reviewer",
186771
186772
  "waiting_feedback",
186772
186773
  "discussing",
@@ -186844,7 +186845,9 @@ var mapAgentSession = (row) => ({
186844
186845
  last_completed_at: row.last_completed_at,
186845
186846
  favorited_at: row.favorited_at,
186846
186847
  native_session_id: row.native_session_id,
186847
- history_epoch: row.history_epoch
186848
+ history_epoch: row.history_epoch,
186849
+ branched_from_session_id: row.branched_from_session_id,
186850
+ branched_from_entry_index: row.branched_from_entry_index
186848
186851
  });
186849
186852
  var parseActivityTimestamp = (value) => {
186850
186853
  const explicitZone = /(?:Z|[+-]\d\d:\d\d)$/i.test(value);
@@ -187007,6 +187010,7 @@ var mapRemoteCreationIntent = (row) => ({
187007
187010
  var mapRemoteReviewerCreationIntent = (row) => ({
187008
187011
  ...row,
187009
187012
  review_span: row.review_span,
187013
+ review_context_mode: row.review_context_mode ?? null,
187010
187014
  status: row.status
187011
187015
  });
187012
187016
  var nowActivityAt = () => sql`cast((julianday('now') - 2440587.5) * 86400000 as integer)`;
@@ -187231,6 +187235,9 @@ var createAgentSessionRepos = (kdb, h) => ({
187231
187235
  // Toggle favorite without touching updated_at — favoriting is a passive
187232
187236
  // bookmark, not a "this session was active" signal, so it must not
187233
187237
  // disturb the dropdown's recency ordering.
187238
+ setBranchedFrom: async (id, sourceSessionId, entryIndex) => {
187239
+ await kdb.updateTable("agent_sessions").set({ branched_from_session_id: sourceSessionId, branched_from_entry_index: entryIndex }).where("id", "=", id).execute();
187240
+ },
187234
187241
  setFavorited: async (id, favorited) => {
187235
187242
  await kdb.updateTable("agent_sessions").set({ favorited_at: favorited ? Date.now() : null }).where("id", "=", id).execute();
187236
187243
  },
@@ -187549,6 +187556,7 @@ var createAgentSessionRepos = (kdb, h) => ({
187549
187556
  review_focus: intent.reviewFocus ?? null,
187550
187557
  source_turn_end_index: intent.sourceTurnEndIndex ?? null,
187551
187558
  review_span: intent.reviewSpan,
187559
+ review_context_mode: intent.reviewContextMode ?? null,
187552
187560
  agent_type: intent.agentType,
187553
187561
  intent_brief: intent.intentBrief ?? null,
187554
187562
  user_id: intent.userId ?? null,
@@ -187558,7 +187566,7 @@ var createAgentSessionRepos = (kdb, h) => ({
187558
187566
  updated_at: h.nowMs()
187559
187567
  }).onConflict((oc) => oc.column("local_reviewer_session_id").doNothing()).execute();
187560
187568
  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);
187569
+ 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
187570
  if (!sameIdentity) {
187563
187571
  throw new Error(`Remote reviewer creation intent ${intent.localReviewerSessionId} has conflicting identity`);
187564
187572
  }
@@ -188469,7 +188477,7 @@ var createWorkflowRunRepos = (kdb) => ({
188469
188477
  ...opts,
188470
188478
  reviewer_session_id: opts.reviewer_session_id ?? null,
188471
188479
  review_span: opts.review_span ?? "this_turn",
188472
- status: "waiting_reviewer"
188480
+ status: opts.status ?? "waiting_reviewer"
188473
188481
  }).execute();
188474
188482
  const row = await kdb.selectFrom("workflow_runs").selectAll().where("id", "=", opts.id).executeTakeFirstOrThrow();
188475
188483
  return asRun(row);
@@ -203758,6 +203766,8 @@ var tightenWorkspaceCheckoutForeignKeys = (db) => {
203758
203766
  favorited_at INTEGER DEFAULT NULL,
203759
203767
  native_session_id TEXT DEFAULT NULL,
203760
203768
  history_epoch INTEGER NOT NULL DEFAULT 0,
203769
+ branched_from_session_id TEXT DEFAULT NULL,
203770
+ branched_from_entry_index INTEGER DEFAULT NULL,
203761
203771
  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
203762
203772
  FOREIGN KEY (workspace_checkout_id) REFERENCES workspace_checkouts(id)
203763
203773
  DEFERRABLE INITIALLY DEFERRED
@@ -203765,10 +203775,12 @@ var tightenWorkspaceCheckoutForeignKeys = (db) => {
203765
203775
  INSERT INTO agent_sessions_fk_new
203766
203776
  (id, project_id, branch, workspace_checkout_id, status, permission_mode, agent_type,
203767
203777
  title, model, created_at, updated_at, activity_at, last_user_message_at,
203768
- last_completed_at, favorited_at, native_session_id, history_epoch)
203778
+ last_completed_at, favorited_at, native_session_id, history_epoch,
203779
+ branched_from_session_id, branched_from_entry_index)
203769
203780
  SELECT id, project_id, branch, workspace_checkout_id, status, permission_mode, agent_type,
203770
203781
  title, model, created_at, updated_at, activity_at, last_user_message_at,
203771
- last_completed_at, favorited_at, native_session_id, history_epoch
203782
+ last_completed_at, favorited_at, native_session_id, history_epoch,
203783
+ branched_from_session_id, branched_from_entry_index
203772
203784
  FROM agent_sessions;
203773
203785
  DROP TABLE agent_sessions;
203774
203786
  ALTER TABLE agent_sessions_fk_new RENAME TO agent_sessions;
@@ -204179,6 +204191,7 @@ var initializeSchema = (db) => {
204179
204191
  review_focus TEXT,
204180
204192
  source_turn_end_index INTEGER,
204181
204193
  review_span TEXT NOT NULL CHECK (review_span IN ('this_turn', 'session_start')),
204194
+ review_context_mode TEXT CHECK (review_context_mode IN ('briefed', 'blind')),
204182
204195
  agent_type TEXT NOT NULL,
204183
204196
  intent_brief TEXT,
204184
204197
  user_id TEXT,
@@ -204595,6 +204608,11 @@ var initializeSchema = (db) => {
204595
204608
  coalesce(cast((julianday(created_at) - 2440587.5) * 86400000 as integer), 0)
204596
204609
  )`);
204597
204610
  }
204611
+ const sessionBranchedFromInfo = db.prepare("PRAGMA table_info(agent_sessions)").all();
204612
+ if (!sessionBranchedFromInfo.some((col) => col.name === "branched_from_session_id")) {
204613
+ db.exec("ALTER TABLE agent_sessions ADD COLUMN branched_from_session_id TEXT DEFAULT NULL");
204614
+ db.exec("ALTER TABLE agent_sessions ADD COLUMN branched_from_entry_index INTEGER DEFAULT NULL");
204615
+ }
204598
204616
  db.exec(`
204599
204617
  CREATE INDEX IF NOT EXISTS idx_agent_sessions_project_branch
204600
204618
  ON agent_sessions(project_id, branch);
@@ -205344,6 +205362,10 @@ var initializeSchema = (db) => {
205344
205362
  if (!workflowRunsInfo.some((col) => col.name === "review_span")) {
205345
205363
  db.exec("ALTER TABLE workflow_runs ADD COLUMN review_span TEXT NOT NULL DEFAULT 'this_turn'");
205346
205364
  }
205365
+ const reviewerIntentsInfo = db.prepare("PRAGMA table_info(remote_reviewer_creation_intents)").all();
205366
+ if (!reviewerIntentsInfo.some((col) => col.name === "review_context_mode")) {
205367
+ db.exec("ALTER TABLE remote_reviewer_creation_intents ADD COLUMN review_context_mode TEXT CHECK (review_context_mode IN ('briefed', 'blind'))");
205368
+ }
205347
205369
  db.exec(`
205348
205370
  CREATE TABLE IF NOT EXISTS user_settings (
205349
205371
  user_id TEXT NOT NULL,
@@ -231165,6 +231187,7 @@ var AgentSessionManager = class {
231165
231187
  reset: opts.historyEpoch !== void 0 && opts.historyEpoch !== session.historyEpoch
231166
231188
  }
231167
231189
  }));
231190
+ ws.send(JSON.stringify(this.backgroundTasksMessage(session)));
231168
231191
  for (const patch of session.store.patches) {
231169
231192
  const entryIndices = patch.flatMap((op) => {
231170
231193
  const match2 = op.path.match(/^\/entries\/(\d+)$/);
@@ -231177,8 +231200,6 @@ var AgentSessionManager = class {
231177
231200
  ws.send(JSON.stringify({ Ready: true, historyEpoch: session.historyEpoch }));
231178
231201
  const statusPatch = ConversationPatch.updateStatus(session.status);
231179
231202
  ws.send(JSON.stringify({ JsonPatch: statusPatch }));
231180
- const tasksMsg = this.backgroundTasksMessage(session);
231181
- ws.send(JSON.stringify(tasksMsg));
231182
231203
  return () => {
231183
231204
  session.subscribers.delete(ws);
231184
231205
  };
@@ -231977,6 +231998,8 @@ var AgentSessionManager = class {
231977
231998
  permissionMode,
231978
231999
  agentType: dbSession.agent_type || "claude-code",
231979
232000
  model: dbSession.model ?? null,
232001
+ branchedFromSessionId: dbSession.branched_from_session_id ?? null,
232002
+ branchedFromEntryIndex: dbSession.branched_from_entry_index ?? null,
231980
232003
  completion: new TurnCompletionLedger(this.parkTimeoutMs),
231981
232004
  graceTimer: null,
231982
232005
  parkTimer: null,
@@ -232103,6 +232126,14 @@ var AgentSessionManager = class {
232103
232126
  if (existingRuntime && opts.crossRemoteMcp) {
232104
232127
  existingRuntime.crossRemoteMcp = opts.crossRemoteMcp;
232105
232128
  }
232129
+ if (!existingBranch.branched_from_session_id) {
232130
+ const repairedEntryIndex = entryRows[entryRows.length - 1].entry_index;
232131
+ await this.storage.agentSessions.setBranchedFrom(newId, sourceSessionId, repairedEntryIndex);
232132
+ if (existingRuntime) {
232133
+ existingRuntime.branchedFromSessionId = sourceSessionId;
232134
+ existingRuntime.branchedFromEntryIndex = repairedEntryIndex;
232135
+ }
232136
+ }
232106
232137
  return { ok: true, sessionId: newId };
232107
232138
  }
232108
232139
  }
@@ -232136,6 +232167,8 @@ var AgentSessionManager = class {
232136
232167
  for (const row of entryRows) {
232137
232168
  await this.storage.agentSessions.upsertEntry(newId, row.entry_index, row.data);
232138
232169
  }
232170
+ const branchedFromEntryIndex = opts.upToEntryIndex ?? entryRows[entryRows.length - 1].entry_index;
232171
+ await this.storage.agentSessions.setBranchedFrom(newId, sourceSessionId, branchedFromEntryIndex);
232139
232172
  let baseTitle = sourceRow?.title ?? null;
232140
232173
  if (!baseTitle) {
232141
232174
  for (const row of entryRows) {
@@ -232179,7 +232212,9 @@ var AgentSessionManager = class {
232179
232212
  lastActiveAt: Date.now(),
232180
232213
  turnOpenSince: null,
232181
232214
  turnDisposition: null,
232182
- crossRemoteMcp: opts.crossRemoteMcp
232215
+ crossRemoteMcp: opts.crossRemoteMcp,
232216
+ branchedFromSessionId: sourceSessionId,
232217
+ branchedFromEntryIndex
232183
232218
  };
232184
232219
  this.sessions.set(newId, branched);
232185
232220
  await this.emitDerivedBranchActivity(projectId, branch);
@@ -233369,27 +233404,39 @@ async function createRemoteWorkflowReviewer(deps, params) {
233369
233404
  reviewFocus: params.reviewFocus ?? null,
233370
233405
  sourceTurnEndIndex: params.sourceTurnEndIndex ?? null,
233371
233406
  reviewSpan: params.reviewSpan,
233407
+ reviewContextMode: params.reviewContextMode ?? null,
233372
233408
  agentType: params.reviewerAgentType,
233373
233409
  intentBrief: params.intentBrief ?? null,
233374
233410
  userId: params.userId ?? null
233375
233411
  });
233376
233412
  let registeredLocalSessionId = null;
233377
233413
  try {
233378
- const result = await proxyToRemoteAuto(
233414
+ const sharedBody = {
233415
+ sourceSessionId: params.sourceRemoteSessionId,
233416
+ reviewFocus: params.reviewFocus,
233417
+ sourceTurnEndIndex: params.sourceTurnEndIndex,
233418
+ reviewSpan: params.reviewSpan,
233419
+ reviewerAgentType: params.reviewerAgentType,
233420
+ runId: remoteRunId,
233421
+ newReviewerSessionId: remoteReviewerSessionId
233422
+ };
233423
+ const proxyOpts = { reverseConnectManager: deps.reverseConnectManager ?? void 0 };
233424
+ const result = params.phase === "prepare" ? await proxyToRemoteAuto(
233425
+ params.agentMode,
233426
+ "POST",
233427
+ "/api/path/workflow-runs/prepare",
233428
+ sharedBody,
233429
+ proxyOpts
233430
+ ) : await proxyToRemoteAuto(
233379
233431
  params.agentMode,
233380
233432
  "POST",
233381
233433
  "/api/path/workflow-runs",
233382
233434
  {
233383
- sourceSessionId: params.sourceRemoteSessionId,
233384
- reviewFocus: params.reviewFocus,
233385
- sourceTurnEndIndex: params.sourceTurnEndIndex,
233386
- reviewSpan: params.reviewSpan,
233387
- reviewerAgentType: params.reviewerAgentType,
233388
- intentBrief: params.intentBrief,
233389
- runId: remoteRunId,
233390
- newReviewerSessionId: remoteReviewerSessionId
233435
+ ...sharedBody,
233436
+ reviewContextMode: params.reviewContextMode,
233437
+ intentBrief: params.intentBrief
233391
233438
  },
233392
- { reverseConnectManager: deps.reverseConnectManager ?? void 0 }
233439
+ proxyOpts
233393
233440
  );
233394
233441
  if (!result.ok) {
233395
233442
  const uncertain = result.errorCode === "network_error" || result.errorCode === "timeout";
@@ -233575,6 +233622,7 @@ function recoverPendingRemoteReviewerOnce(deps, intent) {
233575
233622
  reviewFocus: intent.review_focus ?? void 0,
233576
233623
  sourceTurnEndIndex: intent.source_turn_end_index ?? void 0,
233577
233624
  reviewSpan: intent.review_span,
233625
+ reviewContextMode: intent.review_context_mode ?? void 0,
233578
233626
  reviewerAgentType: intent.agent_type,
233579
233627
  intentBrief: intent.intent_brief ?? void 0,
233580
233628
  userId: intent.user_id ?? void 0,
@@ -239666,13 +239714,17 @@ var VERDICT_INSTRUCTIONS = [
239666
239714
  "3. Non-blocking notes \u2014 style and polish, briefly, clearly separated from the blocking list."
239667
239715
  ];
239668
239716
  function buildReviewerPrompt(opts) {
239669
- const intent = opts.originalIntent !== opts.taskContext ? opts.originalIntent : null;
239670
- const brief = opts.intentBrief || null;
239671
- const hasExcerpt = Boolean(intent || opts.taskContext || opts.authorSelfReport);
239717
+ const blind = opts.blind === true;
239718
+ const intent = !blind && opts.originalIntent !== opts.taskContext ? opts.originalIntent : null;
239719
+ const brief = blind ? null : opts.intentBrief || null;
239720
+ const taskContext = blind ? null : opts.taskContext;
239721
+ const selfReport = blind ? null : opts.authorSelfReport;
239722
+ const hasExcerpt = Boolean(intent || taskContext || selfReport);
239672
239723
  const scope = opts.scope && opts.scope.changedFiles.length > 0 ? opts.scope : null;
239673
- const noDiffWithAnalysis = Boolean(opts.authorSelfReport) && opts.authorSelfReport.trim().length >= SELF_REPORT_MIN_CHARS;
239724
+ const noDiffWithAnalysis = Boolean(selfReport) && selfReport.trim().length >= SELF_REPORT_MIN_CHARS;
239674
239725
  return [
239675
239726
  "You are a code reviewer agent. Another agent just completed work in this workspace; review it critically and independently.",
239727
+ 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,
239676
239728
  brief ? `
239677
239729
  ## Intent brief (distilled from the source conversation)
239678
239730
  ${brief}` : null,
@@ -239682,10 +239734,10 @@ ${intent}` : null,
239682
239734
  // Deliberately not titled "Original task": in confirmation-style
239683
239735
  // conversations the latest message is often just "ok" — informative as
239684
239736
  // the user's last word, misleading as a statement of the task.
239685
- !brief && opts.taskContext ? `
239737
+ !brief && taskContext ? `
239686
239738
  ## Latest user message (verbatim)
239687
- ${opts.taskContext}` : null,
239688
- selfReportSection(opts.authorSelfReport),
239739
+ ${taskContext}` : null,
239740
+ selfReportSection(selfReport),
239689
239741
  opts.reviewFocus ? `
239690
239742
  ## Review focus (from the user)
239691
239743
  ${opts.reviewFocus}` : null,
@@ -239702,14 +239754,17 @@ Confine your review to these files and changes. Other uncommitted or pre-existin
239702
239754
  "- Do NOT modify any files \u2014 you are in read-only review mode.",
239703
239755
  "- Inspect the actual workspace state yourself: read the relevant files, run `git diff`, `git status` and `git log`.",
239704
239756
  reviewTargetPromptLine(opts.target),
239705
- 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.",
239757
+ 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.",
239706
239758
  // These two only make sense against a distilled brief: tier 2 has no
239707
239759
  // [settled]/[tentative] marks and no stated scope, and implying it does
239708
239760
  // would suppress findings on the strength of data that doesn't exist.
239709
239761
  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,
239710
239762
  brief ? "- Do not propose enhancements beyond the brief's stated scope \u2014 scope expansion is a product decision, not a review finding." : null,
239711
239763
  ...VERDICT_INSTRUCTIONS,
239712
- 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)"
239764
+ // "deliberately withheld" vs the tier-3 "was unavailable": both are
239765
+ // workspace-only prompts, but post-hoc attribution must be able to tell a
239766
+ // user choice from a degradation.
239767
+ 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)"
239713
239768
  ].filter((l) => l !== null).join("\n");
239714
239769
  }
239715
239770
  function reviewTargetPromptLine(target) {
@@ -239758,6 +239813,7 @@ var FINAL_VERDICT_PROMPT = [
239758
239813
  ...VERDICT_INSTRUCTIONS
239759
239814
  ].join("\n");
239760
239815
  var REVIEWER_AGENT_TYPES = /* @__PURE__ */ new Set(["claude-code", "codex"]);
239816
+ var PREPARE_TIMEOUT_MS = 10 * 6e4;
239761
239817
  var WorkflowEngine = class {
239762
239818
  constructor(storage2, agentOps) {
239763
239819
  this.storage = storage2;
@@ -239768,6 +239824,12 @@ var WorkflowEngine = class {
239768
239824
  eventBus;
239769
239825
  /** sessionId → participation in an active run (rebuilt on boot). */
239770
239826
  participants = /* @__PURE__ */ new Map();
239827
+ /** runId → prompt inputs captured at prepare time (see PendingActivation). */
239828
+ pendingActivations = /* @__PURE__ */ new Map();
239829
+ /** runId → armed preparation-timeout timer. */
239830
+ prepareTimers = /* @__PURE__ */ new Map();
239831
+ /** runId → in-flight activation, so concurrent calls join instead of double-sending. */
239832
+ activationFlights = /* @__PURE__ */ new Map();
239771
239833
  setEventBus(bus) {
239772
239834
  this.eventBus = bus;
239773
239835
  bus.subscribe((event) => {
@@ -239792,6 +239854,12 @@ var WorkflowEngine = class {
239792
239854
  await this.storage.workflowRuns.update(run2.id, {
239793
239855
  error: "\u670D\u52A1\u91CD\u542F\uFF0C\u53EF\u80FD\u9519\u8FC7 reviewer \u5B8C\u6210\u4E8B\u4EF6\u3002\u82E5 reviewer \u5DF2\u5B8C\u6210\uFF0C\u8BF7\u6253\u5F00\u5176\u7A97\u53E3\u67E5\u770B\uFF0C\u6216\u7ED3\u675F\u672C\u6B21 review\u3002"
239794
239856
  });
239857
+ } else if (run2.status === "preparing") {
239858
+ const createdAt = Date.parse(
239859
+ run2.created_at.includes("T") ? run2.created_at : run2.created_at.replace(" ", "T") + "Z"
239860
+ );
239861
+ const elapsed = Number.isFinite(createdAt) ? Date.now() - createdAt : PREPARE_TIMEOUT_MS;
239862
+ this.armPrepareTimeout(run2.id, PREPARE_TIMEOUT_MS - elapsed);
239795
239863
  }
239796
239864
  this.trackParticipants(run2);
239797
239865
  }
@@ -239806,6 +239874,37 @@ var WorkflowEngine = class {
239806
239874
  for (const [sid, p2] of this.participants) {
239807
239875
  if (p2.runId === run2.id) this.participants.delete(sid);
239808
239876
  }
239877
+ this.clearPendingActivation(run2.id);
239878
+ }
239879
+ clearPendingActivation(runId) {
239880
+ this.pendingActivations.delete(runId);
239881
+ const timer = this.prepareTimers.get(runId);
239882
+ if (timer) {
239883
+ clearTimeout(timer);
239884
+ this.prepareTimers.delete(runId);
239885
+ }
239886
+ }
239887
+ /**
239888
+ * Backstop for a run stuck in `preparing` (see PREPARE_TIMEOUT_MS). A timed
239889
+ * out preparation becomes a normal failed run — visible in the UI and as a
239890
+ * workflow_failed milestone — instead of a placeholder that spins forever.
239891
+ * `unref` so an armed timer never holds the process open.
239892
+ */
239893
+ armPrepareTimeout(runId, delayMs) {
239894
+ const existing = this.prepareTimers.get(runId);
239895
+ if (existing) clearTimeout(existing);
239896
+ const timer = setTimeout(() => {
239897
+ this.prepareTimers.delete(runId);
239898
+ void (async () => {
239899
+ const run2 = await this.storage.workflowRuns.getById(runId);
239900
+ if (run2?.status === "preparing") {
239901
+ await this.failRun(run2, "review \u51C6\u5907\u8D85\u65F6\uFF1Aintent brief \u7684\u751F\u6210\u65B9\u6CA1\u6709\u56DE\u6765\u6FC0\u6D3B reviewer\u3002\u53EF\u7ED3\u675F\u540E\u91CD\u65B0\u53D1\u8D77\u3002");
239902
+ }
239903
+ this.pendingActivations.delete(runId);
239904
+ })().catch((err) => console.error("[WorkflowEngine] prepare-timeout sweep failed:", err));
239905
+ }, Math.max(0, delayMs));
239906
+ timer.unref?.();
239907
+ this.prepareTimers.set(runId, timer);
239809
239908
  }
239810
239909
  releaseReservations(runId) {
239811
239910
  for (const [sid, participant] of this.participants) {
@@ -239846,7 +239945,10 @@ var WorkflowEngine = class {
239846
239945
  );
239847
239946
  if (!ok) return;
239848
239947
  const failed = await this.storage.workflowRuns.getById(run2.id);
239849
- if (failed) this.untrackRun(failed);
239948
+ if (failed) {
239949
+ this.untrackRun(failed);
239950
+ this.emitRunUpdated(failed);
239951
+ }
239850
239952
  this.onMilestoneCreated?.();
239851
239953
  }
239852
239954
  /** Test seam for the failure path (see failRun). */
@@ -239878,6 +239980,7 @@ var WorkflowEngine = class {
239878
239980
  sessionId: null,
239879
239981
  title: null,
239880
239982
  agentType: null,
239983
+ lastActiveAt: null,
239881
239984
  reason
239882
239985
  });
239883
239986
  const source = await this.storage.agentSessions.getById(sourceSessionId);
@@ -239910,16 +240013,42 @@ var WorkflowEngine = class {
239910
240013
  sessionId: reviewer.id,
239911
240014
  title: reviewer.title ?? null,
239912
240015
  agentType: reviewer.agent_type,
240016
+ lastActiveAt: reviewerProjection.lastActiveAt,
239913
240017
  reason: null
239914
240018
  };
239915
240019
  }
240020
+ /**
240021
+ * Single-shot start, preserved for old callers and durable-intent replays:
240022
+ * prepare + activate back to back. The reuse-reviewer path delivers its
240023
+ * prompt inside prepare (no preparing state — nothing distills for a
240024
+ * re-review); a fresh reviewer comes back `preparing` and is activated
240025
+ * inline, which also completes a replayed run that a previous caller
240026
+ * prepared but never activated.
240027
+ */
239916
240028
  async startAdhocReview(opts) {
240029
+ const run2 = await this.prepareAdhocReview(opts);
240030
+ if (opts.reviewerSessionId || run2.status !== "preparing") return run2;
240031
+ return this.activateAdhocReview(run2.id, { intentBrief: opts.intentBrief, blind: opts.blind });
240032
+ }
240033
+ /**
240034
+ * Phase 1 of the two-phase adhoc review: validate, reserve participants,
240035
+ * create the run row and (for a fresh review) the reviewer session — titled
240036
+ * and visible in the sidebar — WITHOUT sending the reviewer prompt. Fast
240037
+ * (no model calls), so the route can respond immediately and let the
240038
+ * intent-brief distillation happen after the user has moved on. The run
240039
+ * stays `preparing` until activateAdhocReview delivers the first message,
240040
+ * bounded by PREPARE_TIMEOUT_MS.
240041
+ */
240042
+ async prepareAdhocReview(opts) {
239917
240043
  if (opts.reviewerSessionId === opts.sourceSessionId) {
239918
240044
  throw new WorkflowError("reviewer-unavailable", "reviewer session \u4E0D\u80FD\u4E0E source session \u76F8\u540C");
239919
240045
  }
239920
240046
  if (opts.reviewerSessionId && opts.newReviewerSessionId) {
239921
240047
  throw new WorkflowError("reviewer-unavailable", "\u4E0D\u80FD\u540C\u65F6\u590D\u7528\u548C\u65B0\u5EFA reviewer session");
239922
240048
  }
240049
+ if (opts.blind && opts.reviewerSessionId) {
240050
+ throw new WorkflowError("reviewer-unavailable", "blind review \u4E0D\u80FD\u590D\u7528\u5DF2\u6709 reviewer session");
240051
+ }
239923
240052
  const runId = opts.runId ?? randomUUID6();
239924
240053
  const existingRun = opts.runId ? await this.storage.workflowRuns.getById(runId) : void 0;
239925
240054
  if (existingRun) {
@@ -239933,7 +240062,7 @@ var WorkflowEngine = class {
239933
240062
  }
239934
240063
  return existingRun;
239935
240064
  }
239936
- if (existingRun.status !== "waiting_reviewer") {
240065
+ if (existingRun.status !== "waiting_reviewer" && existingRun.status !== "preparing") {
239937
240066
  throw new WorkflowError("bad-state", "workflow run \u5DF2\u7EC8\u6B62\uFF0C\u4E0D\u80FD\u4F5C\u4E3A\u672A\u77E5\u521B\u5EFA\u7ED3\u679C\u91CD\u653E");
239938
240067
  }
239939
240068
  }
@@ -240011,9 +240140,17 @@ var WorkflowEngine = class {
240011
240140
  review_focus: opts.reviewFocus ?? null,
240012
240141
  review_target: JSON.stringify(target),
240013
240142
  reviewer_session_id: opts.reviewerSessionId ?? null,
240014
- review_span: opts.reviewSpan ?? "this_turn"
240143
+ review_span: opts.reviewSpan ?? "this_turn",
240144
+ // Reuse skips the preparing state entirely: its prompt is delivered
240145
+ // below, before this method returns.
240146
+ status: opts.reviewerSessionId ? "waiting_reviewer" : "preparing"
240015
240147
  });
240016
240148
  this.trackParticipants(run2);
240149
+ if (existingRun && !existingRun.reviewer_session_id && !opts.reviewerSessionId && run2.status === "waiting_reviewer") {
240150
+ const flipped = await this.storage.workflowRuns.transition(run2.id, "waiting_reviewer", "preparing");
240151
+ if (!flipped) throw new WorkflowError("bad-state", "run \u5728\u91CD\u653E\u671F\u95F4\u5DF2\u88AB\u53D6\u6D88\u6216\u5931\u8D25");
240152
+ run2.status = "preparing";
240153
+ }
240017
240154
  if (opts.reviewerSessionId && reviewerSession) {
240018
240155
  if (reviewerSession.permission_mode !== "plan") {
240019
240156
  let switched = false;
@@ -240064,7 +240201,13 @@ var WorkflowEngine = class {
240064
240201
  false,
240065
240202
  "plan",
240066
240203
  opts.reviewerAgentType ?? "claude-code",
240067
- true,
240204
+ // announceRunning=false — deliberate change from the single-shot
240205
+ // start: the preparing reviewer must appear in the sidebar (the
240206
+ // spawn's unconditional session:process emit covers that) WITHOUT
240207
+ // the session:status "running" emit that auto-surfaces it into the
240208
+ // open agent window. The user stays on the source session and opens
240209
+ // the reviewer from the sidebar when they choose to.
240210
+ false,
240068
240211
  false,
240069
240212
  {
240070
240213
  startSnapshot: endSnap,
@@ -240076,26 +240219,25 @@ var WorkflowEngine = class {
240076
240219
  reviewerId,
240077
240220
  `Review - ${sourceSession?.title || (taskContext ? snippetTitle(taskContext) : null) || "Conversation"}`
240078
240221
  ).catch((err) => console.warn(`[WorkflowEngine] failed to set reviewer title for ${reviewerId}:`, err));
240079
- const prompt = buildReviewerPrompt({
240222
+ this.pendingActivations.set(run2.id, {
240223
+ scope,
240080
240224
  taskContext,
240081
240225
  originalIntent: extractFirstUserMessage(entries),
240082
- authorSelfReport: extractAuthorSelfReport(entries, turnEndIndex),
240083
- intentBrief: opts.intentBrief ?? null,
240084
- reviewFocus: opts.reviewFocus ?? null,
240085
- target,
240086
- scope
240226
+ authorSelfReport: extractAuthorSelfReport(entries, turnEndIndex)
240227
+ });
240228
+ const updated = await this.storage.workflowRuns.update(run2.id, {
240229
+ reviewer_session_id: reviewerId,
240230
+ // Legacy replay backfill: a durable run interrupted before reviewer
240231
+ // binding may carry no stored target. Activation builds the prompt
240232
+ // from the stored one, so persist the target just captured (same
240233
+ // worktree, cutoff verified above to match the stored run).
240234
+ ...run2.review_target ? {} : { review_target: JSON.stringify(target) }
240087
240235
  });
240088
- const sent = await this.agentOps.sendUserMessage(reviewerId, prompt, opts.project.path, void 0, REVIEWER_TURN);
240089
- if (!sent) {
240090
- await this.failRun({ ...run2, reviewer_session_id: reviewerId }, "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
240091
- throw new WorkflowError("spawn-failed", "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
240092
- }
240093
- const updated = await this.storage.workflowRuns.update(run2.id, { reviewer_session_id: reviewerId });
240094
240236
  this.trackParticipants(updated);
240237
+ this.armPrepareTimeout(run2.id, PREPARE_TIMEOUT_MS);
240095
240238
  this.emitRunUpdated(updated);
240096
240239
  return updated;
240097
240240
  } catch (err) {
240098
- if (err instanceof WorkflowError && err.code === "spawn-failed") throw err;
240099
240241
  await this.failRun(run2, `\u521B\u5EFA reviewer \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
240100
240242
  throw new WorkflowError("spawn-failed", "\u521B\u5EFA reviewer session \u5931\u8D25");
240101
240243
  }
@@ -240104,6 +240246,73 @@ var WorkflowEngine = class {
240104
240246
  throw err;
240105
240247
  }
240106
240248
  }
240249
+ /**
240250
+ * Phase 2 of the two-phase adhoc review: build the reviewer prompt and send
240251
+ * the first message. Called after the intent brief finished distilling —
240252
+ * inline for single-shot starts, in the background (or over the tunnel, for
240253
+ * remote reviews) after the route already responded. Idempotent: replaying
240254
+ * an already-activated run returns it unchanged, which the durable-intent
240255
+ * replay path relies on.
240256
+ */
240257
+ async activateAdhocReview(runId, opts = {}) {
240258
+ const existing = this.activationFlights.get(runId);
240259
+ if (existing) return existing;
240260
+ const flight = this.runActivation(runId, opts);
240261
+ this.activationFlights.set(runId, flight);
240262
+ const clear = () => {
240263
+ if (this.activationFlights.get(runId) === flight) this.activationFlights.delete(runId);
240264
+ };
240265
+ flight.then(clear, clear);
240266
+ return flight;
240267
+ }
240268
+ async runActivation(runId, opts) {
240269
+ const run2 = await this.storage.workflowRuns.getById(runId);
240270
+ if (!run2) throw new WorkflowError("bad-state", "run \u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u6FC0\u6D3B");
240271
+ if (run2.status !== "preparing") {
240272
+ if (TERMINAL_STATUSES.has(run2.status)) {
240273
+ throw new WorkflowError("bad-state", "run \u5DF2\u7ED3\u675F\uFF0C\u65E0\u6CD5\u6FC0\u6D3B");
240274
+ }
240275
+ return run2;
240276
+ }
240277
+ if (!run2.reviewer_session_id) {
240278
+ throw new WorkflowError("bad-state", "run \u8FD8\u6CA1\u6709 reviewer session\uFF0C\u65E0\u6CD5\u6FC0\u6D3B");
240279
+ }
240280
+ if (!run2.review_target) {
240281
+ throw new WorkflowError("bad-state", "run \u7F3A\u5C11 review target\uFF0C\u65E0\u6CD5\u6FC0\u6D3B");
240282
+ }
240283
+ const pending = this.pendingActivations.get(runId);
240284
+ try {
240285
+ const entries = pending ? null : this.agentOps.getRawMessages(run2.source_session_id);
240286
+ const prompt = buildReviewerPrompt({
240287
+ taskContext: pending ? pending.taskContext : extractTaskContextBefore(entries, run2.source_turn_end_index),
240288
+ originalIntent: pending ? pending.originalIntent : extractFirstUserMessage(entries),
240289
+ authorSelfReport: pending ? pending.authorSelfReport : extractAuthorSelfReport(entries, run2.source_turn_end_index),
240290
+ intentBrief: opts.intentBrief ?? null,
240291
+ blind: opts.blind,
240292
+ reviewFocus: run2.review_focus,
240293
+ target: JSON.parse(run2.review_target),
240294
+ scope: pending?.scope ?? null
240295
+ });
240296
+ const project = await this.storage.projects.getById(run2.project_id);
240297
+ const sent = await this.agentOps.sendUserMessage(run2.reviewer_session_id, prompt, project?.path ?? void 0, void 0, REVIEWER_TURN).catch(() => false);
240298
+ if (!sent) {
240299
+ await this.failRun(run2, "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
240300
+ throw new WorkflowError("spawn-failed", "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
240301
+ }
240302
+ } catch (err) {
240303
+ if (err instanceof WorkflowError && err.code === "spawn-failed") throw err;
240304
+ await this.failRun(run2, `\u6FC0\u6D3B reviewer \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
240305
+ throw new WorkflowError("spawn-failed", "\u6FC0\u6D3B reviewer session \u5931\u8D25");
240306
+ }
240307
+ const claimed = await this.storage.workflowRuns.transition(runId, "preparing", "waiting_reviewer");
240308
+ if (!claimed) {
240309
+ throw new WorkflowError("bad-state", "run \u5728\u51C6\u5907\u671F\u95F4\u5DF2\u88AB\u53D6\u6D88\u6216\u5931\u8D25");
240310
+ }
240311
+ this.clearPendingActivation(runId);
240312
+ const updated = await this.storage.workflowRuns.getById(runId);
240313
+ this.emitRunUpdated(updated);
240314
+ return updated;
240315
+ }
240107
240316
  async handleTaskCompleted(event) {
240108
240317
  const p2 = this.participants.get(event.sessionId);
240109
240318
  if (!p2 || p2.role !== "reviewer") return;
@@ -240221,7 +240430,7 @@ var WorkflowEngine = class {
240221
240430
  if (!run2) return void 0;
240222
240431
  if (TERMINAL_STATUSES.has(run2.status)) return run2;
240223
240432
  const patch = reason ? { error: reason } : void 0;
240224
- const cancelled = await this.storage.workflowRuns.transition(runId, "waiting_reviewer", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_feedback", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "discussing", "cancelled", patch);
240433
+ const cancelled = await this.storage.workflowRuns.transition(runId, "preparing", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_reviewer", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_feedback", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "discussing", "cancelled", patch);
240225
240434
  if (!cancelled) {
240226
240435
  const current = await this.storage.workflowRuns.getById(runId);
240227
240436
  if (current?.status === "sending_feedback") {
@@ -247598,6 +247807,25 @@ var routes11 = async (fastify2) => {
247598
247807
  }
247599
247808
  return reader(session.id, "runtime");
247600
247809
  }
247810
+ function sendBackFields(session) {
247811
+ const parentId = session.branchedFromSessionId;
247812
+ if (!parentId) return {};
247813
+ return {
247814
+ branchedFromSessionId: parentId,
247815
+ branchedFromAvailable: fastify2.agentSessionManager.getSession(parentId) != null,
247816
+ // Entry indices survive the branch copy unchanged, so this tells the UI
247817
+ // which dividers are inherited history (≤) vs the branch's own turns (>)
247818
+ // — send-back only makes sense on the latter.
247819
+ ...session.branchedFromEntryIndex != null ? { branchedFromEntryIndex: session.branchedFromEntryIndex } : {}
247820
+ };
247821
+ }
247822
+ async function mapRemoteSendBackFields(remoteServerId, session) {
247823
+ const { branchedFromSessionId: workerParentId, branchedFromAvailable, branchedFromEntryIndex, ...rest } = session;
247824
+ if (typeof workerParentId !== "string") return rest;
247825
+ const mapping = await fastify2.storage.remoteSessionMappings.getByRemote?.(remoteServerId, workerParentId);
247826
+ if (!mapping) return rest;
247827
+ return { ...rest, branchedFromSessionId: mapping.local_session_id, branchedFromAvailable, branchedFromEntryIndex };
247828
+ }
247601
247829
  async function performLocalBranch(sourceSessionId, userId, opts) {
247602
247830
  const sourceRow = await fastify2.storage.agentSessions.getById(sourceSessionId);
247603
247831
  const sourceProjection = sourceRow ? await projectLocalSessionIdentity(sourceRow) : void 0;
@@ -247633,7 +247861,8 @@ var routes11 = async (fastify2) => {
247633
247861
  permissionMode: session?.permissionMode || "edit",
247634
247862
  agentType: session?.agentType || "claude-code",
247635
247863
  model: session?.model ?? null,
247636
- title: dbRow?.title ?? null
247864
+ title: dbRow?.title ?? null,
247865
+ ...session ? sendBackFields(session) : {}
247637
247866
  },
247638
247867
  messages
247639
247868
  }
@@ -247721,7 +247950,8 @@ var routes11 = async (fastify2) => {
247721
247950
  workspaceCheckoutId: session?.workspaceCheckoutId ?? null,
247722
247951
  worktreePath: projection?.worktreePath ?? session?.checkoutPath ?? null,
247723
247952
  checkoutDeletedAt: projection?.checkoutDeletedAt ?? null,
247724
- processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
247953
+ processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false,
247954
+ ...session ? sendBackFields(session) : {}
247725
247955
  },
247726
247956
  messages: historyWindow ? historyWindow.entries.map((entry) => entry.message) : messages,
247727
247957
  ...historyWindow ? { historyWindow } : {}
@@ -247871,7 +248101,8 @@ var routes11 = async (fastify2) => {
247871
248101
  model: active.model ?? null,
247872
248102
  workspaceCheckoutId: active.workspaceCheckoutId,
247873
248103
  worktreePath: active.checkoutPath,
247874
- processAlive: fastify2.agentSessionManager.getSessionProcessAlive(sessionId)
248104
+ processAlive: fastify2.agentSessionManager.getSessionProcessAlive(sessionId),
248105
+ ...sendBackFields(active)
247875
248106
  },
247876
248107
  messages: fastify2.agentSessionManager.getMessages(sessionId)
247877
248108
  });
@@ -248195,7 +248426,7 @@ var routes11 = async (fastify2) => {
248195
248426
  }
248196
248427
  return reply.code(200).send({
248197
248428
  session: {
248198
- ...remoteData.session,
248429
+ ...await mapRemoteSendBackFields(agentMode, remoteData.session),
248199
248430
  id: localSessionId,
248200
248431
  projectId: req.params.projectId
248201
248432
  },
@@ -248237,7 +248468,8 @@ var routes11 = async (fastify2) => {
248237
248468
  permissionMode: session?.permissionMode || "edit",
248238
248469
  agentType: session?.agentType || "claude-code",
248239
248470
  model: session?.model ?? null,
248240
- processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
248471
+ processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false,
248472
+ ...session ? sendBackFields(session) : {}
248241
248473
  },
248242
248474
  messages: historyWindow ? historyWindow.entries.map((entry) => entry.message) : messages,
248243
248475
  ...historyWindow ? { historyWindow } : {}
@@ -248374,7 +248606,7 @@ var routes11 = async (fastify2) => {
248374
248606
  return reply.code(200).send({
248375
248607
  ...remoteData,
248376
248608
  session: {
248377
- ...remoteData.session,
248609
+ ...await mapRemoteSendBackFields(remoteInfo.remoteServerId, remoteData.session),
248378
248610
  id: req.params.sessionId,
248379
248611
  projectId: registered?.workspace.project_id ?? mapping?.project_id,
248380
248612
  branch: registered ? registered.workspace.branch === "" ? null : registered.workspace.branch : mapping?.branch,
@@ -248419,7 +248651,8 @@ var routes11 = async (fastify2) => {
248419
248651
  permissionMode: session.permissionMode,
248420
248652
  agentType: session.agentType || "claude-code",
248421
248653
  model: session.model ?? null,
248422
- processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id)
248654
+ processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id),
248655
+ ...sendBackFields(session)
248423
248656
  },
248424
248657
  messages
248425
248658
  });
@@ -248450,7 +248683,7 @@ var routes11 = async (fastify2) => {
248450
248683
  return reply.code(200).send({
248451
248684
  ...data,
248452
248685
  session: data.session ? {
248453
- ...data.session,
248686
+ ...await mapRemoteSendBackFields(remoteInfo.remoteServerId, data.session),
248454
248687
  id: req.params.sessionId,
248455
248688
  projectId: projectIdFromRemoteSessionId(req.params.sessionId, remoteInfo),
248456
248689
  branch: remoteInfo.branch ?? null
@@ -248488,7 +248721,8 @@ var routes11 = async (fastify2) => {
248488
248721
  permissionMode: session.permissionMode,
248489
248722
  agentType: session.agentType,
248490
248723
  model: session.model,
248491
- processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id)
248724
+ processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id),
248725
+ ...sendBackFields(session)
248492
248726
  }
248493
248727
  });
248494
248728
  });
@@ -250548,6 +250782,10 @@ function parseReviewSpan(raw) {
250548
250782
  if (raw === void 0) return "this_turn";
250549
250783
  return raw === "this_turn" || raw === "session_start" ? raw : null;
250550
250784
  }
250785
+ function parseReviewContextMode(raw) {
250786
+ if (raw === void 0) return "briefed";
250787
+ return raw === "briefed" || raw === "blind" ? raw : null;
250788
+ }
250551
250789
  function normalizeIntentBrief(raw) {
250552
250790
  if (!raw?.trim()) return void 0;
250553
250791
  return raw.length > 8e3 ? raw.slice(0, 8e3) + "\u2026" : raw;
@@ -250606,6 +250844,34 @@ async function routes20(fastify2) {
250606
250844
  const proxyAuto = (info, method, apiPath, body) => proxyToRemoteAuto(info.remoteServerId, method, apiPath, body, {
250607
250845
  reverseConnectManager: fastify2.reverseConnectManager
250608
250846
  });
250847
+ const TWO_PHASE_REVIEW_CAPABILITIES = [
250848
+ "http:POST /api/path/workflow-runs/prepare",
250849
+ "http:POST /api/path/workflow-runs/:param/activate"
250850
+ ];
250851
+ const activateRemoteReview = async (opts) => {
250852
+ const blind = opts.reviewContextMode === "blind";
250853
+ let intentBrief = opts.clientBrief;
250854
+ if (!opts.clientProvidedBrief && !blind) {
250855
+ intentBrief = await distillIntentBrief(opts.userId, opts.sourceSessionId);
250856
+ }
250857
+ const body = { intentBrief, reviewContextMode: opts.reviewContextMode };
250858
+ for (let attempt = 1; ; attempt++) {
250859
+ const result = await proxyAuto(
250860
+ opts,
250861
+ "POST",
250862
+ `/api/path/workflow-runs/${opts.bareRunId}/activate`,
250863
+ body
250864
+ );
250865
+ if (result.ok) return;
250866
+ if (result.status !== 0 || attempt >= 3) {
250867
+ console.warn(
250868
+ `[WorkflowRuns] remote activation failed for run ${opts.bareRunId}: status=${result.status} code=${result.errorCode ?? "n/a"}`
250869
+ );
250870
+ return;
250871
+ }
250872
+ await new Promise((resolve3) => setTimeout(resolve3, attempt * 5e3));
250873
+ }
250874
+ };
250609
250875
  const sendProxyFailure = (reply, result) => reply.code(proxyStatus(result)).send(
250610
250876
  result.status === 0 ? { error: `Remote proxy failed: ${result.errorCode || "unknown"}` } : result.data
250611
250877
  );
@@ -250655,12 +250921,18 @@ async function routes20(fastify2) {
250655
250921
  return reply.code(400).send({ error: "reviewerSessionId and reviewerAgentType are mutually exclusive" });
250656
250922
  }
250657
250923
  const reviewerSessionId = reviewerSessionIdRaw?.trim();
250924
+ const reviewContextMode = parseReviewContextMode(req.body?.reviewContextMode);
250925
+ if (reviewContextMode === null) return reply.code(400).send({ error: "reviewContextMode must be one of: briefed, blind" });
250926
+ const blind = reviewContextMode === "blind";
250927
+ if (blind && reviewerSessionId) {
250928
+ return reply.code(400).send({ error: "blind review requires a new reviewer session" });
250929
+ }
250658
250930
  const intentBriefRaw = req.body?.intentBrief;
250659
250931
  if (intentBriefRaw !== void 0 && typeof intentBriefRaw !== "string") {
250660
250932
  return reply.code(400).send({ error: "intentBrief must be a string" });
250661
250933
  }
250662
250934
  const clientProvidedBrief = intentBriefRaw !== void 0;
250663
- const clientBrief = normalizeIntentBrief(intentBriefRaw);
250935
+ const clientBrief = blind ? void 0 : normalizeIntentBrief(intentBriefRaw);
250664
250936
  if (sourceSessionId.startsWith("remote-")) {
250665
250937
  const remoteInfo = fastify2.remoteSessionMap.get(sourceSessionId);
250666
250938
  if (!remoteInfo) return reply.code(404).send({ error: "Session not found" });
@@ -250682,10 +250954,6 @@ async function routes20(fastify2) {
250682
250954
  }
250683
250955
  bareReviewerSessionId = reviewerInfo.remoteSessionId;
250684
250956
  }
250685
- let intentBrief2 = clientBrief;
250686
- if (!clientProvidedBrief && !bareReviewerSessionId) {
250687
- intentBrief2 = await distillIntentBrief(userId, sourceSessionId);
250688
- }
250689
250957
  if (reviewerSessionId && !await fastify2.remoteNotificationSync.prepareForNewTurn(reviewerSessionId)) {
250690
250958
  return reply.code(502).send({
250691
250959
  error: "Could not reach the remote server to prepare notification delivery",
@@ -250694,6 +250962,7 @@ async function routes20(fastify2) {
250694
250962
  }
250695
250963
  const reviewerActivityAt = Date.now();
250696
250964
  let bareRun;
250965
+ let twoPhase = false;
250697
250966
  if (bareReviewerSessionId) {
250698
250967
  const result = await proxyAuto(remoteInfo, "POST", "/api/path/workflow-runs", {
250699
250968
  sourceSessionId: remoteInfo.remoteSessionId,
@@ -250701,7 +250970,7 @@ async function routes20(fastify2) {
250701
250970
  sourceTurnEndIndex,
250702
250971
  reviewSpan,
250703
250972
  reviewerSessionId: bareReviewerSessionId,
250704
- intentBrief: intentBrief2
250973
+ intentBrief: clientBrief
250705
250974
  });
250706
250975
  if (!result.ok) return sendProxyFailure(reply, result);
250707
250976
  bareRun = result.data.run;
@@ -250711,6 +250980,14 @@ async function routes20(fastify2) {
250711
250980
  remoteInfo.remoteServerId
250712
250981
  );
250713
250982
  if (!remoteConfig) return reply.code(404).send({ error: "Remote project configuration not found" });
250983
+ const server = await fastify2.storage.remoteServers.getById(remoteInfo.remoteServerId);
250984
+ twoPhase = TWO_PHASE_REVIEW_CAPABILITIES.every(
250985
+ (capability) => server?.worker_capabilities?.includes(capability)
250986
+ );
250987
+ let intentBrief = clientBrief;
250988
+ if (!twoPhase && !clientProvidedBrief && !blind) {
250989
+ intentBrief = await distillIntentBrief(userId, sourceSessionId);
250990
+ }
250714
250991
  const result = await createRemoteWorkflowReviewer({
250715
250992
  remoteSessionMap: fastify2.remoteSessionMap,
250716
250993
  remoteSessionMappings: fastify2.storage.remoteSessionMappings,
@@ -250727,9 +251004,14 @@ async function routes20(fastify2) {
250727
251004
  reviewFocus,
250728
251005
  sourceTurnEndIndex,
250729
251006
  reviewSpan,
251007
+ // Additive tunnel field: a worker that predates it ignores the flag
251008
+ // and runs a briefed (tier-2) review — the reviewer prompt's
251009
+ // trailing "(review context: …)" line records what actually ran.
251010
+ reviewContextMode,
250730
251011
  reviewerAgentType: reviewerAgentType ?? "claude-code",
250731
- intentBrief: intentBrief2,
250732
- userId
251012
+ intentBrief,
251013
+ userId,
251014
+ ...twoPhase ? { phase: "prepare" } : {}
250733
251015
  });
250734
251016
  if (!result.ok) return reply.code(proxyStatus(result)).send(result.data);
250735
251017
  bareRun = result.remoteRun;
@@ -250800,18 +251082,34 @@ async function routes20(fastify2) {
250800
251082
  sessionId: localRun.reviewer_session_id,
250801
251083
  alive: true
250802
251084
  });
250803
- fastify2.eventBus.emit({
250804
- type: "session:status",
250805
- projectId,
250806
- branch: bareRun.branch,
250807
- sessionId: localRun.reviewer_session_id,
250808
- status: "running"
250809
- });
251085
+ if (!twoPhase) {
251086
+ fastify2.eventBus.emit({
251087
+ type: "session:status",
251088
+ projectId,
251089
+ branch: bareRun.branch,
251090
+ sessionId: localRun.reviewer_session_id,
251091
+ status: "running"
251092
+ });
251093
+ }
250810
251094
  }
250811
251095
  fastify2.agentSessionManager.markTitleResolved(localRun.reviewer_session_id);
250812
251096
  await fastify2.storage.remoteSessionMappings.markTitleResolved(localRun.reviewer_session_id);
250813
251097
  }
250814
251098
  fastify2.eventBus.emit({ type: "workflow:run-updated", projectId, branch: bareRun.branch, run: localRun });
251099
+ if (twoPhase) {
251100
+ const bareRunId = bareRun.id;
251101
+ void activateRemoteReview({
251102
+ remoteServerId: remoteInfo.remoteServerId,
251103
+ bareRunId,
251104
+ sourceSessionId,
251105
+ userId,
251106
+ reviewContextMode,
251107
+ clientBrief,
251108
+ clientProvidedBrief
251109
+ }).catch((err) => {
251110
+ console.warn(`[WorkflowRuns] remote activation task failed for run ${bareRunId}:`, err);
251111
+ });
251112
+ }
250815
251113
  return reply.code(201).send({ run: localRun });
250816
251114
  }
250817
251115
  const project = await fastify2.storage.projects.getById(projectId, userId);
@@ -250826,9 +251124,33 @@ async function routes20(fastify2) {
250826
251124
  if (branch !== void 0 && (branch || null) !== runBranch) {
250827
251125
  return reply.code(400).send({ error: "branch does not match source session" });
250828
251126
  }
250829
- let intentBrief = clientBrief;
250830
- if (!clientProvidedBrief && !reviewerSessionId) {
250831
- intentBrief = await distillIntentBrief(userId, sourceSessionId);
251127
+ if (!reviewerSessionId) {
251128
+ let run2;
251129
+ try {
251130
+ run2 = await fastify2.workflowEngine.prepareAdhocReview({
251131
+ project: { id: project.id, path: project.path },
251132
+ branch: runBranch,
251133
+ sourceSessionId,
251134
+ reviewFocus,
251135
+ sourceTurnEndIndex,
251136
+ reviewSpan,
251137
+ reviewerAgentType
251138
+ });
251139
+ } catch (err) {
251140
+ const status = errStatus(err);
251141
+ if (status) return reply.code(status).send({ error: err.message });
251142
+ throw err;
251143
+ }
251144
+ void (async () => {
251145
+ let intentBrief = clientBrief;
251146
+ if (!clientProvidedBrief && !blind) {
251147
+ intentBrief = await distillIntentBrief(userId, sourceSessionId);
251148
+ }
251149
+ await fastify2.workflowEngine.activateAdhocReview(run2.id, { intentBrief, blind });
251150
+ })().catch((err) => {
251151
+ console.warn(`[WorkflowRuns] background activation failed for run ${run2.id}:`, err);
251152
+ });
251153
+ return reply.code(201).send({ run: run2 });
250832
251154
  }
250833
251155
  try {
250834
251156
  const run2 = await fastify2.workflowEngine.startAdhocReview({
@@ -250838,9 +251160,9 @@ async function routes20(fastify2) {
250838
251160
  reviewFocus,
250839
251161
  sourceTurnEndIndex,
250840
251162
  reviewSpan,
250841
- reviewerAgentType,
250842
251163
  reviewerSessionId,
250843
- intentBrief
251164
+ intentBrief: clientBrief,
251165
+ blind
250844
251166
  });
250845
251167
  return reply.code(201).send({ run: run2 });
250846
251168
  } catch (err) {
@@ -251057,11 +251379,14 @@ async function routes20(fastify2) {
251057
251379
  if (!sourceSessionId) return reply.code(400).send({ error: "sourceSessionId is required" });
251058
251380
  const reviewSpan = parseReviewSpan(req.body?.reviewSpan);
251059
251381
  if (reviewSpan === null) return reply.code(400).send({ error: "reviewSpan must be one of: this_turn, session_start" });
251382
+ const reviewContextMode = parseReviewContextMode(req.body?.reviewContextMode);
251383
+ if (reviewContextMode === null) return reply.code(400).send({ error: "reviewContextMode must be one of: briefed, blind" });
251384
+ const blind = reviewContextMode === "blind";
251060
251385
  const intentBriefRaw = req.body?.intentBrief;
251061
251386
  if (intentBriefRaw !== void 0 && typeof intentBriefRaw !== "string") {
251062
251387
  return reply.code(400).send({ error: "intentBrief must be a string" });
251063
251388
  }
251064
- const intentBrief = normalizeIntentBrief(intentBriefRaw);
251389
+ const intentBrief = blind ? void 0 : normalizeIntentBrief(intentBriefRaw);
251065
251390
  const reviewerAgentType = parseReviewerAgentType(req.body?.reviewerAgentType);
251066
251391
  if (reviewerAgentType === null) return reply.code(400).send({ error: "reviewerAgentType must be one of: claude-code, codex" });
251067
251392
  const reviewerSessionIdRaw = req.body?.reviewerSessionId;
@@ -251099,6 +251424,7 @@ async function routes20(fastify2) {
251099
251424
  reviewerAgentType,
251100
251425
  reviewerSessionId,
251101
251426
  intentBrief,
251427
+ blind,
251102
251428
  runId: runId || void 0,
251103
251429
  newReviewerSessionId: newReviewerSessionId || void 0
251104
251430
  });
@@ -251121,6 +251447,76 @@ async function routes20(fastify2) {
251121
251447
  throw err;
251122
251448
  }
251123
251449
  });
251450
+ fastify2.post("/api/path/workflow-runs/prepare", async (req, reply) => {
251451
+ const userId = requireAuth(req, reply);
251452
+ if (userId === null) return;
251453
+ const { sourceSessionId, reviewFocus, sourceTurnEndIndex } = req.body ?? {};
251454
+ if (!sourceSessionId) return reply.code(400).send({ error: "sourceSessionId is required" });
251455
+ const reviewSpan = parseReviewSpan(req.body?.reviewSpan);
251456
+ if (reviewSpan === null) return reply.code(400).send({ error: "reviewSpan must be one of: this_turn, session_start" });
251457
+ const reviewerAgentType = parseReviewerAgentType(req.body?.reviewerAgentType);
251458
+ if (reviewerAgentType === null) return reply.code(400).send({ error: "reviewerAgentType must be one of: claude-code, codex" });
251459
+ const runId = typeof req.body?.runId === "string" ? req.body.runId.trim() : "";
251460
+ const newReviewerSessionId = typeof req.body?.newReviewerSessionId === "string" ? req.body.newReviewerSessionId.trim() : "";
251461
+ if (!runId || !newReviewerSessionId) {
251462
+ return reply.code(400).send({ error: "runId and newReviewerSessionId are required" });
251463
+ }
251464
+ const sourceSession = await fastify2.storage.agentSessions.getById(sourceSessionId);
251465
+ if (!sourceSession) return reply.code(404).send({ error: "Session not found" });
251466
+ const sourceProjection = await projectLocalSession(sourceSession);
251467
+ if (!sourceProjection) return reply.code(409).send({ error: "Session workspace binding is unavailable" });
251468
+ const project = await fastify2.storage.projects.getById(sourceProjection.projectId);
251469
+ if (!project) return reply.code(404).send({ error: "Session not found" });
251470
+ if (!project.path) return reply.code(400).send({ error: "Project has no local path" });
251471
+ const projectPath = project.path;
251472
+ try {
251473
+ let flight = durableReviewFlights.get(runId);
251474
+ if (!flight) {
251475
+ flight = fastify2.workflowEngine.prepareAdhocReview({
251476
+ project: { id: project.id, path: projectPath },
251477
+ branch: sourceProjection.branch,
251478
+ sourceSessionId,
251479
+ reviewFocus,
251480
+ sourceTurnEndIndex,
251481
+ reviewSpan,
251482
+ reviewerAgentType,
251483
+ runId,
251484
+ newReviewerSessionId
251485
+ });
251486
+ durableReviewFlights.set(runId, flight);
251487
+ const clear = () => {
251488
+ if (durableReviewFlights.get(runId) === flight) durableReviewFlights.delete(runId);
251489
+ };
251490
+ void flight.then(clear, clear);
251491
+ }
251492
+ const run2 = await flight;
251493
+ return reply.code(201).send({ run: run2 });
251494
+ } catch (err) {
251495
+ const status = errStatus(err);
251496
+ if (status) return reply.code(status).send({ error: err.message });
251497
+ throw err;
251498
+ }
251499
+ });
251500
+ fastify2.post("/api/path/workflow-runs/:id/activate", async (req, reply) => {
251501
+ const userId = requireAuth(req, reply);
251502
+ if (userId === null) return;
251503
+ const reviewContextMode = parseReviewContextMode(req.body?.reviewContextMode);
251504
+ if (reviewContextMode === null) return reply.code(400).send({ error: "reviewContextMode must be one of: briefed, blind" });
251505
+ const blind = reviewContextMode === "blind";
251506
+ const intentBriefRaw = req.body?.intentBrief;
251507
+ if (intentBriefRaw !== void 0 && typeof intentBriefRaw !== "string") {
251508
+ return reply.code(400).send({ error: "intentBrief must be a string" });
251509
+ }
251510
+ const intentBrief = blind ? void 0 : normalizeIntentBrief(intentBriefRaw);
251511
+ try {
251512
+ const run2 = await fastify2.workflowEngine.activateAdhocReview(req.params.id, { intentBrief, blind });
251513
+ return reply.send({ run: run2 });
251514
+ } catch (err) {
251515
+ const status = errStatus(err);
251516
+ if (status) return reply.code(status).send({ error: err.message });
251517
+ throw err;
251518
+ }
251519
+ });
251124
251520
  fastify2.get("/api/path/workflow-runs/reviewer-candidate", async (req, reply) => {
251125
251521
  const userId = requireAuth(req, reply);
251126
251522
  if (userId === null) return;
@@ -252043,6 +252439,16 @@ var routes23 = async (fastify2) => {
252043
252439
  const stopHeartbeat = attachWsHeartbeat(socket, { label: "ExecutorMux" });
252044
252440
  const subs = /* @__PURE__ */ new Map();
252045
252441
  const handleInputMap = /* @__PURE__ */ new Map();
252442
+ const wanted = /* @__PURE__ */ new Set();
252443
+ const dropSubscription = (processId) => {
252444
+ const sub = subs.get(processId);
252445
+ if (sub) {
252446
+ sub.detach();
252447
+ sub.cleanup();
252448
+ subs.delete(processId);
252449
+ }
252450
+ handleInputMap.delete(processId);
252451
+ };
252046
252452
  const subscribeProcess = async (processId) => {
252047
252453
  if (subs.has(processId)) return;
252048
252454
  const ownerUserId = processOwnerScope(principal);
@@ -252055,25 +252461,31 @@ var routes23 = async (fastify2) => {
252055
252461
  return;
252056
252462
  }
252057
252463
  if (subs.has(processId)) return;
252464
+ if (!wanted.has(processId)) return;
252465
+ let detached = false;
252058
252466
  const send = (msg) => {
252467
+ if (detached) return;
252059
252468
  try {
252060
252469
  socket.send(JSON.stringify({ processId, ...msg }));
252061
252470
  } catch {
252062
252471
  }
252063
252472
  };
252064
252473
  let terminated = false;
252474
+ let self2 = null;
252065
252475
  const onTerminal = () => {
252066
252476
  terminated = true;
252067
- const c = subs.get(processId);
252068
- if (c) {
252069
- c();
252477
+ if (self2 && subs.get(processId) === self2) {
252070
252478
  subs.delete(processId);
252479
+ handleInputMap.delete(processId);
252071
252480
  }
252072
- handleInputMap.delete(processId);
252481
+ self2?.cleanup();
252073
252482
  };
252074
252483
  const handle = processId.startsWith("remote-") ? attachRemoteProcessStream(fastify2, processId, send, onTerminal) : attachLocalProcessStream(fastify2, processId, send, onTerminal);
252075
252484
  if (!terminated) {
252076
- subs.set(processId, handle.cleanup);
252485
+ self2 = { cleanup: handle.cleanup, detach: () => {
252486
+ detached = true;
252487
+ } };
252488
+ subs.set(processId, self2);
252077
252489
  handleInputMap.set(processId, handle.handleInput);
252078
252490
  }
252079
252491
  };
@@ -252081,13 +252493,13 @@ var routes23 = async (fastify2) => {
252081
252493
  try {
252082
252494
  const msg = JSON.parse(data.toString());
252083
252495
  if (msg.type === "subscribe") {
252496
+ wanted.add(msg.processId);
252084
252497
  subscribeProcess(msg.processId).catch((err) => {
252085
252498
  console.error(`[ExecutorMux] Failed to subscribe to ${msg.processId}:`, err);
252086
252499
  });
252087
252500
  } else if (msg.type === "unsubscribe") {
252088
- subs.get(msg.processId)?.();
252089
- subs.delete(msg.processId);
252090
- handleInputMap.delete(msg.processId);
252501
+ wanted.delete(msg.processId);
252502
+ dropSubscription(msg.processId);
252091
252503
  } else if (msg.type === "input") {
252092
252504
  handleInputMap.get(msg.processId)?.({ type: "input", data: msg.data });
252093
252505
  } else if (msg.type === "resize") {
@@ -252103,7 +252515,11 @@ var routes23 = async (fastify2) => {
252103
252515
  socket.on("close", () => {
252104
252516
  console.log(`[ExecutorMux] Client disconnected; cleaning ${subs.size} subscriptions`);
252105
252517
  stopHeartbeat();
252106
- for (const cleanup of subs.values()) cleanup();
252518
+ wanted.clear();
252519
+ for (const sub of subs.values()) {
252520
+ sub.detach();
252521
+ sub.cleanup();
252522
+ }
252107
252523
  subs.clear();
252108
252524
  handleInputMap.clear();
252109
252525
  });
@@ -252185,6 +252601,12 @@ var routes23 = async (fastify2) => {
252185
252601
  }));
252186
252602
  } catch {
252187
252603
  }
252604
+ if (cacheEntry.backgroundTasks !== null) {
252605
+ try {
252606
+ socket.send(cacheEntry.backgroundTasks);
252607
+ } catch {
252608
+ }
252609
+ }
252188
252610
  for (const raw of cacheEntry.messages) {
252189
252611
  if (replayAfter >= 0) {
252190
252612
  try {
@@ -252208,12 +252630,6 @@ var routes23 = async (fastify2) => {
252208
252630
  socket.send(JSON.stringify({ Ready: true, historyEpoch: cacheEntry.historyEpoch ?? void 0 }));
252209
252631
  } catch {
252210
252632
  }
252211
- if (cacheEntry.backgroundTasks !== null) {
252212
- try {
252213
- socket.send(cacheEntry.backgroundTasks);
252214
- } catch {
252215
- }
252216
- }
252217
252633
  if (cacheEntry.finished) {
252218
252634
  try {
252219
252635
  socket.send(JSON.stringify({ finished: true }));
@@ -260806,6 +261222,12 @@ var WORKER_CAPABILITIES = {
260806
261222
  // --- Workflow runs ---
260807
261223
  // Empirically bisected via cross-version e2e: 0.2.4 → 404, 0.2.5 → serves.
260808
261224
  "http:POST /api/path/workflow-runs": { since: "0.2.5", summary: "\u521B\u5EFA workflow run" },
261225
+ // Both additive, checked as a pair (TWO_PHASE_REVIEW_CAPABILITIES in
261226
+ // workflow-run-routes.ts): a worker missing either gets the original
261227
+ // single-shot create — the hub distills inline and the submit blocks on it,
261228
+ // exactly the pre-two-phase behavior.
261229
+ "http:POST /api/path/workflow-runs/prepare": { since: "0.3.30", summary: "\u4E24\u6BB5\u5F0F review:\u51C6\u5907(\u5360\u4F4D run + reviewer session)" },
261230
+ "http:POST /api/path/workflow-runs/:param/activate": { since: "0.3.30", summary: "\u4E24\u6BB5\u5F0F review:\u6FC0\u6D3B(\u6295\u9012 reviewer \u9996\u6761\u6D88\u606F)" },
260809
261231
  "http:GET /api/path/workflow-runs": { since: "0.2.5", summary: "workflow run \u5217\u8868" },
260810
261232
  "http:GET /api/path/workflow-runs/reviewer-candidate": { since: "0.2.5", summary: "reviewer \u5019\u9009\u67E5\u8BE2" },
260811
261233
  "http:GET /api/workflow-runs/:param": { since: "0.2.5", summary: "\u8BFB workflow run" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedeckx/linux-x64",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "description": "Vibedeckx platform binaries for Linux x64",
5
5
  "os": [
6
6
  "linux"