@moxxy/cli 0.14.2 → 0.14.4

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.
package/dist/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'node:module';
3
3
  import { z as z$1, createMutex, defineTunnelProvider, definePlugin, defineProvider, defineTool, MoxxyError, asTurnId, defineMode, asPluginId, defineCommand, defineChannel, defineWorkflowExecutor, toFriendlyError, estimateTextTokens, classifyHttpStatus, createStuckLoopDetector, runCompactionIfNeeded, runElisionIfNeeded, collectProviderStream, usageEventFields, isContextOverflowError, emitRequestsAndDetectStuck, executeToolUses, buildSystemPromptWithSkills, projectMessages, defineCompactor, defineCacheStrategy, denyByDefaultResolver, createAllowListResolver, zodToJsonSchema, fileDiffSummary, runSingleShotTurn, defineSurface, runManualCompaction, isFileDiffDisplay, renderFrontmatter, defineEmbedder, migrateModeName, skillFrontmatterSchema, asSkillId, startChannelWith, parseFrontmatterFile, createDeferredPermissionResolver, getInstallHint, defineTranscriber, summarizeTokensByModel, countNodes, moxxyPackageSchema, encodeLoginPrompt, classifyNetworkError, addModelTotals, createJsonFileStore, ISOLATION_RANK, MOXXY_PCM16_24KHZ_MIME, fileDiffVerb, parseFrontmatter, createCallbackResolver, autoAllowResolver, asSessionId, asToolCallId, defineViewRenderer, DEFAULT_VIEW_TAGS, isSafeViewUrl, evaluateToolRule, summarizeSessionTokensFromEvents, toDiffRows, diffGutterNo, computeElisionState, toolResultStubbed, toolResultStub, toolResultBytes, conversationalStubbed, conversationalStub, asEventId } from '@moxxy/sdk';
4
4
  import * as fs32 from 'fs';
5
- import fs32__default, { existsSync, promises, ReadStream, mkdirSync, statSync, readdirSync, writeFileSync, readFileSync, unlinkSync, chmodSync, watch, createReadStream } from 'fs';
5
+ import fs32__default, { existsSync, promises, ReadStream, mkdirSync, rmSync, statSync, readdirSync, writeFileSync, readFileSync, unlinkSync, chmodSync, watch, createReadStream } from 'fs';
6
6
  import * as path3 from 'path';
7
7
  import path3__default, { join, dirname, resolve, relative, isAbsolute, basename } from 'path';
8
8
  import { isCliTunnelAvailable, writeFileAtomic, spawnCliTunnel, moxxyPath, moxxyHome, bearerTokenMatches, resolveChannelToken, rotateChannelToken, readRequestBody, MOXXY_WS_SUBPROTOCOL, bearerGuard, tokenFromWsProtocolHeader, writeFileAtomicSync } from '@moxxy/sdk/server';
@@ -3666,6 +3666,107 @@ var init_session = __esm({
3666
3666
  };
3667
3667
  }
3668
3668
  });
3669
+
3670
+ // ../core/dist/setup-agent.js
3671
+ function resolvePermissions(p3) {
3672
+ if (p3 === "deny")
3673
+ return denyByDefaultResolver;
3674
+ if (p3 === void 0 || p3 === "auto")
3675
+ return autoAllowResolver;
3676
+ return p3;
3677
+ }
3678
+ function finalText(events) {
3679
+ for (let i2 = events.length - 1; i2 >= 0; i2 -= 1) {
3680
+ const e3 = events[i2];
3681
+ if (e3.type === "assistant_message")
3682
+ return e3.content;
3683
+ }
3684
+ return "";
3685
+ }
3686
+ function mergePresets(presets) {
3687
+ const plugins = [];
3688
+ const seen = /* @__PURE__ */ new Set();
3689
+ const tools = [];
3690
+ let provider;
3691
+ let cwd2;
3692
+ let permissions;
3693
+ let session;
3694
+ for (const p3 of presets) {
3695
+ for (const pl of p3.plugins ?? []) {
3696
+ if (seen.has(pl.name))
3697
+ continue;
3698
+ seen.add(pl.name);
3699
+ plugins.push(pl);
3700
+ }
3701
+ if (p3.tools)
3702
+ tools.push(...p3.tools);
3703
+ if (!provider && p3.provider)
3704
+ provider = p3.provider;
3705
+ cwd2 ??= p3.cwd;
3706
+ permissions ??= p3.permissions;
3707
+ if (p3.session)
3708
+ session = { ...session, ...p3.session };
3709
+ }
3710
+ return { plugins, tools, provider, cwd: cwd2, permissions, session };
3711
+ }
3712
+ function setupAgent(input = {}) {
3713
+ const opts = Array.isArray(input) ? mergePresets(input) : input;
3714
+ const session = new Session({
3715
+ cwd: opts.cwd ?? process.cwd(),
3716
+ permissionResolver: resolvePermissions(opts.permissions),
3717
+ ...opts.session
3718
+ });
3719
+ for (const plugin5 of opts.plugins ?? [])
3720
+ session.pluginHost.registerStatic(plugin5);
3721
+ for (const tool of opts.tools ?? [])
3722
+ session.tools.register(tool);
3723
+ if (opts.provider)
3724
+ session.providers.setActive(opts.provider.name, opts.provider.config);
3725
+ const agent = {
3726
+ session,
3727
+ async *stream(prompt, runOpts) {
3728
+ yield* runTurn(session, prompt, runOpts);
3729
+ },
3730
+ async ask(prompt, runOpts) {
3731
+ return finalText(await collectTurn(session, prompt, runOpts));
3732
+ },
3733
+ collect(prompt, runOpts) {
3734
+ return collectTurn(session, prompt, runOpts);
3735
+ },
3736
+ async discover() {
3737
+ await session.pluginHost.discoverAndLoad();
3738
+ return agent;
3739
+ },
3740
+ use(plugin5) {
3741
+ session.pluginHost.registerStatic(plugin5);
3742
+ return agent;
3743
+ },
3744
+ addTool(tool) {
3745
+ session.tools.register(tool);
3746
+ return agent;
3747
+ },
3748
+ removeTool(name) {
3749
+ session.tools.unregister(name);
3750
+ return agent;
3751
+ },
3752
+ setProvider(name, config) {
3753
+ session.providers.setActive(name, config);
3754
+ return agent;
3755
+ },
3756
+ setMode(name) {
3757
+ session.modes.setActive(name);
3758
+ return agent;
3759
+ }
3760
+ };
3761
+ return agent;
3762
+ }
3763
+ var init_setup_agent = __esm({
3764
+ "../core/dist/setup-agent.js"() {
3765
+ init_session();
3766
+ init_run_turn();
3767
+ init_resolvers();
3768
+ }
3769
+ });
3669
3770
  function preferencesPath() {
3670
3771
  return path3.join(os5.homedir(), ".moxxy", "preferences.json");
3671
3772
  }
