@vibedeckx/linux-x64 0.3.29 → 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 +386 -58
  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);
@@ -233410,22 +233411,32 @@ async function createRemoteWorkflowReviewer(deps, params) {
233410
233411
  });
233411
233412
  let registeredLocalSessionId = null;
233412
233413
  try {
233413
- 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(
233414
233431
  params.agentMode,
233415
233432
  "POST",
233416
233433
  "/api/path/workflow-runs",
233417
233434
  {
233418
- sourceSessionId: params.sourceRemoteSessionId,
233419
- reviewFocus: params.reviewFocus,
233420
- sourceTurnEndIndex: params.sourceTurnEndIndex,
233421
- reviewSpan: params.reviewSpan,
233435
+ ...sharedBody,
233422
233436
  reviewContextMode: params.reviewContextMode,
233423
- reviewerAgentType: params.reviewerAgentType,
233424
- intentBrief: params.intentBrief,
233425
- runId: remoteRunId,
233426
- newReviewerSessionId: remoteReviewerSessionId
233437
+ intentBrief: params.intentBrief
233427
233438
  },
233428
- { reverseConnectManager: deps.reverseConnectManager ?? void 0 }
233439
+ proxyOpts
233429
233440
  );
233430
233441
  if (!result.ok) {
233431
233442
  const uncertain = result.errorCode === "network_error" || result.errorCode === "timeout";
@@ -239802,6 +239813,7 @@ var FINAL_VERDICT_PROMPT = [
239802
239813
  ...VERDICT_INSTRUCTIONS
239803
239814
  ].join("\n");
239804
239815
  var REVIEWER_AGENT_TYPES = /* @__PURE__ */ new Set(["claude-code", "codex"]);
239816
+ var PREPARE_TIMEOUT_MS = 10 * 6e4;
239805
239817
  var WorkflowEngine = class {
239806
239818
  constructor(storage2, agentOps) {
239807
239819
  this.storage = storage2;
@@ -239812,6 +239824,12 @@ var WorkflowEngine = class {
239812
239824
  eventBus;
239813
239825
  /** sessionId → participation in an active run (rebuilt on boot). */
239814
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();
239815
239833
  setEventBus(bus) {
239816
239834
  this.eventBus = bus;
239817
239835
  bus.subscribe((event) => {
@@ -239836,6 +239854,12 @@ var WorkflowEngine = class {
239836
239854
  await this.storage.workflowRuns.update(run2.id, {
239837
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"
239838
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);
239839
239863
  }
239840
239864
  this.trackParticipants(run2);
239841
239865
  }
@@ -239850,6 +239874,37 @@ var WorkflowEngine = class {
239850
239874
  for (const [sid, p2] of this.participants) {
239851
239875
  if (p2.runId === run2.id) this.participants.delete(sid);
239852
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);
239853
239908
  }
239854
239909
  releaseReservations(runId) {
239855
239910
  for (const [sid, participant] of this.participants) {
@@ -239890,7 +239945,10 @@ var WorkflowEngine = class {
239890
239945
  );
239891
239946
  if (!ok) return;
239892
239947
  const failed = await this.storage.workflowRuns.getById(run2.id);
239893
- if (failed) this.untrackRun(failed);
239948
+ if (failed) {
239949
+ this.untrackRun(failed);
239950
+ this.emitRunUpdated(failed);
239951
+ }
239894
239952
  this.onMilestoneCreated?.();
239895
239953
  }
239896
239954
  /** Test seam for the failure path (see failRun). */
@@ -239959,7 +240017,29 @@ var WorkflowEngine = class {
239959
240017
  reason: null
239960
240018
  };
239961
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
+ */
239962
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) {
239963
240043
  if (opts.reviewerSessionId === opts.sourceSessionId) {
239964
240044
  throw new WorkflowError("reviewer-unavailable", "reviewer session \u4E0D\u80FD\u4E0E source session \u76F8\u540C");
239965
240045
  }
@@ -239982,7 +240062,7 @@ var WorkflowEngine = class {
239982
240062
  }
239983
240063
  return existingRun;
239984
240064
  }
239985
- if (existingRun.status !== "waiting_reviewer") {
240065
+ if (existingRun.status !== "waiting_reviewer" && existingRun.status !== "preparing") {
239986
240066
  throw new WorkflowError("bad-state", "workflow run \u5DF2\u7EC8\u6B62\uFF0C\u4E0D\u80FD\u4F5C\u4E3A\u672A\u77E5\u521B\u5EFA\u7ED3\u679C\u91CD\u653E");
239987
240067
  }
239988
240068
  }
@@ -240060,9 +240140,17 @@ var WorkflowEngine = class {
240060
240140
  review_focus: opts.reviewFocus ?? null,
240061
240141
  review_target: JSON.stringify(target),
240062
240142
  reviewer_session_id: opts.reviewerSessionId ?? null,
240063
- 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"
240064
240147
  });
240065
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
+ }
240066
240154
  if (opts.reviewerSessionId && reviewerSession) {
240067
240155
  if (reviewerSession.permission_mode !== "plan") {
240068
240156
  let switched = false;
@@ -240113,7 +240201,13 @@ var WorkflowEngine = class {
240113
240201
  false,
240114
240202
  "plan",
240115
240203
  opts.reviewerAgentType ?? "claude-code",
240116
- 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,
240117
240211
  false,
240118
240212
  {
240119
240213
  startSnapshot: endSnap,
@@ -240125,27 +240219,25 @@ var WorkflowEngine = class {
240125
240219
  reviewerId,
240126
240220
  `Review - ${sourceSession?.title || (taskContext ? snippetTitle(taskContext) : null) || "Conversation"}`
240127
240221
  ).catch((err) => console.warn(`[WorkflowEngine] failed to set reviewer title for ${reviewerId}:`, err));
240128
- const prompt = buildReviewerPrompt({
240222
+ this.pendingActivations.set(run2.id, {
240223
+ scope,
240129
240224
  taskContext,
240130
240225
  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
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) }
240137
240235
  });
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
240236
  this.trackParticipants(updated);
240237
+ this.armPrepareTimeout(run2.id, PREPARE_TIMEOUT_MS);
240145
240238
  this.emitRunUpdated(updated);
240146
240239
  return updated;
240147
240240
  } catch (err) {
240148
- if (err instanceof WorkflowError && err.code === "spawn-failed") throw err;
240149
240241
  await this.failRun(run2, `\u521B\u5EFA reviewer \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
240150
240242
  throw new WorkflowError("spawn-failed", "\u521B\u5EFA reviewer session \u5931\u8D25");
240151
240243
  }
@@ -240154,6 +240246,73 @@ var WorkflowEngine = class {
240154
240246
  throw err;
240155
240247
  }
240156
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
+ }
240157
240316
  async handleTaskCompleted(event) {
240158
240317
  const p2 = this.participants.get(event.sessionId);
240159
240318
  if (!p2 || p2.role !== "reviewer") return;
@@ -240271,7 +240430,7 @@ var WorkflowEngine = class {
240271
240430
  if (!run2) return void 0;
240272
240431
  if (TERMINAL_STATUSES.has(run2.status)) return run2;
240273
240432
  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);
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);
240275
240434
  if (!cancelled) {
240276
240435
  const current = await this.storage.workflowRuns.getById(runId);
240277
240436
  if (current?.status === "sending_feedback") {
@@ -250685,6 +250844,34 @@ async function routes20(fastify2) {
250685
250844
  const proxyAuto = (info, method, apiPath, body) => proxyToRemoteAuto(info.remoteServerId, method, apiPath, body, {
250686
250845
  reverseConnectManager: fastify2.reverseConnectManager
250687
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
+ };
250688
250875
  const sendProxyFailure = (reply, result) => reply.code(proxyStatus(result)).send(
250689
250876
  result.status === 0 ? { error: `Remote proxy failed: ${result.errorCode || "unknown"}` } : result.data
250690
250877
  );
@@ -250767,10 +250954,6 @@ async function routes20(fastify2) {
250767
250954
  }
250768
250955
  bareReviewerSessionId = reviewerInfo.remoteSessionId;
250769
250956
  }
250770
- let intentBrief2 = clientBrief;
250771
- if (!clientProvidedBrief && !bareReviewerSessionId && !blind) {
250772
- intentBrief2 = await distillIntentBrief(userId, sourceSessionId);
250773
- }
250774
250957
  if (reviewerSessionId && !await fastify2.remoteNotificationSync.prepareForNewTurn(reviewerSessionId)) {
250775
250958
  return reply.code(502).send({
250776
250959
  error: "Could not reach the remote server to prepare notification delivery",
@@ -250779,6 +250962,7 @@ async function routes20(fastify2) {
250779
250962
  }
250780
250963
  const reviewerActivityAt = Date.now();
250781
250964
  let bareRun;
250965
+ let twoPhase = false;
250782
250966
  if (bareReviewerSessionId) {
250783
250967
  const result = await proxyAuto(remoteInfo, "POST", "/api/path/workflow-runs", {
250784
250968
  sourceSessionId: remoteInfo.remoteSessionId,
@@ -250786,7 +250970,7 @@ async function routes20(fastify2) {
250786
250970
  sourceTurnEndIndex,
250787
250971
  reviewSpan,
250788
250972
  reviewerSessionId: bareReviewerSessionId,
250789
- intentBrief: intentBrief2
250973
+ intentBrief: clientBrief
250790
250974
  });
250791
250975
  if (!result.ok) return sendProxyFailure(reply, result);
250792
250976
  bareRun = result.data.run;
@@ -250796,6 +250980,14 @@ async function routes20(fastify2) {
250796
250980
  remoteInfo.remoteServerId
250797
250981
  );
250798
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
+ }
250799
250991
  const result = await createRemoteWorkflowReviewer({
250800
250992
  remoteSessionMap: fastify2.remoteSessionMap,
250801
250993
  remoteSessionMappings: fastify2.storage.remoteSessionMappings,
@@ -250817,8 +251009,9 @@ async function routes20(fastify2) {
250817
251009
  // trailing "(review context: …)" line records what actually ran.
250818
251010
  reviewContextMode,
250819
251011
  reviewerAgentType: reviewerAgentType ?? "claude-code",
250820
- intentBrief: intentBrief2,
250821
- userId
251012
+ intentBrief,
251013
+ userId,
251014
+ ...twoPhase ? { phase: "prepare" } : {}
250822
251015
  });
250823
251016
  if (!result.ok) return reply.code(proxyStatus(result)).send(result.data);
250824
251017
  bareRun = result.remoteRun;
@@ -250889,18 +251082,34 @@ async function routes20(fastify2) {
250889
251082
  sessionId: localRun.reviewer_session_id,
250890
251083
  alive: true
250891
251084
  });
250892
- fastify2.eventBus.emit({
250893
- type: "session:status",
250894
- projectId,
250895
- branch: bareRun.branch,
250896
- sessionId: localRun.reviewer_session_id,
250897
- status: "running"
250898
- });
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
+ }
250899
251094
  }
250900
251095
  fastify2.agentSessionManager.markTitleResolved(localRun.reviewer_session_id);
250901
251096
  await fastify2.storage.remoteSessionMappings.markTitleResolved(localRun.reviewer_session_id);
250902
251097
  }
250903
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
+ }
250904
251113
  return reply.code(201).send({ run: localRun });
250905
251114
  }
250906
251115
  const project = await fastify2.storage.projects.getById(projectId, userId);
@@ -250915,9 +251124,33 @@ async function routes20(fastify2) {
250915
251124
  if (branch !== void 0 && (branch || null) !== runBranch) {
250916
251125
  return reply.code(400).send({ error: "branch does not match source session" });
250917
251126
  }
250918
- let intentBrief = clientBrief;
250919
- if (!clientProvidedBrief && !reviewerSessionId && !blind) {
250920
- 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 });
250921
251154
  }
250922
251155
  try {
250923
251156
  const run2 = await fastify2.workflowEngine.startAdhocReview({
@@ -250927,9 +251160,8 @@ async function routes20(fastify2) {
250927
251160
  reviewFocus,
250928
251161
  sourceTurnEndIndex,
250929
251162
  reviewSpan,
250930
- reviewerAgentType,
250931
251163
  reviewerSessionId,
250932
- intentBrief,
251164
+ intentBrief: clientBrief,
250933
251165
  blind
250934
251166
  });
250935
251167
  return reply.code(201).send({ run: run2 });
@@ -251215,6 +251447,76 @@ async function routes20(fastify2) {
251215
251447
  throw err;
251216
251448
  }
251217
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
+ });
251218
251520
  fastify2.get("/api/path/workflow-runs/reviewer-candidate", async (req, reply) => {
251219
251521
  const userId = requireAuth(req, reply);
251220
251522
  if (userId === null) return;
@@ -252137,6 +252439,16 @@ var routes23 = async (fastify2) => {
252137
252439
  const stopHeartbeat = attachWsHeartbeat(socket, { label: "ExecutorMux" });
252138
252440
  const subs = /* @__PURE__ */ new Map();
252139
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
+ };
252140
252452
  const subscribeProcess = async (processId) => {
252141
252453
  if (subs.has(processId)) return;
252142
252454
  const ownerUserId = processOwnerScope(principal);
@@ -252149,25 +252461,31 @@ var routes23 = async (fastify2) => {
252149
252461
  return;
252150
252462
  }
252151
252463
  if (subs.has(processId)) return;
252464
+ if (!wanted.has(processId)) return;
252465
+ let detached = false;
252152
252466
  const send = (msg) => {
252467
+ if (detached) return;
252153
252468
  try {
252154
252469
  socket.send(JSON.stringify({ processId, ...msg }));
252155
252470
  } catch {
252156
252471
  }
252157
252472
  };
252158
252473
  let terminated = false;
252474
+ let self2 = null;
252159
252475
  const onTerminal = () => {
252160
252476
  terminated = true;
252161
- const c = subs.get(processId);
252162
- if (c) {
252163
- c();
252477
+ if (self2 && subs.get(processId) === self2) {
252164
252478
  subs.delete(processId);
252479
+ handleInputMap.delete(processId);
252165
252480
  }
252166
- handleInputMap.delete(processId);
252481
+ self2?.cleanup();
252167
252482
  };
252168
252483
  const handle = processId.startsWith("remote-") ? attachRemoteProcessStream(fastify2, processId, send, onTerminal) : attachLocalProcessStream(fastify2, processId, send, onTerminal);
252169
252484
  if (!terminated) {
252170
- subs.set(processId, handle.cleanup);
252485
+ self2 = { cleanup: handle.cleanup, detach: () => {
252486
+ detached = true;
252487
+ } };
252488
+ subs.set(processId, self2);
252171
252489
  handleInputMap.set(processId, handle.handleInput);
252172
252490
  }
252173
252491
  };
@@ -252175,13 +252493,13 @@ var routes23 = async (fastify2) => {
252175
252493
  try {
252176
252494
  const msg = JSON.parse(data.toString());
252177
252495
  if (msg.type === "subscribe") {
252496
+ wanted.add(msg.processId);
252178
252497
  subscribeProcess(msg.processId).catch((err) => {
252179
252498
  console.error(`[ExecutorMux] Failed to subscribe to ${msg.processId}:`, err);
252180
252499
  });
252181
252500
  } else if (msg.type === "unsubscribe") {
252182
- subs.get(msg.processId)?.();
252183
- subs.delete(msg.processId);
252184
- handleInputMap.delete(msg.processId);
252501
+ wanted.delete(msg.processId);
252502
+ dropSubscription(msg.processId);
252185
252503
  } else if (msg.type === "input") {
252186
252504
  handleInputMap.get(msg.processId)?.({ type: "input", data: msg.data });
252187
252505
  } else if (msg.type === "resize") {
@@ -252197,7 +252515,11 @@ var routes23 = async (fastify2) => {
252197
252515
  socket.on("close", () => {
252198
252516
  console.log(`[ExecutorMux] Client disconnected; cleaning ${subs.size} subscriptions`);
252199
252517
  stopHeartbeat();
252200
- for (const cleanup of subs.values()) cleanup();
252518
+ wanted.clear();
252519
+ for (const sub of subs.values()) {
252520
+ sub.detach();
252521
+ sub.cleanup();
252522
+ }
252201
252523
  subs.clear();
252202
252524
  handleInputMap.clear();
252203
252525
  });
@@ -260900,6 +261222,12 @@ var WORKER_CAPABILITIES = {
260900
261222
  // --- Workflow runs ---
260901
261223
  // Empirically bisected via cross-version e2e: 0.2.4 → 404, 0.2.5 → serves.
260902
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)" },
260903
261231
  "http:GET /api/path/workflow-runs": { since: "0.2.5", summary: "workflow run \u5217\u8868" },
260904
261232
  "http:GET /api/path/workflow-runs/reviewer-candidate": { since: "0.2.5", summary: "reviewer \u5019\u9009\u67E5\u8BE2" },
260905
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.29",
3
+ "version": "0.3.30",
4
4
  "description": "Vibedeckx platform binaries for Linux x64",
5
5
  "os": [
6
6
  "linux"