@vibedeckx/linux-x64 0.3.29 → 0.3.31

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 +414 -65
  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",
@@ -188476,7 +188477,7 @@ var createWorkflowRunRepos = (kdb) => ({
188476
188477
  ...opts,
188477
188478
  reviewer_session_id: opts.reviewer_session_id ?? null,
188478
188479
  review_span: opts.review_span ?? "this_turn",
188479
- status: "waiting_reviewer"
188480
+ status: opts.status ?? "waiting_reviewer"
188480
188481
  }).execute();
188481
188482
  const row = await kdb.selectFrom("workflow_runs").selectAll().where("id", "=", opts.id).executeTakeFirstOrThrow();
188482
188483
  return asRun(row);
@@ -188504,6 +188505,10 @@ var createWorkflowRunRepos = (kdb) => ({
188504
188505
  const row = await kdb.selectFrom("workflow_runs").selectAll().where("source_session_id", "=", sourceSessionId).where("status", "=", "completed").where("reviewer_session_id", "is not", null).orderBy("created_at", "desc").orderBy(sql`rowid`, "desc").executeTakeFirst();
188505
188506
  return row ? asRun(row) : void 0;
188506
188507
  },
188508
+ listReviewedSourceSessions: async (projectId, branch) => {
188509
+ const rows = await kdb.selectFrom("workflow_runs").select("source_session_id").distinct().where("project_id", "=", projectId).where("branch", "is", branch).where("status", "=", "completed").where("reviewer_session_id", "is not", null).execute();
188510
+ return rows.map((r) => r.source_session_id);
188511
+ },
188507
188512
  update: async (id, patch) => {
188508
188513
  if (Object.keys(patch).length > 0) {
188509
188514
  await kdb.updateTable("workflow_runs").set({ ...patch, updated_at: sql`datetime('now')` }).where("id", "=", id).execute();
@@ -205223,6 +205228,12 @@ var initializeSchema = (db) => {
205223
205228
  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
205224
205229
  );
205225
205230
 
205231
+ -- getActive (panel poll, every 5s per open workspace) and
205232
+ -- listReviewedSourceSessions (same request) both scope by project+branch
205233
+ -- and differ only in the status filter.
205234
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_project_branch_status
205235
+ ON workflow_runs(project_id, branch, status);
205236
+
205226
205237
  CREATE TABLE IF NOT EXISTS turn_snapshots (
205227
205238
  session_id TEXT NOT NULL,
205228
205239
  turn_end_index INTEGER NOT NULL,
@@ -233410,22 +233421,32 @@ async function createRemoteWorkflowReviewer(deps, params) {
233410
233421
  });
233411
233422
  let registeredLocalSessionId = null;
233412
233423
  try {
233413
- const result = await proxyToRemoteAuto(
233424
+ const sharedBody = {
233425
+ sourceSessionId: params.sourceRemoteSessionId,
233426
+ reviewFocus: params.reviewFocus,
233427
+ sourceTurnEndIndex: params.sourceTurnEndIndex,
233428
+ reviewSpan: params.reviewSpan,
233429
+ reviewerAgentType: params.reviewerAgentType,
233430
+ runId: remoteRunId,
233431
+ newReviewerSessionId: remoteReviewerSessionId
233432
+ };
233433
+ const proxyOpts = { reverseConnectManager: deps.reverseConnectManager ?? void 0 };
233434
+ const result = params.phase === "prepare" ? await proxyToRemoteAuto(
233435
+ params.agentMode,
233436
+ "POST",
233437
+ "/api/path/workflow-runs/prepare",
233438
+ sharedBody,
233439
+ proxyOpts
233440
+ ) : await proxyToRemoteAuto(
233414
233441
  params.agentMode,
233415
233442
  "POST",
233416
233443
  "/api/path/workflow-runs",
233417
233444
  {
233418
- sourceSessionId: params.sourceRemoteSessionId,
233419
- reviewFocus: params.reviewFocus,
233420
- sourceTurnEndIndex: params.sourceTurnEndIndex,
233421
- reviewSpan: params.reviewSpan,
233445
+ ...sharedBody,
233422
233446
  reviewContextMode: params.reviewContextMode,
233423
- reviewerAgentType: params.reviewerAgentType,
233424
- intentBrief: params.intentBrief,
233425
- runId: remoteRunId,
233426
- newReviewerSessionId: remoteReviewerSessionId
233447
+ intentBrief: params.intentBrief
233427
233448
  },
233428
- { reverseConnectManager: deps.reverseConnectManager ?? void 0 }
233449
+ proxyOpts
233429
233450
  );
233430
233451
  if (!result.ok) {
233431
233452
  const uncertain = result.errorCode === "network_error" || result.errorCode === "timeout";
@@ -239802,6 +239823,7 @@ var FINAL_VERDICT_PROMPT = [
239802
239823
  ...VERDICT_INSTRUCTIONS
239803
239824
  ].join("\n");
239804
239825
  var REVIEWER_AGENT_TYPES = /* @__PURE__ */ new Set(["claude-code", "codex"]);
239826
+ var PREPARE_TIMEOUT_MS = 10 * 6e4;
239805
239827
  var WorkflowEngine = class {
239806
239828
  constructor(storage2, agentOps) {
239807
239829
  this.storage = storage2;
@@ -239812,6 +239834,12 @@ var WorkflowEngine = class {
239812
239834
  eventBus;
239813
239835
  /** sessionId → participation in an active run (rebuilt on boot). */
239814
239836
  participants = /* @__PURE__ */ new Map();
239837
+ /** runId → prompt inputs captured at prepare time (see PendingActivation). */
239838
+ pendingActivations = /* @__PURE__ */ new Map();
239839
+ /** runId → armed preparation-timeout timer. */
239840
+ prepareTimers = /* @__PURE__ */ new Map();
239841
+ /** runId → in-flight activation, so concurrent calls join instead of double-sending. */
239842
+ activationFlights = /* @__PURE__ */ new Map();
239815
239843
  setEventBus(bus) {
239816
239844
  this.eventBus = bus;
239817
239845
  bus.subscribe((event) => {
@@ -239836,6 +239864,12 @@ var WorkflowEngine = class {
239836
239864
  await this.storage.workflowRuns.update(run2.id, {
239837
239865
  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"
239838
239866
  });
239867
+ } else if (run2.status === "preparing") {
239868
+ const createdAt = Date.parse(
239869
+ run2.created_at.includes("T") ? run2.created_at : run2.created_at.replace(" ", "T") + "Z"
239870
+ );
239871
+ const elapsed = Number.isFinite(createdAt) ? Date.now() - createdAt : PREPARE_TIMEOUT_MS;
239872
+ this.armPrepareTimeout(run2.id, PREPARE_TIMEOUT_MS - elapsed);
239839
239873
  }
239840
239874
  this.trackParticipants(run2);
239841
239875
  }
@@ -239850,6 +239884,37 @@ var WorkflowEngine = class {
239850
239884
  for (const [sid, p2] of this.participants) {
239851
239885
  if (p2.runId === run2.id) this.participants.delete(sid);
239852
239886
  }
239887
+ this.clearPendingActivation(run2.id);
239888
+ }
239889
+ clearPendingActivation(runId) {
239890
+ this.pendingActivations.delete(runId);
239891
+ const timer = this.prepareTimers.get(runId);
239892
+ if (timer) {
239893
+ clearTimeout(timer);
239894
+ this.prepareTimers.delete(runId);
239895
+ }
239896
+ }
239897
+ /**
239898
+ * Backstop for a run stuck in `preparing` (see PREPARE_TIMEOUT_MS). A timed
239899
+ * out preparation becomes a normal failed run — visible in the UI and as a
239900
+ * workflow_failed milestone — instead of a placeholder that spins forever.
239901
+ * `unref` so an armed timer never holds the process open.
239902
+ */
239903
+ armPrepareTimeout(runId, delayMs) {
239904
+ const existing = this.prepareTimers.get(runId);
239905
+ if (existing) clearTimeout(existing);
239906
+ const timer = setTimeout(() => {
239907
+ this.prepareTimers.delete(runId);
239908
+ void (async () => {
239909
+ const run2 = await this.storage.workflowRuns.getById(runId);
239910
+ if (run2?.status === "preparing") {
239911
+ 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");
239912
+ }
239913
+ this.pendingActivations.delete(runId);
239914
+ })().catch((err) => console.error("[WorkflowEngine] prepare-timeout sweep failed:", err));
239915
+ }, Math.max(0, delayMs));
239916
+ timer.unref?.();
239917
+ this.prepareTimers.set(runId, timer);
239853
239918
  }
239854
239919
  releaseReservations(runId) {
239855
239920
  for (const [sid, participant] of this.participants) {
@@ -239890,7 +239955,10 @@ var WorkflowEngine = class {
239890
239955
  );
239891
239956
  if (!ok) return;
239892
239957
  const failed = await this.storage.workflowRuns.getById(run2.id);
239893
- if (failed) this.untrackRun(failed);
239958
+ if (failed) {
239959
+ this.untrackRun(failed);
239960
+ this.emitRunUpdated(failed);
239961
+ }
239894
239962
  this.onMilestoneCreated?.();
239895
239963
  }
239896
239964
  /** Test seam for the failure path (see failRun). */
@@ -239959,7 +240027,29 @@ var WorkflowEngine = class {
239959
240027
  reason: null
239960
240028
  };
239961
240029
  }
240030
+ /**
240031
+ * Single-shot start, preserved for old callers and durable-intent replays:
240032
+ * prepare + activate back to back. The reuse-reviewer path delivers its
240033
+ * prompt inside prepare (no preparing state — nothing distills for a
240034
+ * re-review); a fresh reviewer comes back `preparing` and is activated
240035
+ * inline, which also completes a replayed run that a previous caller
240036
+ * prepared but never activated.
240037
+ */
239962
240038
  async startAdhocReview(opts) {
240039
+ const run2 = await this.prepareAdhocReview(opts);
240040
+ if (opts.reviewerSessionId || run2.status !== "preparing") return run2;
240041
+ return this.activateAdhocReview(run2.id, { intentBrief: opts.intentBrief, blind: opts.blind });
240042
+ }
240043
+ /**
240044
+ * Phase 1 of the two-phase adhoc review: validate, reserve participants,
240045
+ * create the run row and (for a fresh review) the reviewer session — titled
240046
+ * and visible in the sidebar — WITHOUT sending the reviewer prompt. Fast
240047
+ * (no model calls), so the route can respond immediately and let the
240048
+ * intent-brief distillation happen after the user has moved on. The run
240049
+ * stays `preparing` until activateAdhocReview delivers the first message,
240050
+ * bounded by PREPARE_TIMEOUT_MS.
240051
+ */
240052
+ async prepareAdhocReview(opts) {
239963
240053
  if (opts.reviewerSessionId === opts.sourceSessionId) {
239964
240054
  throw new WorkflowError("reviewer-unavailable", "reviewer session \u4E0D\u80FD\u4E0E source session \u76F8\u540C");
239965
240055
  }
@@ -239982,7 +240072,7 @@ var WorkflowEngine = class {
239982
240072
  }
239983
240073
  return existingRun;
239984
240074
  }
239985
- if (existingRun.status !== "waiting_reviewer") {
240075
+ if (existingRun.status !== "waiting_reviewer" && existingRun.status !== "preparing") {
239986
240076
  throw new WorkflowError("bad-state", "workflow run \u5DF2\u7EC8\u6B62\uFF0C\u4E0D\u80FD\u4F5C\u4E3A\u672A\u77E5\u521B\u5EFA\u7ED3\u679C\u91CD\u653E");
239987
240077
  }
239988
240078
  }
@@ -240060,9 +240150,17 @@ var WorkflowEngine = class {
240060
240150
  review_focus: opts.reviewFocus ?? null,
240061
240151
  review_target: JSON.stringify(target),
240062
240152
  reviewer_session_id: opts.reviewerSessionId ?? null,
240063
- review_span: opts.reviewSpan ?? "this_turn"
240153
+ review_span: opts.reviewSpan ?? "this_turn",
240154
+ // Reuse skips the preparing state entirely: its prompt is delivered
240155
+ // below, before this method returns.
240156
+ status: opts.reviewerSessionId ? "waiting_reviewer" : "preparing"
240064
240157
  });
240065
240158
  this.trackParticipants(run2);
240159
+ if (existingRun && !existingRun.reviewer_session_id && !opts.reviewerSessionId && run2.status === "waiting_reviewer") {
240160
+ const flipped = await this.storage.workflowRuns.transition(run2.id, "waiting_reviewer", "preparing");
240161
+ if (!flipped) throw new WorkflowError("bad-state", "run \u5728\u91CD\u653E\u671F\u95F4\u5DF2\u88AB\u53D6\u6D88\u6216\u5931\u8D25");
240162
+ run2.status = "preparing";
240163
+ }
240066
240164
  if (opts.reviewerSessionId && reviewerSession) {
240067
240165
  if (reviewerSession.permission_mode !== "plan") {
240068
240166
  let switched = false;
@@ -240113,7 +240211,13 @@ var WorkflowEngine = class {
240113
240211
  false,
240114
240212
  "plan",
240115
240213
  opts.reviewerAgentType ?? "claude-code",
240116
- true,
240214
+ // announceRunning=false — deliberate change from the single-shot
240215
+ // start: the preparing reviewer must appear in the sidebar (the
240216
+ // spawn's unconditional session:process emit covers that) WITHOUT
240217
+ // the session:status "running" emit that auto-surfaces it into the
240218
+ // open agent window. The user stays on the source session and opens
240219
+ // the reviewer from the sidebar when they choose to.
240220
+ false,
240117
240221
  false,
240118
240222
  {
240119
240223
  startSnapshot: endSnap,
@@ -240125,27 +240229,25 @@ var WorkflowEngine = class {
240125
240229
  reviewerId,
240126
240230
  `Review - ${sourceSession?.title || (taskContext ? snippetTitle(taskContext) : null) || "Conversation"}`
240127
240231
  ).catch((err) => console.warn(`[WorkflowEngine] failed to set reviewer title for ${reviewerId}:`, err));
240128
- const prompt = buildReviewerPrompt({
240232
+ this.pendingActivations.set(run2.id, {
240233
+ scope,
240129
240234
  taskContext,
240130
240235
  originalIntent: extractFirstUserMessage(entries),
240131
- authorSelfReport: extractAuthorSelfReport(entries, turnEndIndex),
240132
- intentBrief: opts.intentBrief ?? null,
240133
- blind: opts.blind,
240134
- reviewFocus: opts.reviewFocus ?? null,
240135
- target,
240136
- scope
240236
+ authorSelfReport: extractAuthorSelfReport(entries, turnEndIndex)
240237
+ });
240238
+ const updated = await this.storage.workflowRuns.update(run2.id, {
240239
+ reviewer_session_id: reviewerId,
240240
+ // Legacy replay backfill: a durable run interrupted before reviewer
240241
+ // binding may carry no stored target. Activation builds the prompt
240242
+ // from the stored one, so persist the target just captured (same
240243
+ // worktree, cutoff verified above to match the stored run).
240244
+ ...run2.review_target ? {} : { review_target: JSON.stringify(target) }
240137
240245
  });
240138
- const sent = await this.agentOps.sendUserMessage(reviewerId, prompt, opts.project.path, void 0, REVIEWER_TURN);
240139
- if (!sent) {
240140
- await this.failRun({ ...run2, reviewer_session_id: reviewerId }, "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
240141
- throw new WorkflowError("spawn-failed", "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
240142
- }
240143
- const updated = await this.storage.workflowRuns.update(run2.id, { reviewer_session_id: reviewerId });
240144
240246
  this.trackParticipants(updated);
240247
+ this.armPrepareTimeout(run2.id, PREPARE_TIMEOUT_MS);
240145
240248
  this.emitRunUpdated(updated);
240146
240249
  return updated;
240147
240250
  } catch (err) {
240148
- if (err instanceof WorkflowError && err.code === "spawn-failed") throw err;
240149
240251
  await this.failRun(run2, `\u521B\u5EFA reviewer \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
240150
240252
  throw new WorkflowError("spawn-failed", "\u521B\u5EFA reviewer session \u5931\u8D25");
240151
240253
  }
@@ -240154,6 +240256,73 @@ var WorkflowEngine = class {
240154
240256
  throw err;
240155
240257
  }
240156
240258
  }
240259
+ /**
240260
+ * Phase 2 of the two-phase adhoc review: build the reviewer prompt and send
240261
+ * the first message. Called after the intent brief finished distilling —
240262
+ * inline for single-shot starts, in the background (or over the tunnel, for
240263
+ * remote reviews) after the route already responded. Idempotent: replaying
240264
+ * an already-activated run returns it unchanged, which the durable-intent
240265
+ * replay path relies on.
240266
+ */
240267
+ async activateAdhocReview(runId, opts = {}) {
240268
+ const existing = this.activationFlights.get(runId);
240269
+ if (existing) return existing;
240270
+ const flight = this.runActivation(runId, opts);
240271
+ this.activationFlights.set(runId, flight);
240272
+ const clear = () => {
240273
+ if (this.activationFlights.get(runId) === flight) this.activationFlights.delete(runId);
240274
+ };
240275
+ flight.then(clear, clear);
240276
+ return flight;
240277
+ }
240278
+ async runActivation(runId, opts) {
240279
+ const run2 = await this.storage.workflowRuns.getById(runId);
240280
+ if (!run2) throw new WorkflowError("bad-state", "run \u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u6FC0\u6D3B");
240281
+ if (run2.status !== "preparing") {
240282
+ if (TERMINAL_STATUSES.has(run2.status)) {
240283
+ throw new WorkflowError("bad-state", "run \u5DF2\u7ED3\u675F\uFF0C\u65E0\u6CD5\u6FC0\u6D3B");
240284
+ }
240285
+ return run2;
240286
+ }
240287
+ if (!run2.reviewer_session_id) {
240288
+ throw new WorkflowError("bad-state", "run \u8FD8\u6CA1\u6709 reviewer session\uFF0C\u65E0\u6CD5\u6FC0\u6D3B");
240289
+ }
240290
+ if (!run2.review_target) {
240291
+ throw new WorkflowError("bad-state", "run \u7F3A\u5C11 review target\uFF0C\u65E0\u6CD5\u6FC0\u6D3B");
240292
+ }
240293
+ const pending = this.pendingActivations.get(runId);
240294
+ try {
240295
+ const entries = pending ? null : this.agentOps.getRawMessages(run2.source_session_id);
240296
+ const prompt = buildReviewerPrompt({
240297
+ taskContext: pending ? pending.taskContext : extractTaskContextBefore(entries, run2.source_turn_end_index),
240298
+ originalIntent: pending ? pending.originalIntent : extractFirstUserMessage(entries),
240299
+ authorSelfReport: pending ? pending.authorSelfReport : extractAuthorSelfReport(entries, run2.source_turn_end_index),
240300
+ intentBrief: opts.intentBrief ?? null,
240301
+ blind: opts.blind,
240302
+ reviewFocus: run2.review_focus,
240303
+ target: JSON.parse(run2.review_target),
240304
+ scope: pending?.scope ?? null
240305
+ });
240306
+ const project = await this.storage.projects.getById(run2.project_id);
240307
+ const sent = await this.agentOps.sendUserMessage(run2.reviewer_session_id, prompt, project?.path ?? void 0, void 0, REVIEWER_TURN).catch(() => false);
240308
+ if (!sent) {
240309
+ await this.failRun(run2, "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
240310
+ throw new WorkflowError("spawn-failed", "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
240311
+ }
240312
+ } catch (err) {
240313
+ if (err instanceof WorkflowError && err.code === "spawn-failed") throw err;
240314
+ await this.failRun(run2, `\u6FC0\u6D3B reviewer \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
240315
+ throw new WorkflowError("spawn-failed", "\u6FC0\u6D3B reviewer session \u5931\u8D25");
240316
+ }
240317
+ const claimed = await this.storage.workflowRuns.transition(runId, "preparing", "waiting_reviewer");
240318
+ if (!claimed) {
240319
+ throw new WorkflowError("bad-state", "run \u5728\u51C6\u5907\u671F\u95F4\u5DF2\u88AB\u53D6\u6D88\u6216\u5931\u8D25");
240320
+ }
240321
+ this.clearPendingActivation(runId);
240322
+ const updated = await this.storage.workflowRuns.getById(runId);
240323
+ this.emitRunUpdated(updated);
240324
+ return updated;
240325
+ }
240157
240326
  async handleTaskCompleted(event) {
240158
240327
  const p2 = this.participants.get(event.sessionId);
240159
240328
  if (!p2 || p2.role !== "reviewer") return;
@@ -240271,7 +240440,7 @@ var WorkflowEngine = class {
240271
240440
  if (!run2) return void 0;
240272
240441
  if (TERMINAL_STATUSES.has(run2.status)) return run2;
240273
240442
  const patch = reason ? { error: reason } : void 0;
240274
- 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);
240443
+ 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);
240275
240444
  if (!cancelled) {
240276
240445
  const current = await this.storage.workflowRuns.getById(runId);
240277
240446
  if (current?.status === "sending_feedback") {
@@ -250685,6 +250854,34 @@ async function routes20(fastify2) {
250685
250854
  const proxyAuto = (info, method, apiPath, body) => proxyToRemoteAuto(info.remoteServerId, method, apiPath, body, {
250686
250855
  reverseConnectManager: fastify2.reverseConnectManager
250687
250856
  });
250857
+ const TWO_PHASE_REVIEW_CAPABILITIES = [
250858
+ "http:POST /api/path/workflow-runs/prepare",
250859
+ "http:POST /api/path/workflow-runs/:param/activate"
250860
+ ];
250861
+ const activateRemoteReview = async (opts) => {
250862
+ const blind = opts.reviewContextMode === "blind";
250863
+ let intentBrief = opts.clientBrief;
250864
+ if (!opts.clientProvidedBrief && !blind) {
250865
+ intentBrief = await distillIntentBrief(opts.userId, opts.sourceSessionId);
250866
+ }
250867
+ const body = { intentBrief, reviewContextMode: opts.reviewContextMode };
250868
+ for (let attempt = 1; ; attempt++) {
250869
+ const result = await proxyAuto(
250870
+ opts,
250871
+ "POST",
250872
+ `/api/path/workflow-runs/${opts.bareRunId}/activate`,
250873
+ body
250874
+ );
250875
+ if (result.ok) return;
250876
+ if (result.status !== 0 || attempt >= 3) {
250877
+ console.warn(
250878
+ `[WorkflowRuns] remote activation failed for run ${opts.bareRunId}: status=${result.status} code=${result.errorCode ?? "n/a"}`
250879
+ );
250880
+ return;
250881
+ }
250882
+ await new Promise((resolve3) => setTimeout(resolve3, attempt * 5e3));
250883
+ }
250884
+ };
250688
250885
  const sendProxyFailure = (reply, result) => reply.code(proxyStatus(result)).send(
250689
250886
  result.status === 0 ? { error: `Remote proxy failed: ${result.errorCode || "unknown"}` } : result.data
250690
250887
  );
@@ -250767,10 +250964,6 @@ async function routes20(fastify2) {
250767
250964
  }
250768
250965
  bareReviewerSessionId = reviewerInfo.remoteSessionId;
250769
250966
  }
250770
- let intentBrief2 = clientBrief;
250771
- if (!clientProvidedBrief && !bareReviewerSessionId && !blind) {
250772
- intentBrief2 = await distillIntentBrief(userId, sourceSessionId);
250773
- }
250774
250967
  if (reviewerSessionId && !await fastify2.remoteNotificationSync.prepareForNewTurn(reviewerSessionId)) {
250775
250968
  return reply.code(502).send({
250776
250969
  error: "Could not reach the remote server to prepare notification delivery",
@@ -250779,6 +250972,7 @@ async function routes20(fastify2) {
250779
250972
  }
250780
250973
  const reviewerActivityAt = Date.now();
250781
250974
  let bareRun;
250975
+ let twoPhase = false;
250782
250976
  if (bareReviewerSessionId) {
250783
250977
  const result = await proxyAuto(remoteInfo, "POST", "/api/path/workflow-runs", {
250784
250978
  sourceSessionId: remoteInfo.remoteSessionId,
@@ -250786,7 +250980,7 @@ async function routes20(fastify2) {
250786
250980
  sourceTurnEndIndex,
250787
250981
  reviewSpan,
250788
250982
  reviewerSessionId: bareReviewerSessionId,
250789
- intentBrief: intentBrief2
250983
+ intentBrief: clientBrief
250790
250984
  });
250791
250985
  if (!result.ok) return sendProxyFailure(reply, result);
250792
250986
  bareRun = result.data.run;
@@ -250796,6 +250990,14 @@ async function routes20(fastify2) {
250796
250990
  remoteInfo.remoteServerId
250797
250991
  );
250798
250992
  if (!remoteConfig) return reply.code(404).send({ error: "Remote project configuration not found" });
250993
+ const server = await fastify2.storage.remoteServers.getById(remoteInfo.remoteServerId);
250994
+ twoPhase = TWO_PHASE_REVIEW_CAPABILITIES.every(
250995
+ (capability) => server?.worker_capabilities?.includes(capability)
250996
+ );
250997
+ let intentBrief = clientBrief;
250998
+ if (!twoPhase && !clientProvidedBrief && !blind) {
250999
+ intentBrief = await distillIntentBrief(userId, sourceSessionId);
251000
+ }
250799
251001
  const result = await createRemoteWorkflowReviewer({
250800
251002
  remoteSessionMap: fastify2.remoteSessionMap,
250801
251003
  remoteSessionMappings: fastify2.storage.remoteSessionMappings,
@@ -250817,8 +251019,9 @@ async function routes20(fastify2) {
250817
251019
  // trailing "(review context: …)" line records what actually ran.
250818
251020
  reviewContextMode,
250819
251021
  reviewerAgentType: reviewerAgentType ?? "claude-code",
250820
- intentBrief: intentBrief2,
250821
- userId
251022
+ intentBrief,
251023
+ userId,
251024
+ ...twoPhase ? { phase: "prepare" } : {}
250822
251025
  });
250823
251026
  if (!result.ok) return reply.code(proxyStatus(result)).send(result.data);
250824
251027
  bareRun = result.remoteRun;
@@ -250889,18 +251092,34 @@ async function routes20(fastify2) {
250889
251092
  sessionId: localRun.reviewer_session_id,
250890
251093
  alive: true
250891
251094
  });
250892
- fastify2.eventBus.emit({
250893
- type: "session:status",
250894
- projectId,
250895
- branch: bareRun.branch,
250896
- sessionId: localRun.reviewer_session_id,
250897
- status: "running"
250898
- });
251095
+ if (!twoPhase) {
251096
+ fastify2.eventBus.emit({
251097
+ type: "session:status",
251098
+ projectId,
251099
+ branch: bareRun.branch,
251100
+ sessionId: localRun.reviewer_session_id,
251101
+ status: "running"
251102
+ });
251103
+ }
250899
251104
  }
250900
251105
  fastify2.agentSessionManager.markTitleResolved(localRun.reviewer_session_id);
250901
251106
  await fastify2.storage.remoteSessionMappings.markTitleResolved(localRun.reviewer_session_id);
250902
251107
  }
250903
251108
  fastify2.eventBus.emit({ type: "workflow:run-updated", projectId, branch: bareRun.branch, run: localRun });
251109
+ if (twoPhase) {
251110
+ const bareRunId = bareRun.id;
251111
+ void activateRemoteReview({
251112
+ remoteServerId: remoteInfo.remoteServerId,
251113
+ bareRunId,
251114
+ sourceSessionId,
251115
+ userId,
251116
+ reviewContextMode,
251117
+ clientBrief,
251118
+ clientProvidedBrief
251119
+ }).catch((err) => {
251120
+ console.warn(`[WorkflowRuns] remote activation task failed for run ${bareRunId}:`, err);
251121
+ });
251122
+ }
250904
251123
  return reply.code(201).send({ run: localRun });
250905
251124
  }
250906
251125
  const project = await fastify2.storage.projects.getById(projectId, userId);
@@ -250915,9 +251134,33 @@ async function routes20(fastify2) {
250915
251134
  if (branch !== void 0 && (branch || null) !== runBranch) {
250916
251135
  return reply.code(400).send({ error: "branch does not match source session" });
250917
251136
  }
250918
- let intentBrief = clientBrief;
250919
- if (!clientProvidedBrief && !reviewerSessionId && !blind) {
250920
- intentBrief = await distillIntentBrief(userId, sourceSessionId);
251137
+ if (!reviewerSessionId) {
251138
+ let run2;
251139
+ try {
251140
+ run2 = await fastify2.workflowEngine.prepareAdhocReview({
251141
+ project: { id: project.id, path: project.path },
251142
+ branch: runBranch,
251143
+ sourceSessionId,
251144
+ reviewFocus,
251145
+ sourceTurnEndIndex,
251146
+ reviewSpan,
251147
+ reviewerAgentType
251148
+ });
251149
+ } catch (err) {
251150
+ const status = errStatus(err);
251151
+ if (status) return reply.code(status).send({ error: err.message });
251152
+ throw err;
251153
+ }
251154
+ void (async () => {
251155
+ let intentBrief = clientBrief;
251156
+ if (!clientProvidedBrief && !blind) {
251157
+ intentBrief = await distillIntentBrief(userId, sourceSessionId);
251158
+ }
251159
+ await fastify2.workflowEngine.activateAdhocReview(run2.id, { intentBrief, blind });
251160
+ })().catch((err) => {
251161
+ console.warn(`[WorkflowRuns] background activation failed for run ${run2.id}:`, err);
251162
+ });
251163
+ return reply.code(201).send({ run: run2 });
250921
251164
  }
250922
251165
  try {
250923
251166
  const run2 = await fastify2.workflowEngine.startAdhocReview({
@@ -250927,9 +251170,8 @@ async function routes20(fastify2) {
250927
251170
  reviewFocus,
250928
251171
  sourceTurnEndIndex,
250929
251172
  reviewSpan,
250930
- reviewerAgentType,
250931
251173
  reviewerSessionId,
250932
- intentBrief,
251174
+ intentBrief: clientBrief,
250933
251175
  blind
250934
251176
  });
250935
251177
  return reply.code(201).send({ run: run2 });
@@ -251040,19 +251282,27 @@ async function routes20(fastify2) {
251040
251282
  };
251041
251283
  const result = await proxyAuto(info, "GET", `/api/path/workflow-runs?${q}`);
251042
251284
  if (!result.ok) return sendProxyFailure(reply, result);
251043
- const bareRuns = result.data.runs ?? [];
251285
+ const data = result.data;
251286
+ const bareRuns = data.runs ?? [];
251044
251287
  const runs2 = bareRuns.map((r) => {
251045
251288
  const mapped = mapRemoteRun(r, info.remoteServerId, projectId);
251046
251289
  trackRemoteRun(mapped, { ...info, bareRunId: r.id, projectId });
251047
251290
  return mapped;
251048
251291
  });
251049
251292
  logRead(runs2.length, `remote:${info.remoteServerId}`);
251050
- return reply.send({ runs: runs2 });
251293
+ const prefix = `remote-${info.remoteServerId}-${projectId}-`;
251294
+ return reply.send({
251295
+ runs: runs2,
251296
+ ...data.reviewedSessionIds ? { reviewedSessionIds: data.reviewedSessionIds.map((id) => prefix + id) } : {}
251297
+ });
251051
251298
  }
251052
251299
  }
251053
- const runs = await fastify2.storage.workflowRuns.getActive(projectId, branch ?? null);
251300
+ const [runs, reviewedSessionIds] = await Promise.all([
251301
+ fastify2.storage.workflowRuns.getActive(projectId, branch ?? null),
251302
+ fastify2.storage.workflowRuns.listReviewedSourceSessions(projectId, branch ?? null)
251303
+ ]);
251054
251304
  logRead(runs.length, "local");
251055
- return reply.send({ runs });
251305
+ return reply.send({ runs, reviewedSessionIds });
251056
251306
  }
251057
251307
  );
251058
251308
  fastify2.get("/api/workflow-runs/:id", async (req, reply) => {
@@ -251215,6 +251465,76 @@ async function routes20(fastify2) {
251215
251465
  throw err;
251216
251466
  }
251217
251467
  });
251468
+ fastify2.post("/api/path/workflow-runs/prepare", async (req, reply) => {
251469
+ const userId = requireAuth(req, reply);
251470
+ if (userId === null) return;
251471
+ const { sourceSessionId, reviewFocus, sourceTurnEndIndex } = req.body ?? {};
251472
+ if (!sourceSessionId) return reply.code(400).send({ error: "sourceSessionId is required" });
251473
+ const reviewSpan = parseReviewSpan(req.body?.reviewSpan);
251474
+ if (reviewSpan === null) return reply.code(400).send({ error: "reviewSpan must be one of: this_turn, session_start" });
251475
+ const reviewerAgentType = parseReviewerAgentType(req.body?.reviewerAgentType);
251476
+ if (reviewerAgentType === null) return reply.code(400).send({ error: "reviewerAgentType must be one of: claude-code, codex" });
251477
+ const runId = typeof req.body?.runId === "string" ? req.body.runId.trim() : "";
251478
+ const newReviewerSessionId = typeof req.body?.newReviewerSessionId === "string" ? req.body.newReviewerSessionId.trim() : "";
251479
+ if (!runId || !newReviewerSessionId) {
251480
+ return reply.code(400).send({ error: "runId and newReviewerSessionId are required" });
251481
+ }
251482
+ const sourceSession = await fastify2.storage.agentSessions.getById(sourceSessionId);
251483
+ if (!sourceSession) return reply.code(404).send({ error: "Session not found" });
251484
+ const sourceProjection = await projectLocalSession(sourceSession);
251485
+ if (!sourceProjection) return reply.code(409).send({ error: "Session workspace binding is unavailable" });
251486
+ const project = await fastify2.storage.projects.getById(sourceProjection.projectId);
251487
+ if (!project) return reply.code(404).send({ error: "Session not found" });
251488
+ if (!project.path) return reply.code(400).send({ error: "Project has no local path" });
251489
+ const projectPath = project.path;
251490
+ try {
251491
+ let flight = durableReviewFlights.get(runId);
251492
+ if (!flight) {
251493
+ flight = fastify2.workflowEngine.prepareAdhocReview({
251494
+ project: { id: project.id, path: projectPath },
251495
+ branch: sourceProjection.branch,
251496
+ sourceSessionId,
251497
+ reviewFocus,
251498
+ sourceTurnEndIndex,
251499
+ reviewSpan,
251500
+ reviewerAgentType,
251501
+ runId,
251502
+ newReviewerSessionId
251503
+ });
251504
+ durableReviewFlights.set(runId, flight);
251505
+ const clear = () => {
251506
+ if (durableReviewFlights.get(runId) === flight) durableReviewFlights.delete(runId);
251507
+ };
251508
+ void flight.then(clear, clear);
251509
+ }
251510
+ const run2 = await flight;
251511
+ return reply.code(201).send({ run: run2 });
251512
+ } catch (err) {
251513
+ const status = errStatus(err);
251514
+ if (status) return reply.code(status).send({ error: err.message });
251515
+ throw err;
251516
+ }
251517
+ });
251518
+ fastify2.post("/api/path/workflow-runs/:id/activate", async (req, reply) => {
251519
+ const userId = requireAuth(req, reply);
251520
+ if (userId === null) return;
251521
+ const reviewContextMode = parseReviewContextMode(req.body?.reviewContextMode);
251522
+ if (reviewContextMode === null) return reply.code(400).send({ error: "reviewContextMode must be one of: briefed, blind" });
251523
+ const blind = reviewContextMode === "blind";
251524
+ const intentBriefRaw = req.body?.intentBrief;
251525
+ if (intentBriefRaw !== void 0 && typeof intentBriefRaw !== "string") {
251526
+ return reply.code(400).send({ error: "intentBrief must be a string" });
251527
+ }
251528
+ const intentBrief = blind ? void 0 : normalizeIntentBrief(intentBriefRaw);
251529
+ try {
251530
+ const run2 = await fastify2.workflowEngine.activateAdhocReview(req.params.id, { intentBrief, blind });
251531
+ return reply.send({ run: run2 });
251532
+ } catch (err) {
251533
+ const status = errStatus(err);
251534
+ if (status) return reply.code(status).send({ error: err.message });
251535
+ throw err;
251536
+ }
251537
+ });
251218
251538
  fastify2.get("/api/path/workflow-runs/reviewer-candidate", async (req, reply) => {
251219
251539
  const userId = requireAuth(req, reply);
251220
251540
  if (userId === null) return;
@@ -251234,9 +251554,12 @@ async function routes20(fastify2) {
251234
251554
  const { path: projectPath, branch } = req.query;
251235
251555
  if (!projectPath) return reply.code(400).send({ error: "path is required" });
251236
251556
  const project = await fastify2.storage.projects.getByPath(projectPath) ?? await fastify2.storage.projects.getById(`path:${projectPath}`);
251237
- if (!project) return reply.send({ runs: [] });
251238
- const runs = await fastify2.storage.workflowRuns.getActive(project.id, branch || null);
251239
- return reply.send({ runs });
251557
+ if (!project) return reply.send({ runs: [], reviewedSessionIds: [] });
251558
+ const [runs, reviewedSessionIds] = await Promise.all([
251559
+ fastify2.storage.workflowRuns.getActive(project.id, branch || null),
251560
+ fastify2.storage.workflowRuns.listReviewedSourceSessions(project.id, branch || null)
251561
+ ]);
251562
+ return reply.send({ runs, reviewedSessionIds });
251240
251563
  });
251241
251564
  }
251242
251565
  var workflow_run_routes_default = (0, import_fastify_plugin21.default)(routes20, { name: "workflow-run-routes" });
@@ -252137,6 +252460,16 @@ var routes23 = async (fastify2) => {
252137
252460
  const stopHeartbeat = attachWsHeartbeat(socket, { label: "ExecutorMux" });
252138
252461
  const subs = /* @__PURE__ */ new Map();
252139
252462
  const handleInputMap = /* @__PURE__ */ new Map();
252463
+ const wanted = /* @__PURE__ */ new Set();
252464
+ const dropSubscription = (processId) => {
252465
+ const sub = subs.get(processId);
252466
+ if (sub) {
252467
+ sub.detach();
252468
+ sub.cleanup();
252469
+ subs.delete(processId);
252470
+ }
252471
+ handleInputMap.delete(processId);
252472
+ };
252140
252473
  const subscribeProcess = async (processId) => {
252141
252474
  if (subs.has(processId)) return;
252142
252475
  const ownerUserId = processOwnerScope(principal);
@@ -252149,25 +252482,31 @@ var routes23 = async (fastify2) => {
252149
252482
  return;
252150
252483
  }
252151
252484
  if (subs.has(processId)) return;
252485
+ if (!wanted.has(processId)) return;
252486
+ let detached = false;
252152
252487
  const send = (msg) => {
252488
+ if (detached) return;
252153
252489
  try {
252154
252490
  socket.send(JSON.stringify({ processId, ...msg }));
252155
252491
  } catch {
252156
252492
  }
252157
252493
  };
252158
252494
  let terminated = false;
252495
+ let self2 = null;
252159
252496
  const onTerminal = () => {
252160
252497
  terminated = true;
252161
- const c = subs.get(processId);
252162
- if (c) {
252163
- c();
252498
+ if (self2 && subs.get(processId) === self2) {
252164
252499
  subs.delete(processId);
252500
+ handleInputMap.delete(processId);
252165
252501
  }
252166
- handleInputMap.delete(processId);
252502
+ self2?.cleanup();
252167
252503
  };
252168
252504
  const handle = processId.startsWith("remote-") ? attachRemoteProcessStream(fastify2, processId, send, onTerminal) : attachLocalProcessStream(fastify2, processId, send, onTerminal);
252169
252505
  if (!terminated) {
252170
- subs.set(processId, handle.cleanup);
252506
+ self2 = { cleanup: handle.cleanup, detach: () => {
252507
+ detached = true;
252508
+ } };
252509
+ subs.set(processId, self2);
252171
252510
  handleInputMap.set(processId, handle.handleInput);
252172
252511
  }
252173
252512
  };
@@ -252175,13 +252514,13 @@ var routes23 = async (fastify2) => {
252175
252514
  try {
252176
252515
  const msg = JSON.parse(data.toString());
252177
252516
  if (msg.type === "subscribe") {
252517
+ wanted.add(msg.processId);
252178
252518
  subscribeProcess(msg.processId).catch((err) => {
252179
252519
  console.error(`[ExecutorMux] Failed to subscribe to ${msg.processId}:`, err);
252180
252520
  });
252181
252521
  } else if (msg.type === "unsubscribe") {
252182
- subs.get(msg.processId)?.();
252183
- subs.delete(msg.processId);
252184
- handleInputMap.delete(msg.processId);
252522
+ wanted.delete(msg.processId);
252523
+ dropSubscription(msg.processId);
252185
252524
  } else if (msg.type === "input") {
252186
252525
  handleInputMap.get(msg.processId)?.({ type: "input", data: msg.data });
252187
252526
  } else if (msg.type === "resize") {
@@ -252197,7 +252536,11 @@ var routes23 = async (fastify2) => {
252197
252536
  socket.on("close", () => {
252198
252537
  console.log(`[ExecutorMux] Client disconnected; cleaning ${subs.size} subscriptions`);
252199
252538
  stopHeartbeat();
252200
- for (const cleanup of subs.values()) cleanup();
252539
+ wanted.clear();
252540
+ for (const sub of subs.values()) {
252541
+ sub.detach();
252542
+ sub.cleanup();
252543
+ }
252201
252544
  subs.clear();
252202
252545
  handleInputMap.clear();
252203
252546
  });
@@ -260900,6 +261243,12 @@ var WORKER_CAPABILITIES = {
260900
261243
  // --- Workflow runs ---
260901
261244
  // Empirically bisected via cross-version e2e: 0.2.4 → 404, 0.2.5 → serves.
260902
261245
  "http:POST /api/path/workflow-runs": { since: "0.2.5", summary: "\u521B\u5EFA workflow run" },
261246
+ // Both additive, checked as a pair (TWO_PHASE_REVIEW_CAPABILITIES in
261247
+ // workflow-run-routes.ts): a worker missing either gets the original
261248
+ // single-shot create — the hub distills inline and the submit blocks on it,
261249
+ // exactly the pre-two-phase behavior.
261250
+ "http:POST /api/path/workflow-runs/prepare": { since: "0.3.30", summary: "\u4E24\u6BB5\u5F0F review:\u51C6\u5907(\u5360\u4F4D run + reviewer session)" },
261251
+ "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)" },
260903
261252
  "http:GET /api/path/workflow-runs": { since: "0.2.5", summary: "workflow run \u5217\u8868" },
260904
261253
  "http:GET /api/path/workflow-runs/reviewer-candidate": { since: "0.2.5", summary: "reviewer \u5019\u9009\u67E5\u8BE2" },
260905
261254
  "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.29",
3
+ "version": "0.3.31",
4
4
  "description": "Vibedeckx platform binaries for Linux x64",
5
5
  "os": [
6
6
  "linux"