@@ -4149,6 +4250,45 @@ async function restoreEvents(sessionId, dir = defaultSessionsDir(), logger = cre
4149
4250
  }
4150
4251
  return events;
4151
4252
  }
4253
+ async function readEventPage(sessionId, opts, dir = defaultSessionsDir()) {
4254
+ const logPath = path3.join(dir, `${sessionId}.jsonl`);
4255
+ let raw;
4256
+ try {
4257
+ raw = await promises.readFile(logPath, "utf8");
4258
+ } catch {
4259
+ return { events: [], prevCursor: null };
4260
+ }
4261
+ const all = [];
4262
+ for (const line of raw.split("\n")) {
4263
+ if (!line.trim())
4264
+ continue;
4265
+ try {
4266
+ all.push(JSON.parse(line));
4267
+ } catch {
4268
+ }
4269
+ }
4270
+ return pageEvents(all, opts.before, opts.limit);
4271
+ }
4272
+ function pageEvents(events, before, limit2) {
4273
+ const cap = Math.max(0, Math.floor(limit2));
4274
+ if (cap === 0 || events.length === 0) {
4275
+ return { events: [], prevCursor: events.length === 0 ? null : before };
4276
+ }
4277
+ let end = events.length;
4278
+ if (before !== null) {
4279
+ end = 0;
4280
+ for (let i2 = 0; i2 < events.length; i2 += 1) {
4281
+ if (events[i2].seq < before)
4282
+ end = i2 + 1;
4283
+ else
4284
+ break;
4285
+ }
4286
+ }
4287
+ const start = Math.max(0, end - cap);
4288
+ const page = events.slice(start, end);
4289
+ const prevCursor = start <= 0 ? null : page[0].seq;
4290
+ return { events: page, prevCursor };
4291
+ }
4152
4292
  async function deleteSession(sessionId, dir = defaultSessionsDir()) {
4153
4293
  await promises.rm(path3.join(dir, `${sessionId}.jsonl`), { force: true });
4154
4294
  await promises.rm(metaPath(dir, sessionId), { force: true });
@@ -8414,16 +8554,19 @@ __export(dist_exports, {
8414
8554
  mergeUsageStats: () => mergeUsageStats,
8415
8555
  newSessionId: () => newSessionId,
8416
8556
  newTurnId: () => newTurnId,
8557
+ pageEvents: () => pageEvents,
8417
8558
  parseFrontmatter: () => parseFrontmatter,
8418
8559
  parseSkillFile: () => parseSkillFile,
8419
8560
  parseView: () => parseView,
8420
8561
  permissionPolicySchema: () => permissionPolicySchema,
8421
8562
  preferencesPath: () => preferencesPath,
8422
8563
  readPackageMoxxyRequirements: () => readPackageMoxxyRequirements,
8564
+ readSessionEventPage: () => readEventPage,
8423
8565
  readSessionIndex: () => readIndex,
8424
8566
  restoreSessionEvents: () => restoreEvents,
8425
8567
  runTurn: () => runTurn,
8426
8568
  savePreferences: () => savePreferences,
8569
+ setupAgent: () => setupAgent,
8427
8570
  silentLogger: () => silentLogger,
8428
8571
  synthesizeSkill: () => synthesizeSkill,
8429
8572
  toposortPluginManifests: () => toposortPluginManifests,
@@ -8434,6 +8577,7 @@ var init_dist = __esm({
8434
8577
  "../core/dist/index.js"() {
8435
8578
  init_session();
8436
8579
  init_run_turn();
8580
+ init_setup_agent();
8437
8581
  init_subagents();
8438
8582
  init_preferences();
8439
8583
  init_usage_stats();
@@ -136266,7 +136410,7 @@ async function reclaimStaleSocket(socketPath) {
136266
136410
 
136267
136411
  // ../runner/dist/server.js
136268
136412
  init_dist();
136269
- var RUNNER_PROTOCOL_VERSION = 9;
136413
+ var RUNNER_PROTOCOL_VERSION = 10;
136270
136414
  var MIN_COMPATIBLE_PROTOCOL_VERSION = 1;
136271
136415
  var RunnerMethod = {
136272
136416
  /** client->server: handshake; returns the initial info snapshot. */
@@ -136285,6 +136429,13 @@ var RunnerMethod = {
136285
136429
  * attached clients so every mirror clears in lockstep.
136286
136430
  */
136287
136431
  SessionReset: "session.reset",
136432
+ /**
136433
+ * client->server: page the runner's authoritative event history (v10).
136434
+ * `{ before, limit }` → `{ events, prevCursor }`, newest-first. Backs the
136435
+ * desktop dual-history retirement; a v10 client gates it on the server's
136436
+ * reported version and falls back to its NDJSON store against an older runner.
136437
+ */
136438
+ SessionLoadHistory: "session.loadHistory",
136288
136439
  /** client->server: declare which resolvers this client will answer. */
136289
136440
  SetResolver: "setResolver",
136290
136441
  /** client->server: switch the active mode. */
@@ -136410,6 +136561,11 @@ var modeSetActiveParamsSchema = z.object({ name: z.string() });
136410
136561
  var sessionSetReasoningParamsSchema = z.object({
136411
136562
  effort: z.enum(["off", "low", "medium", "high"])
136412
136563
  });
136564
+ var MAX_HISTORY_PAGE_LIMIT = 2e3;
136565
+ var sessionLoadHistoryParamsSchema = z.object({
136566
+ before: z.number().int().nonnegative().nullable(),
136567
+ limit: z.number().int().positive().max(MAX_HISTORY_PAGE_LIMIT)
136568
+ });
136413
136569
  var providerSetActiveParamsSchema = z.object({
136414
136570
  name: z.string(),
136415
136571
  config: z.record(z.unknown()).optional()
@@ -136734,6 +136890,7 @@ async function handleSurfaceClose(ctx, raw) {
136734
136890
  }
136735
136891
 
136736
136892
  // ../runner/dist/handlers/session-handlers.js
136893
+ init_dist();
136737
136894
  function handleModeSetActive(ctx, raw) {
136738
136895
  const { name } = modeSetActiveParamsSchema.parse(raw);
136739
136896
  ctx.session.modes.setActive(name);
@@ -136745,6 +136902,14 @@ function handleSessionSetReasoning(ctx, raw) {
136745
136902
  ctx.broadcastInfo();
136746
136903
  return {};
136747
136904
  }
136905
+ async function handleSessionLoadHistory(ctx, raw) {
136906
+ const { before, limit: limit2 } = sessionLoadHistoryParamsSchema.parse(raw);
136907
+ const log = ctx.session.log;
136908
+ if (log.baseSeq === 0) {
136909
+ return pageEvents(log.toJSON(), before, limit2);
136910
+ }
136911
+ return readEventPage(String(ctx.session.id), { before, limit: limit2 }, ctx.sessionsDir);
136912
+ }
136748
136913
  async function handlePermissionAddAllow(ctx, raw) {
136749
136914
  const { name, reason } = permissionAddAllowParamsSchema.parse(raw);
136750
136915
  await ctx.session.permissions.addAllow({ name, ...reason ? { reason } : {} });
@@ -136863,6 +137028,7 @@ var RunnerServer = class {
136863
137028
  peer.handle(RunnerMethod.RunTurn, (raw) => this.handleRunTurn(client, raw));
136864
137029
  peer.handle(RunnerMethod.Abort, (raw) => this.handleAbort(client, raw));
136865
137030
  peer.handle(RunnerMethod.SessionReset, () => this.handleSessionReset());
137031
+ peer.handle(RunnerMethod.SessionLoadHistory, (raw) => handleSessionLoadHistory(ctx, raw));
136866
137032
  peer.handle(RunnerMethod.SetResolver, (raw) => this.handleSetResolver(client, raw));
136867
137033
  peer.handle(RunnerMethod.ModeSetActive, (raw) => handleModeSetActive(ctx, raw));
136868
137034
  peer.handle(RunnerMethod.SessionSetReasoning, (raw) => handleSessionSetReasoning(ctx, raw));
@@ -136946,6 +137112,7 @@ var RunnerServer = class {
136946
137112
  } finally {
136947
137113
  this.turnControllers.delete(turnId);
136948
137114
  client.turns.delete(turnId);
137115
+ await this.sealUnsealedStreamedText(turnId);
136949
137116
  this.broadcast(RunnerNotification.TurnComplete, {
136950
137117
  turnId,
136951
137118
  ...error2 ? { error: error2 } : {}
@@ -136990,6 +137157,59 @@ var RunnerServer = class {
136990
137157
  this.session.log.clear();
136991
137158
  return {};
136992
137159
  }
137160
+ /**
137161
+ * If this turn streamed assistant text (`assistant_chunk`) that no
137162
+ * `assistant_message` ever sealed, append a REAL `assistant_message` to the
137163
+ * authoritative log so it persists + replays like any other reply. It
137164
+ * accumulates chunk deltas and seals whatever text remains at turn end — so a
137165
+ * cleanly sealed reply (the normal path) leaves an empty remainder and this is
137166
+ * a no-op.
137167
+ *
137168
+ * The accumulator resets on BOTH per-iteration boundaries — an
137169
+ * `assistant_message` (a reply was sealed) AND a `provider_request` (a fresh
137170
+ * provider iteration begins). Resetting on `provider_request` is what scopes
137171
+ * the seal to the FINAL iteration: a retryable provider error can abandon a
137172
+ * partially-streamed iteration WITHOUT sealing it (the mode loop emits the
137173
+ * error and `continue`s), and a later iteration then streams fresh text and
137174
+ * may itself end unsealed (a fatal error / abort / max-iterations). Without
137175
+ * the `provider_request` reset, the abandoned attempt's chunks would be
137176
+ * concatenated INTO the sealed reply ("ABANDONED-final" instead of "final"),
137177
+ * durably corrupting authoritative/replayed history. (This is a deliberate
137178
+ * improvement over the desktop renderer's old turn-complete synthesis, which
137179
+ * accumulated across iterations and had exactly that defect — and which this
137180
+ * seal retires.)
137181
+ *
137182
+ * The append flows through `session.log` → persistence + the broadcast stream,
137183
+ * so mirrors ingest it as a normal event and never need to synthesize their
137184
+ * own.
137185
+ */
137186
+ async sealUnsealedStreamedText(turnId) {
137187
+ const events = this.session.log.byTurn(turnId);
137188
+ if (events.length === 0)
137189
+ return;
137190
+ let unsealed = "";
137191
+ for (const event of events) {
137192
+ if (event.type === "assistant_message" || event.type === "provider_request")
137193
+ unsealed = "";
137194
+ else if (event.type === "assistant_chunk")
137195
+ unsealed += event.delta;
137196
+ }
137197
+ if (!unsealed.trim())
137198
+ return;
137199
+ try {
137200
+ await this.session.log.append({
137201
+ type: "assistant_message",
137202
+ sessionId: this.session.id,
137203
+ turnId,
137204
+ source: "model",
137205
+ content: unsealed,
137206
+ // The turn ended without the provider sealing the message (error/abort
137207
+ // after partial text); record it as a normal completed reply.
137208
+ stopReason: "end_turn"
137209
+ });
137210
+ } catch {
137211
+ }
137212
+ }
136993
137213
  handleSetResolver(client, raw) {
136994
137214
  const params = setResolverParamsSchema.parse(raw);
136995
137215
  if (params.permission !== void 0)
@@ -137602,6 +137822,26 @@ var RemoteSession = class {
137602
137822
  async reset() {
137603
137823
  await this.peer.request(RunnerMethod.SessionReset, {});
137604
137824
  }
137825
+ /**
137826
+ * Page the runner's AUTHORITATIVE event history (protocol v10). Backs the
137827
+ * desktop's dual-history retirement — the renderer reads transcript history
137828
+ * from the runner instead of its own NDJSON chat store. Newest-first paging:
137829
+ * pass `before: null` for the newest page, then feed each result's
137830
+ * `prevCursor` back as `before` to walk older pages until `prevCursor` is
137831
+ * `null` (start of history).
137832
+ *
137833
+ * GATED on the server reporting protocol v10+. Against an OLDER runner this
137834
+ * throws a clear, actionable error (not a raw method-not-found) — the desktop
137835
+ * CATCHES it and falls back to its existing NDJSON path, so no transcript
137836
+ * ever goes blank when the runner predates this method.
137837
+ */
137838
+ async loadHistory(before, limit2) {
137839
+ this.requireServerProtocol(10, "Loading session history from the runner");
137840
+ return this.peer.request(RunnerMethod.SessionLoadHistory, {
137841
+ before,
137842
+ limit: limit2
137843
+ });
137844
+ }
137605
137845
  getInfo() {
137606
137846
  return this.requireInfo();
137607
137847
  }
@@ -138418,7 +138658,7 @@ var CollaborationState = class {
138418
138658
  return;
138419
138659
  agent.status = status;
138420
138660
  this.emitFn({ kind: "agent_status", agentId, status, ...detail ? { detail } : {} });
138421
- if (status === "crashed" || status === "killed")
138661
+ if (status === "crashed" || status === "killed" || status === "failed")
138422
138662
  this.releaseAllFor(agentId);
138423
138663
  }
138424
138664
  markDone(agentId, summary, artifacts) {
@@ -138431,7 +138671,7 @@ var CollaborationState = class {
138431
138671
  this.emitFn({ kind: "agent_done", agentId, summary, ...artifacts ? { artifacts } : {} });
138432
138672
  }
138433
138673
  allDone() {
138434
- const live = this.agentOrder.map((id) => this.agents.get(id)).filter((a2) => a2.status !== "crashed" && a2.status !== "killed");
138674
+ const live = this.agentOrder.map((id) => this.agents.get(id)).filter((a2) => a2.status !== "crashed" && a2.status !== "killed" && a2.status !== "failed");
138435
138675
  return live.length > 0 && live.every((a2) => a2.status === "done");
138436
138676
  }
138437
138677
  roleOf(agentId) {
@@ -138789,7 +139029,7 @@ async function createCollaborationHub(opts) {
138789
139029
  connections.delete(peer);
138790
139030
  if (agentId) {
138791
139031
  const agent = state.rosterView().agents.find((a2) => a2.id === agentId);
138792
- if (agent && agent.status !== "done" && agent.status !== "killed") {
139032
+ if (agent && agent.status !== "done" && agent.status !== "failed" && agent.status !== "killed") {
138793
139033
  state.setStatus(agentId, "crashed");
138794
139034
  }
138795
139035
  }
@@ -139035,13 +139275,24 @@ var PeerSupervisor = class {
139035
139275
  child.on("exit", () => {
139036
139276
  proc.exited = true;
139037
139277
  });
139278
+ child.on("error", (err) => {
139279
+ proc.exited = true;
139280
+ proc.stderr.push(`spawn error: ${err.message}`);
139281
+ });
139038
139282
  return { socket };
139039
139283
  }
139284
+ /** True once the child has exited or its spawn failed. */
139285
+ hasExited(agentId) {
139286
+ const proc = this.peers.get(agentId);
139287
+ return proc ? proc.exited : false;
139288
+ }
139040
139289
  /** Last stderr lines from a peer — used to diagnose a crash. */
139041
139290
  stderrOf(agentId) {
139042
139291
  return this.peers.get(agentId)?.stderr ?? [];
139043
139292
  }
139044
- /** Best-effort: abort a single peer's in-flight turn via its runner, then kill. */
139293
+ /** Stop a single peer and AWAIT its real exit (with a force-kill fallback), so
139294
+ * callers — e.g. the sequential fallback — can rely on the workspace being
139295
+ * free before the next agent starts. */
139045
139296
  async stop(agentId) {
139046
139297
  const proc = this.peers.get(agentId);
139047
139298
  if (!proc || proc.exited)
@@ -139050,6 +139301,27 @@ var PeerSupervisor = class {
139050
139301
  proc.child.kill("SIGTERM");
139051
139302
  } catch {
139052
139303
  }
139304
+ await new Promise((resolve13) => {
139305
+ if (proc.exited)
139306
+ return resolve13();
139307
+ let settled = false;
139308
+ const done = () => {
139309
+ if (settled)
139310
+ return;
139311
+ settled = true;
139312
+ clearTimeout(timer);
139313
+ resolve13();
139314
+ };
139315
+ proc.child.once("exit", done);
139316
+ const timer = setTimeout(() => {
139317
+ try {
139318
+ proc.child.kill("SIGKILL");
139319
+ } catch {
139320
+ }
139321
+ done();
139322
+ }, FORCE_KILL_GRACE_MS);
139323
+ timer.unref?.();
139324
+ });
139053
139325
  }
139054
139326
  async shutdownAll(_reason) {
139055
139327
  if (this.shuttingDown)
@@ -139183,6 +139455,7 @@ function releaseCollabLock(sessionId) {
139183
139455
 
139184
139456
  // ../mode-collaborative/dist/collab-loop.js
139185
139457
  var POLL_MS = 500;
139458
+ var BOOT_DEADLINE_MS = 9e4;
139186
139459
  function runCollaborativeMode(ctx) {
139187
139460
  return runCollaborative(ctx, {});
139188
139461
  }
@@ -139251,13 +139524,18 @@ async function* runCollaborative(ctx, deps) {
139251
139524
  yield await ctx.emit(plugin4(ctx, "collab_started", { task, parallel, gitInstalled: gitInstalled2, gitRepo }));
139252
139525
  supervisor.spawn({ entry: architectEntry, cwd: cwd2, mode: COLLAB_ARCHITECT_MODE_NAME });
139253
139526
  yield await ctx.emit(plugin4(ctx, "collab_agent_spawned", { id: ARCHITECT_AGENT_ID, role: "architect" }));
139254
- const architectOk = await waitForAgent(hub, ARCHITECT_AGENT_ID, ctx.signal, cfg.wallClockMs);
139527
+ const architectOk = await waitForAgent(hub, supervisor, ARCHITECT_AGENT_ID, ctx.signal, cfg.wallClockMs);
139255
139528
  if (ctx.signal.aborted) {
139256
139529
  yield await ctx.emit(emitAbort(ctx, "aborted during design"));
139257
139530
  return;
139258
139531
  }
139259
139532
  if (!architectOk) {
139260
- yield await ctx.emit(assistant(ctx, "The architect did not finish the design. Stopping the collaboration."));
139533
+ const why = supervisor.stderrOf(ARCHITECT_AGENT_ID).slice(-4).join("\n");
139534
+ yield await ctx.emit(plugin4(ctx, "collab_agent_failed", { id: ARCHITECT_AGENT_ID, status: statusOf(hub, ARCHITECT_AGENT_ID), stderr: supervisor.stderrOf(ARCHITECT_AGENT_ID).slice(-6) }));
139535
+ yield await ctx.emit(assistant(ctx, `The architect did not finish the design \u2014 stopping the collaboration.${why ? `
139536
+
139537
+ Last diagnostics:
139538
+ ${why}` : ""}`));
139261
139539
  return;
139262
139540
  }
139263
139541
  let roster = readRoster(join(cwd2, COLLAB_SCAFFOLD_DIR, ROSTER_FILENAME), cfg.maxAgents);
@@ -139298,7 +139576,8 @@ async function* runCollaborative(ctx, deps) {
139298
139576
  supervisor.spawn({ entry, cwd: wt3, mode: COLLAB_PEER_MODE_NAME });
139299
139577
  yield await ctx.emit(plugin4(ctx, "collab_agent_spawned", { id: entry.id, role: entry.role }));
139300
139578
  }
139301
- await waitForAgents(hub, roster.map((r2) => r2.id), ctx.signal, cfg.wallClockMs);
139579
+ await waitForAgents(hub, supervisor, roster.map((r2) => r2.id), ctx.signal, cfg.wallClockMs);
139580
+ yield* surfaceFailures(ctx, hub, supervisor, roster.map((r2) => r2.id));
139302
139581
  for (const r2 of roster)
139303
139582
  if (statusOf(hub, r2.id) === "done")
139304
139583
  doneIds.push(r2.id);
@@ -139309,7 +139588,9 @@ async function* runCollaborative(ctx, deps) {
139309
139588
  hub.state.addAgent(entry);
139310
139589
  supervisor.spawn({ entry, cwd: cwd2, mode: COLLAB_PEER_MODE_NAME });
139311
139590
  yield await ctx.emit(plugin4(ctx, "collab_agent_spawned", { id: entry.id, role: entry.role }));
139312
- const ok = await waitForAgent(hub, entry.id, ctx.signal, cfg.wallClockMs);
139591
+ const ok = await waitForAgent(hub, supervisor, entry.id, ctx.signal, cfg.wallClockMs);
139592
+ if (!ok)
139593
+ yield* surfaceFailures(ctx, hub, supervisor, [entry.id]);
139313
139594
  await supervisor.stop(entry.id);
139314
139595
  if (ok && statusOf(hub, entry.id) === "done")
139315
139596
  doneIds.push(entry.id);
@@ -139353,6 +139634,16 @@ ${mergeNote}` : ""}`));
139353
139634
  if (hub)
139354
139635
  await hub.close();
139355
139636
  releaseCollabLock(String(ctx.sessionId));
139637
+ for (const wt3 of worktrees.values()) {
139638
+ await removeWorktree(cwd2, wt3).catch(() => void 0);
139639
+ }
139640
+ try {
139641
+ rmSync(collabRunDir(runId), { recursive: true, force: true });
139642
+ rmSync(worktreeRoot(runId), { recursive: true, force: true });
139643
+ } catch {
139644
+ }
139645
+ if (worktrees.size > 0)
139646
+ await git(cwd2, ["worktree", "prune"]).catch(() => void 0);
139356
139647
  }
139357
139648
  }
139358
139649
  function lastUserPromptText(ctx) {
@@ -139403,30 +139694,62 @@ function slug(s2) {
139403
139694
  function statusOf(hub, id) {
139404
139695
  return hub.state.rosterView().agents.find((a2) => a2.id === id)?.status;
139405
139696
  }
139406
- async function waitForAgent(hub, id, signal, timeoutMs) {
139407
- const deadline = Date.now() + timeoutMs;
139697
+ function agentSettled(hub, supervisor, id, connected, bootDeadlineAt) {
139698
+ const status = statusOf(hub, id);
139699
+ if (status && status !== "pending")
139700
+ connected.add(id);
139701
+ if (status === "done")
139702
+ return "done";
139703
+ if (status === "failed" || status === "crashed" || status === "killed")
139704
+ return "failed";
139705
+ if (supervisor?.hasExited(id))
139706
+ return "failed";
139707
+ if (!connected.has(id) && Date.now() > bootDeadlineAt)
139708
+ return "failed";
139709
+ return void 0;
139710
+ }
139711
+ async function waitForAgent(hub, supervisor, id, signal, wallClockMs) {
139712
+ const wallDeadline = Date.now() + wallClockMs;
139713
+ const bootDeadlineAt = Date.now() + BOOT_DEADLINE_MS;
139714
+ const connected = /* @__PURE__ */ new Set();
139408
139715
  for (; ; ) {
139409
- const status = statusOf(hub, id);
139410
- if (status === "done")
139716
+ const settled = agentSettled(hub, supervisor, id, connected, bootDeadlineAt);
139717
+ if (settled === "done")
139411
139718
  return true;
139412
- if (status === "crashed" || status === "killed")
139719
+ if (settled === "failed")
139413
139720
  return false;
139414
- if (signal.aborted || Date.now() > deadline)
139721
+ if (signal.aborted || Date.now() > wallDeadline)
139415
139722
  return false;
139416
139723
  await sleep5(POLL_MS, signal);
139417
139724
  }
139418
139725
  }
139419
- async function waitForAgents(hub, ids, signal, timeoutMs) {
139420
- const deadline = Date.now() + timeoutMs;
139421
- const terminal = (s2) => s2 === "done" || s2 === "crashed" || s2 === "killed";
139726
+ async function waitForAgents(hub, supervisor, ids, signal, wallClockMs) {
139727
+ const wallDeadline = Date.now() + wallClockMs;
139728
+ const bootDeadlineAt = Date.now() + BOOT_DEADLINE_MS;
139729
+ const connected = /* @__PURE__ */ new Set();
139422
139730
  for (; ; ) {
139423
- if (ids.every((id) => terminal(statusOf(hub, id))))
139731
+ if (ids.every((id) => agentSettled(hub, supervisor, id, connected, bootDeadlineAt) !== void 0))
139424
139732
  return;
139425
- if (signal.aborted || Date.now() > deadline)
139733
+ if (signal.aborted || Date.now() > wallDeadline)
139426
139734
  return;
139427
139735
  await sleep5(POLL_MS, signal);
139428
139736
  }
139429
139737
  }
139738
+ async function* surfaceFailures(ctx, hub, supervisor, ids) {
139739
+ for (const id of ids) {
139740
+ const status = statusOf(hub, id);
139741
+ if (status === "done")
139742
+ continue;
139743
+ if (status !== "failed" && status !== "crashed" && status !== "killed") {
139744
+ hub.state.setStatus(id, "crashed", "did not reach a terminal status");
139745
+ }
139746
+ yield await ctx.emit(plugin4(ctx, "collab_agent_failed", {
139747
+ id,
139748
+ status: statusOf(hub, id),
139749
+ stderr: supervisor.stderrOf(id).slice(-6)
139750
+ }));
139751
+ }
139752
+ }
139430
139753
  function sleep5(ms, signal) {
139431
139754
  return new Promise((resolve13) => {
139432
139755
  if (signal.aborted)
@@ -139548,6 +139871,8 @@ async function* runCollabAgentLoop(ctx, opts) {
139548
139871
  permissions: autoApprove
139549
139872
  };
139550
139873
  const hub = await getProcessHubClient();
139874
+ if (hub)
139875
+ await hub.setStatus("working").catch(() => void 0);
139551
139876
  const detector = createStuckLoopDetector();
139552
139877
  const maxIterations = ctx.maxIterations ?? DEFAULT_MAX_ITERATIONS;
139553
139878
  let noop3 = 0;
@@ -144150,12 +144475,9 @@ var REMOTE_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
144150
144475
  // Voice input (capability-probed; transcribe fails coded without a transcriber).
144151
144476
  "session.hasTranscriber",
144152
144477
  "session.transcribe",
144153
- // Per-workspace transcript log (the mobile ChatStoreBridge persists through
144154
- // these; they're scoped to a workspace's NDJSON log, not host config).
144155
- "chat.append",
144156
- "chat.loadSegment",
144157
- "chat.clearLog",
144158
- "chat.migrate",
144478
+ // Read a workspace's transcript history from the runner's authoritative log
144479
+ // (a paired phone may read history, scoped to a workspace, not host config).
144480
+ "chat.loadHistory",
144159
144481
  // Workflows: READ + run an existing one only. Authoring (`workflows.save`,
144160
144482
  // `workflows.validateDraft`, `workflows.setEnabled`) is host-only — a paired
144161
144483
  // phone must not rewrite or re-enable the host's workflows.
@@ -144363,25 +144685,13 @@ var ipcInputSchemas = {
144363
144685
  "mobileGateway.status": z.undefined(),
144364
144686
  "mobileGateway.rotateToken": z.undefined(),
144365
144687
  "mobileGateway.setEnabled": z.object({ enabled: z.boolean() }).strict(),
144366
- "chat.append": z.object({
144367
- workspaceId: z.string().min(1).max(256),
144368
- events: z.array(z.unknown()).max(1e4)
144369
- }),
144370
- "chat.loadSegment": z.object({
144688
+ // `before` is a runner `seq` cursor; the page is RAW events. The runner itself
144689
+ // re-validates and caps at its own MAX_HISTORY_PAGE_LIMIT (2000), so bound the
144690
+ // renderer's raw-window request to that ceiling.
144691
+ "chat.loadHistory": z.object({
144371
144692
  workspaceId: z.string().min(1).max(256),
144372
144693
  before: z.number().int().nonnegative().nullable(),
144373
- limit: z.number().int().positive().max(1e3)
144374
- }),
144375
- "chat.clearLog": z.object({ workspaceId: z.string().min(1).max(256) }),
144376
- // chat.migrate writes the supplied events straight into per-workspace NDJSON
144377
- // logs on disk, so it's a filesystem-touching command: bound both the number
144378
- // of workspaces and the events per workspace, and lock the workspaceId to a
144379
- // non-empty bounded slug so it can't traverse out of the log directory.
144380
- "chat.migrate": z.object({
144381
- workspaces: z.array(z.object({
144382
- workspaceId: z.string().min(1).max(256),
144383
- events: z.array(z.unknown()).max(1e4)
144384
- })).max(100)
144694
+ limit: z.number().int().positive().max(2e3)
144385
144695
  }),
144386
144696
  // Vault writes are security-sensitive: lock the key name to a safe slug
144387
144697
  // (letters/digits + . _ / - , no traversal) and bound the secret size.
@@ -144626,13 +144936,7 @@ var MobileSessionHost = class {
144626
144936
  this.bus.handle("ask.respond", async ({ requestId, response }) => {
144627
144937
  this.answerAsk(requestId, response);
144628
144938
  });
144629
- this.bus.handle("chat.loadSegment", async () => ({ events: [], prevCursor: null }));
144630
- this.bus.handle("chat.append", async () => {
144631
- });
144632
- this.bus.handle("chat.clearLog", async () => {
144633
- });
144634
- this.bus.handle("chat.migrate", async () => {
144635
- });
144939
+ this.bus.handle("chat.loadHistory", async () => ({ events: [], prevCursor: null }));
144636
144940
  }
144637
144941
  /** Stream session events to clients + install the ask resolvers. */
144638
144942
  wire() {
@@ -145703,8 +146007,8 @@ function runProcess(cmd, args, opts) {
145703
146007
  }
145704
146008
 
145705
146009
  // ../plugin-browser/dist/browser-surface.js
145706
- var FRAME_INTERVAL_MS = 450;
145707
- var FAIL_GRACE = 4;
146010
+ var FRAME_INTERVAL_MS = 300;
146011
+ var FAIL_GRACE = 6;
145708
146012
  function buildBrowserSurface(deps) {
145709
146013
  return defineSurface({
145710
146014
  kind: "browser",
@@ -145831,9 +146135,10 @@ function buildBrowserSurface(deps) {
145831
146135
  } else if (msg.type === "move" && typeof msg.fx === "number" && typeof msg.fy === "number") {
145832
146136
  await browserSidecarCall("mousemove", { x: msg.fx * vw, y: msg.fy * vh }, deps).catch(() => void 0);
145833
146137
  void tick();
145834
- } else if (msg.type === "pick" && typeof msg.fx === "number" && typeof msg.fy === "number") {
145835
- const element = await browserSidecarCall("pick", { x: msg.fx * vw, y: msg.fy * vh }, deps).catch(() => null);
145836
- emit2({ type: "picked", element });
146138
+ } else if (msg.type === "capture" && typeof msg.fx === "number" && typeof msg.fy === "number" && typeof msg.fw === "number" && typeof msg.fh === "number") {
146139
+ const shot = await browserSidecarCall("capture", { x: msg.fx * vw, y: msg.fy * vh, width: msg.fw * vw, height: msg.fh * vh }, deps).catch(() => null);
146140
+ if (shot)
146141
+ emit2({ type: "captured", base64: shot.base64, mediaType: shot.mediaType });
145837
146142
  } else if (msg.type === "zoom" && typeof msg.factor === "number") {
145838
146143
  await browserSidecarCall("zoom", { factor: msg.factor }, deps).catch(() => void 0);
145839
146144
  bump();
@@ -155668,6 +155973,16 @@ async function runAgentCommand(argv) {
155668
155973
  for await (const _2 of session.runTurn(subtask)) void _2;
155669
155974
  } catch {
155670
155975
  }
155976
+ try {
155977
+ const hub = await getProcessHubClient();
155978
+ if (hub) {
155979
+ const mine = (await hub.roster()).agents.find((a2) => a2.id === hub.agentId);
155980
+ if (mine && mine.status !== "done") {
155981
+ await hub.setStatus("failed", "turn ended without calling collab_done");
155982
+ }
155983
+ }
155984
+ } catch {
155985
+ }
155671
155986
  })();
155672
155987
  await runUntilSignal2(runnerServer, session, turnDone);
155673
155988
  return 0;