@vibedeckx/darwin-arm64 0.2.16 → 0.2.17

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 +333 -83
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -185613,6 +185613,7 @@ var mapAgentSession = (row) => ({
185613
185613
  permission_mode: row.permission_mode ?? void 0,
185614
185614
  agent_type: row.agent_type ?? void 0,
185615
185615
  title: row.title,
185616
+ model: row.model ?? null,
185616
185617
  created_at: row.created_at,
185617
185618
  updated_at: row.updated_at,
185618
185619
  last_user_message_at: row.last_user_message_at,
@@ -185627,7 +185628,7 @@ var createAgentSessionRepos = (kdb, h) => ({
185627
185628
  // writes — this is what lets getLatestByBranch break ties
185628
185629
  // deterministically (see the schema.ts / sqlite.ts DDL comment on
185629
185630
  // agent_sessions).
185630
- create: async ({ id, project_id, branch, permission_mode, agent_type }) => {
185631
+ create: async ({ id, project_id, branch, permission_mode, agent_type, model }) => {
185631
185632
  await kdb.insertInto("agent_sessions").values({
185632
185633
  id,
185633
185634
  project_id,
@@ -185635,6 +185636,7 @@ var createAgentSessionRepos = (kdb, h) => ({
185635
185636
  status: "running",
185636
185637
  permission_mode: permission_mode ?? "edit",
185637
185638
  agent_type: agent_type ?? "claude-code",
185639
+ model: model ?? null,
185638
185640
  created_at: h.nowMs(),
185639
185641
  updated_at: h.nowMs()
185640
185642
  }).execute();
@@ -185677,6 +185679,9 @@ var createAgentSessionRepos = (kdb, h) => ({
185677
185679
  updateAgentType: async (id, agent_type) => {
185678
185680
  await kdb.updateTable("agent_sessions").set({ agent_type, updated_at: h.nowMs() }).where("id", "=", id).execute();
185679
185681
  },
185682
+ updateModel: async (id, model) => {
185683
+ await kdb.updateTable("agent_sessions").set({ model, updated_at: h.nowMs() }).where("id", "=", id).execute();
185684
+ },
185680
185685
  updateTitle: async (id, title) => {
185681
185686
  await kdb.updateTable("agent_sessions").set({ title, updated_at: h.nowMs() }).where("id", "=", id).execute();
185682
185687
  },
@@ -186591,6 +186596,10 @@ var createDatabase = (dbPath) => {
186591
186596
  branch TEXT NOT NULL DEFAULT '',
186592
186597
  status TEXT NOT NULL DEFAULT 'running',
186593
186598
  title TEXT DEFAULT NULL,
186599
+ -- Per-session agent model, e.g. 'opus' or 'gpt-5.6-sol'. NULL = use the
186600
+ -- CLI's own default (no flag is passed). Never validated: an unknown
186601
+ -- name is passed to the CLI and fails there.
186602
+ model TEXT DEFAULT NULL,
186594
186603
  -- Millisecond-precision timestamps. CURRENT_TIMESTAMP is seconds-only,
186595
186604
  -- which lets two sessions tie on updated_at within the same second and
186596
186605
  -- corrupts the ordering used by getLatestByBranch. The 'YYYY-MM-DD
@@ -186863,6 +186872,10 @@ var createDatabase = (dbPath) => {
186863
186872
  if (!sessionInfoV6.some((col) => col.name === "favorited_at")) {
186864
186873
  db.exec("ALTER TABLE agent_sessions ADD COLUMN favorited_at INTEGER DEFAULT NULL");
186865
186874
  }
186875
+ const sessionInfoV7 = db.prepare("PRAGMA table_info(agent_sessions)").all();
186876
+ if (!sessionInfoV7.some((col) => col.name === "model")) {
186877
+ db.exec("ALTER TABLE agent_sessions ADD COLUMN model TEXT DEFAULT NULL");
186878
+ }
186866
186879
  db.exec(`
186867
186880
  CREATE INDEX IF NOT EXISTS idx_agent_sessions_project_branch
186868
186881
  ON agent_sessions(project_id, branch);
@@ -201310,8 +201323,11 @@ var UserInputParamsSchema = external_exports.looseObject({
201310
201323
 
201311
201324
  // src/protocol/codex/cli.ts
201312
201325
  var CROSS_REMOTE_MCP_TOKEN_ENV = "VIBEDECKX_CROSS_REMOTE_MCP_TOKEN";
201313
- function buildCodexAppServerSpawnConfig(nativeBinary, crossRemoteMcp) {
201326
+ function buildCodexAppServerSpawnConfig(nativeBinary, crossRemoteMcp, model) {
201314
201327
  const args = ["app-server"];
201328
+ if (model && model.trim()) {
201329
+ args.push("-c", `model=${JSON.stringify(model.trim())}`);
201330
+ }
201315
201331
  if (crossRemoteMcp) {
201316
201332
  args.push(
201317
201333
  "-c",
@@ -201396,11 +201412,16 @@ function withNpxFallback(nativeBinary, args) {
201396
201412
  }
201397
201413
  return { command: "npx", args: ["-y", CLAUDE_NPM_PACKAGE, ...args] };
201398
201414
  }
201399
- function buildClaudeSessionSpawnConfig(nativeBinary, permissionMode, mcpConfigArg) {
201415
+ function buildClaudeSessionSpawnConfig(nativeBinary, permissionMode, mcpConfigArg, model) {
201400
201416
  const permissionFlag = permissionMode === "plan" ? "--permission-mode=plan" : "--dangerously-skip-permissions";
201401
201417
  const args = [
201402
201418
  ...STREAM_JSON_ARGS,
201403
- permissionFlag,
201419
+ permissionFlag
201420
+ ];
201421
+ if (model && model.trim()) {
201422
+ args.push("--model", model.trim());
201423
+ }
201424
+ args.push(
201404
201425
  // AskUserQuestion can't work over piped (non-TTY) stdin: claude resolves it
201405
201426
  // internally as "dismissed" before we can present a picker and wait for the
201406
201427
  // user. Disable it so the agent falls back to asking in plain text, which the
@@ -201408,7 +201429,7 @@ function buildClaudeSessionSpawnConfig(nativeBinary, permissionMode, mcpConfigAr
201408
201429
  "--disallowedTools",
201409
201430
  "AskUserQuestion",
201410
201431
  "--verbose"
201411
- ];
201432
+ );
201412
201433
  if (mcpConfigArg) {
201413
201434
  args.push("--mcp-config", mcpConfigArg);
201414
201435
  }
@@ -202501,11 +202522,12 @@ var ClaudeCodeProvider = class {
202501
202522
  isAvailable() {
202502
202523
  return true;
202503
202524
  }
202504
- buildSpawnConfig(_cwd, permissionMode, crossRemoteMcp) {
202525
+ buildSpawnConfig(_cwd, permissionMode, crossRemoteMcp, model) {
202505
202526
  return buildClaudeSessionSpawnConfig(
202506
202527
  this.detectBinary(),
202507
202528
  permissionMode,
202508
- crossRemoteMcp ? buildMcpConfigArg(crossRemoteMcp) : void 0
202529
+ crossRemoteMcp ? buildMcpConfigArg(crossRemoteMcp) : void 0,
202530
+ model
202509
202531
  );
202510
202532
  }
202511
202533
  parseStdoutLine(line, _sessionId) {
@@ -202695,9 +202717,9 @@ var CodexProvider = class _CodexProvider {
202695
202717
  isAvailable() {
202696
202718
  return true;
202697
202719
  }
202698
- buildSpawnConfig(_cwd, permissionMode, crossRemoteMcp) {
202720
+ buildSpawnConfig(_cwd, permissionMode, crossRemoteMcp, model) {
202699
202721
  this.lastPermissionMode = permissionMode;
202700
- return buildCodexAppServerSpawnConfig(this.detectBinary(), crossRemoteMcp);
202722
+ return buildCodexAppServerSpawnConfig(this.detectBinary(), crossRemoteMcp, model);
202701
202723
  }
202702
202724
  // ============ Task 5.5: parseStdoutLine — JSON-RPC message routing ============
202703
202725
  parseStdoutLine(line, sessionId) {
@@ -224595,6 +224617,21 @@ var TurnCompletionLedger = class {
224595
224617
  };
224596
224618
 
224597
224619
  // src/agent-session-manager.ts
224620
+ function buildStartupFailureMessage(agentType, stderrTail, stdoutTail) {
224621
+ const provider = getProvider(agentType);
224622
+ const name25 = provider.getDisplayName();
224623
+ const details = [stdoutTail.trim(), stderrTail.trim()].filter(Boolean).join("\n");
224624
+ const hint = stdoutTail.trim() ? void 0 : provider.getInstallHint?.();
224625
+ let msg = `Couldn't start ${name25}.`;
224626
+ if (hint) msg += `
224627
+
224628
+ ${hint}`;
224629
+ if (details) msg += `
224630
+
224631
+ Details:
224632
+ ${details}`;
224633
+ return msg;
224634
+ }
224598
224635
  var SUMMARY_TEXT_CAP = 1500;
224599
224636
  function extractLastAssistantText(entries) {
224600
224637
  for (let i = entries.length - 1; i >= 0; i--) {
@@ -224901,6 +224938,7 @@ var AgentSessionManager = class {
224901
224938
  await this.ensureResidentCapacity({ projectId, branch }, { force });
224902
224939
  const sessionId = opts.sessionId ?? randomUUID();
224903
224940
  const branchKey = branch ?? "";
224941
+ const model = opts.model?.trim() ? opts.model.trim() : null;
224904
224942
  const absoluteWorktreePath = resolveWorktreePath(projectPath, branch);
224905
224943
  if (!skipDb) {
224906
224944
  await this.storage.agentSessions.create({
@@ -224908,7 +224946,8 @@ var AgentSessionManager = class {
224908
224946
  project_id: projectId,
224909
224947
  branch: branchKey,
224910
224948
  permission_mode: permissionMode,
224911
- agent_type: agentType
224949
+ agent_type: agentType,
224950
+ model
224912
224951
  });
224913
224952
  }
224914
224953
  if (!skipDb) {
@@ -224936,6 +224975,7 @@ var AgentSessionManager = class {
224936
224975
  permissionMode,
224937
224976
  crossRemoteMcp: opts.crossRemoteMcp,
224938
224977
  agentType,
224978
+ model,
224939
224979
  completion: new TurnCompletionLedger(),
224940
224980
  graceTimer: null,
224941
224981
  eventChain: Promise.resolve(),
@@ -225026,7 +225066,7 @@ var AgentSessionManager = class {
225026
225066
  this.broadcastRaw(session.id, { finished: true });
225027
225067
  return;
225028
225068
  }
225029
- const config2 = provider.buildSpawnConfig(cwd, session.permissionMode, session.crossRemoteMcp);
225069
+ const config2 = provider.buildSpawnConfig(cwd, session.permissionMode, session.crossRemoteMcp, session.model);
225030
225070
  if (config2.command !== "npx") {
225031
225071
  const agentVersion = getBinaryVersion(config2.command);
225032
225072
  console.log(`[AgentSession] ${provider.getDisplayName()} version: ${agentVersion ?? "unknown (--version probe failed)"}`);
@@ -225034,6 +225074,7 @@ var AgentSessionManager = class {
225034
225074
  console.log(`[AgentSession] ${provider.getDisplayName()} running via npx (version resolved at spawn by npm)`);
225035
225075
  }
225036
225076
  session.producedOutput = false;
225077
+ session.unparsedStdoutTail = "";
225037
225078
  this.resetCompletion(session);
225038
225079
  let stderrTail = "";
225039
225080
  let spawnFailed = false;
@@ -225082,7 +225123,7 @@ var AgentSessionManager = class {
225082
225123
  try {
225083
225124
  await this.pushEntry(session.id, {
225084
225125
  type: "error",
225085
- message: this.buildStartupFailureMessage(session.agentType, stderrTail),
225126
+ message: buildStartupFailureMessage(session.agentType, stderrTail, session.unparsedStdoutTail ?? ""),
225086
225127
  timestamp: Date.now()
225087
225128
  }, true);
225088
225129
  } catch (err) {
@@ -225115,7 +225156,7 @@ var AgentSessionManager = class {
225115
225156
  const isNotFound = error48.code === "ENOENT";
225116
225157
  this.pushEntry(session.id, {
225117
225158
  type: "error",
225118
- message: isNotFound ? this.buildStartupFailureMessage(session.agentType, stderrTail) : error48.message,
225159
+ message: isNotFound ? buildStartupFailureMessage(session.agentType, stderrTail, session.unparsedStdoutTail ?? "") : error48.message,
225119
225160
  timestamp: Date.now()
225120
225161
  }, true).catch((err) => {
225121
225162
  console.error(`[AgentSession] Failed to push spawn-error entry for ${session.id}:`, err);
@@ -225240,6 +225281,8 @@ var AgentSessionManager = class {
225240
225281
  const events = provider.parseStdoutLine(line, session.id);
225241
225282
  if (events.length > 0) {
225242
225283
  session.producedOutput = true;
225284
+ } else {
225285
+ session.unparsedStdoutTail = ((session.unparsedStdoutTail ?? "") + line + "\n").slice(-4e3);
225243
225286
  }
225244
225287
  for (const event of events) {
225245
225288
  await this.processAgentEvent(session.id, event);
@@ -225493,25 +225536,6 @@ var AgentSessionManager = class {
225493
225536
  /**
225494
225537
  * Push a new entry with ADD patch
225495
225538
  */
225496
- /**
225497
- * Build a user-facing message for when an agent process fails to start.
225498
- * Includes the provider's install hint plus any captured stderr tail.
225499
- */
225500
- buildStartupFailureMessage(agentType, stderrTail) {
225501
- const provider = getProvider(agentType);
225502
- const name25 = provider.getDisplayName();
225503
- const hint = provider.getInstallHint?.();
225504
- let msg = `Couldn't start ${name25}.`;
225505
- if (hint) msg += `
225506
-
225507
- ${hint}`;
225508
- const details = stderrTail.trim();
225509
- if (details) msg += `
225510
-
225511
- Details:
225512
- ${details}`;
225513
- return msg;
225514
- }
225515
225539
  /**
225516
225540
  * Allocate the next entry index, record the message in the in-memory store,
225517
225541
  * and build its ADD replay patch. Shared by `pushEntry` and `pushTurnEnd` so
@@ -226003,12 +226027,19 @@ ${details}`;
226003
226027
  session.store.currentAssistantIndex = null;
226004
226028
  session.buffer = "";
226005
226029
  this.resetCompletion(session);
226030
+ const previousAgentType = session.agentType;
226006
226031
  getProvider(session.agentType).onSessionDestroyed?.(sessionId);
226007
226032
  session.agentType = agentType;
226008
226033
  if (!session.skipDb) await this.storage.agentSessions.updateAgentType(sessionId, agentType);
226034
+ const clearedModel = session.model;
226035
+ if (clearedModel !== null) {
226036
+ session.model = null;
226037
+ if (!session.skipDb) await this.storage.agentSessions.updateModel(sessionId, null);
226038
+ }
226039
+ const agentDisplayName = (t) => t === "codex" ? "Codex" : "Claude Code";
226009
226040
  await this.pushEntry(sessionId, {
226010
226041
  type: "system",
226011
- content: `Coding agent switched to ${agentType === "codex" ? "Codex" : "Claude Code"}.`,
226042
+ content: `Coding agent switched to ${agentDisplayName(agentType)}.` + (clearedModel !== null ? ` Model reset to the default (\`${clearedModel}\` was set for ${agentDisplayName(previousAgentType)}).` : ""),
226012
226043
  timestamp: Date.now()
226013
226044
  });
226014
226045
  session.dormant = true;
@@ -226320,6 +226351,7 @@ ${details}`;
226320
226351
  skipDb: false,
226321
226352
  permissionMode,
226322
226353
  agentType: dbSession.agent_type || "claude-code",
226354
+ model: dbSession.model ?? null,
226323
226355
  completion: new TurnCompletionLedger(),
226324
226356
  graceTimer: null,
226325
226357
  eventChain: Promise.resolve(),
@@ -226381,13 +226413,16 @@ ${details}`;
226381
226413
  const branch = source?.branch ?? (sourceRow.branch || null);
226382
226414
  const permissionMode = source?.permissionMode ?? (sourceRow?.permission_mode === "plan" ? "plan" : "edit");
226383
226415
  const agentType = agentTypeOverride ?? source?.agentType ?? (sourceRow?.agent_type || "claude-code");
226416
+ const sourceAgentType = source?.agentType ?? (sourceRow?.agent_type || "claude-code");
226417
+ const model = agentType === sourceAgentType ? source?.model ?? sourceRow?.model ?? null : null;
226384
226418
  const newId = opts.sessionId ?? randomUUID();
226385
226419
  await this.storage.agentSessions.create({
226386
226420
  id: newId,
226387
226421
  project_id: projectId,
226388
226422
  branch: branch ?? "",
226389
226423
  permission_mode: permissionMode,
226390
- agent_type: agentType
226424
+ agent_type: agentType,
226425
+ model
226391
226426
  });
226392
226427
  await this.storage.agentSessions.updateStatusPreservingTimestamp(newId, "stopped");
226393
226428
  for (const row of entryRows) {
@@ -226422,6 +226457,7 @@ ${details}`;
226422
226457
  skipDb: false,
226423
226458
  permissionMode,
226424
226459
  agentType,
226460
+ model,
226425
226461
  completion: new TurnCompletionLedger(),
226426
226462
  graceTimer: null,
226427
226463
  eventChain: Promise.resolve(),
@@ -227166,7 +227202,7 @@ function groupByServer(mappings) {
227166
227202
 
227167
227203
  // src/remote-agent-sessions.ts
227168
227204
  async function createRemoteAgentSession(deps, params) {
227169
- const { projectId, agentMode, remoteConfig, branch, permissionMode, agentType, force, userId } = params;
227205
+ const { projectId, agentMode, remoteConfig, branch, permissionMode, agentType, model, force, userId } = params;
227170
227206
  const remoteSessionId = randomUUID3();
227171
227207
  const localSessionId = `remote-${agentMode}-${projectId}-${remoteSessionId}`;
227172
227208
  const crossRemoteMcp = await mintCrossRemoteMcpConfig(
@@ -227188,7 +227224,7 @@ async function createRemoteAgentSession(deps, params) {
227188
227224
  remoteConfig.server_api_key || "",
227189
227225
  "POST",
227190
227226
  `/api/path/agent-sessions/new`,
227191
- { path: remoteConfig.remote_path, branch, permissionMode, agentType, force, sessionId: remoteSessionId, crossRemoteMcp },
227227
+ { path: remoteConfig.remote_path, branch, permissionMode, agentType, force, sessionId: remoteSessionId, crossRemoteMcp, model },
227192
227228
  { reverseConnectManager: deps.reverseConnectManager ?? void 0 }
227193
227229
  );
227194
227230
  if (!result.ok) {
@@ -235587,6 +235623,14 @@ function resolveUserId(authResult) {
235587
235623
 
235588
235624
  // src/routes/agent-session-routes.ts
235589
235625
  import { randomUUID as randomUUID15 } from "crypto";
235626
+
235627
+ // src/protocol/model-suggestions.ts
235628
+ var MODEL_SUGGESTIONS = {
235629
+ "claude-code": ["opus", "sonnet", "haiku"],
235630
+ codex: ["gpt-5.6-sol", "gpt-5.6-codex", "o3"]
235631
+ };
235632
+
235633
+ // src/routes/agent-session-routes.ts
235590
235634
  async function resolveProjectPath(projectId, storage) {
235591
235635
  if (projectId.startsWith("path:")) {
235592
235636
  return projectId.slice(5);
@@ -235650,6 +235694,7 @@ var routes12 = async (fastify2) => {
235650
235694
  status: session?.status || "stopped",
235651
235695
  permissionMode: session?.permissionMode || "edit",
235652
235696
  agentType: session?.agentType || "claude-code",
235697
+ model: session?.model ?? null,
235653
235698
  title: dbRow?.title ?? null
235654
235699
  },
235655
235700
  messages
@@ -235660,15 +235705,23 @@ var routes12 = async (fastify2) => {
235660
235705
  fastify2.agentSessionManager.emitSessionTitle(projectId, branch, sessionId, title);
235661
235706
  }
235662
235707
  fastify2.get("/api/agent-providers", async (_req, reply) => {
235663
- const providers2 = getAllProviders().map((provider) => ({
235664
- type: provider.getAgentType(),
235665
- displayName: provider.getDisplayName(),
235666
- available: provider.isAvailable?.() ?? provider.detectBinary() !== null
235667
- }));
235708
+ const providers2 = getAllProviders().map((provider) => {
235709
+ const type = provider.getAgentType();
235710
+ return {
235711
+ type,
235712
+ displayName: provider.getDisplayName(),
235713
+ available: provider.isAvailable?.() ?? provider.detectBinary() !== null,
235714
+ // Suggestions only. The picker also accepts free text, and nothing
235715
+ // validates against this list — the session's real capabilities depend
235716
+ // on the CLI version and account tier of whichever machine spawns it,
235717
+ // which this server cannot know.
235718
+ models: MODEL_SUGGESTIONS[type] ?? []
235719
+ };
235720
+ });
235668
235721
  return reply.code(200).send({ providers: providers2 });
235669
235722
  });
235670
235723
  fastify2.post("/api/path/agent-sessions", async (req, reply) => {
235671
- const { path: projectPath, branch, permissionMode, agentType, force } = req.body;
235724
+ const { path: projectPath, branch, permissionMode, agentType, force, model } = req.body;
235672
235725
  if (!projectPath) {
235673
235726
  return reply.code(400).send({ error: "Path is required" });
235674
235727
  }
@@ -235716,6 +235769,7 @@ var routes12 = async (fastify2) => {
235716
235769
  status: effectiveStatus,
235717
235770
  permissionMode: session?.permissionMode || "edit",
235718
235771
  agentType: session?.agentType || "claude-code",
235772
+ model: session?.model ?? null,
235719
235773
  processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
235720
235774
  },
235721
235775
  messages
@@ -235760,7 +235814,7 @@ var routes12 = async (fastify2) => {
235760
235814
  }
235761
235815
  );
235762
235816
  fastify2.post("/api/path/agent-sessions/new", async (req, reply) => {
235763
- const { path: projectPath, branch, permissionMode, agentType, force, sessionId, crossRemoteMcp } = req.body;
235817
+ const { path: projectPath, branch, permissionMode, agentType, force, sessionId, crossRemoteMcp, model } = req.body;
235764
235818
  if (!projectPath) {
235765
235819
  return reply.code(400).send({ error: "Path is required" });
235766
235820
  }
@@ -235788,7 +235842,7 @@ var routes12 = async (fastify2) => {
235788
235842
  agentType || "claude-code",
235789
235843
  false,
235790
235844
  force === true,
235791
- { sessionId, crossRemoteMcp }
235845
+ { sessionId, crossRemoteMcp, model }
235792
235846
  );
235793
235847
  const session = fastify2.agentSessionManager.getSession(createdSessionId);
235794
235848
  return reply.code(200).send({
@@ -235799,6 +235853,7 @@ var routes12 = async (fastify2) => {
235799
235853
  status: session?.status || "running",
235800
235854
  permissionMode: session?.permissionMode || "edit",
235801
235855
  agentType: session?.agentType || "claude-code",
235856
+ model: session?.model ?? null,
235802
235857
  processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(session.id) : false
235803
235858
  },
235804
235859
  messages: []
@@ -235901,7 +235956,7 @@ var routes12 = async (fastify2) => {
235901
235956
  if (!project) {
235902
235957
  return reply.code(404).send({ error: "Project not found" });
235903
235958
  }
235904
- const { branch, permissionMode, agentType } = req.body;
235959
+ const { branch, permissionMode, agentType, model } = req.body;
235905
235960
  let agentMode = project.agent_mode;
235906
235961
  let useRemoteAgent = agentMode !== "local";
235907
235962
  if (useRemoteAgent && agentMode === "remote") {
@@ -235936,7 +235991,7 @@ var routes12 = async (fastify2) => {
235936
235991
  remoteConfig.server_api_key || "",
235937
235992
  "POST",
235938
235993
  `/api/path/agent-sessions`,
235939
- { path: remoteConfig.remote_path, branch, permissionMode, agentType }
235994
+ { path: remoteConfig.remote_path, branch, permissionMode, agentType, model }
235940
235995
  );
235941
235996
  console.log(`[API] Remote proxy result: ok=${result.ok}, status=${result.status}, data=${JSON.stringify(result.data).substring(0, 500)}`);
235942
235997
  if (result.ok) {
@@ -236014,6 +236069,7 @@ var routes12 = async (fastify2) => {
236014
236069
  status: effectiveStatus,
236015
236070
  permissionMode: session?.permissionMode || "edit",
236016
236071
  agentType: session?.agentType || "claude-code",
236072
+ model: session?.model ?? null,
236017
236073
  processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
236018
236074
  },
236019
236075
  messages
@@ -236030,7 +236086,7 @@ var routes12 = async (fastify2) => {
236030
236086
  if (!project) {
236031
236087
  return reply.code(404).send({ error: "Project not found" });
236032
236088
  }
236033
- const { branch, permissionMode, agentType, force } = req.body;
236089
+ const { branch, permissionMode, agentType, force, model } = req.body;
236034
236090
  const agentMode = project.agent_mode;
236035
236091
  const useRemoteAgent = agentMode !== "local";
236036
236092
  if (useRemoteAgent) {
@@ -236056,7 +236112,8 @@ var routes12 = async (fastify2) => {
236056
236112
  permissionMode: permissionMode || "edit",
236057
236113
  agentType,
236058
236114
  force: force === true,
236059
- userId
236115
+ userId,
236116
+ model
236060
236117
  }
236061
236118
  );
236062
236119
  if (created.ok) {
@@ -236089,7 +236146,7 @@ var routes12 = async (fastify2) => {
236089
236146
  agentType || "claude-code",
236090
236147
  false,
236091
236148
  force === true,
236092
- { sessionId: preSessionId, crossRemoteMcp }
236149
+ { sessionId: preSessionId, crossRemoteMcp, model }
236093
236150
  );
236094
236151
  const session = fastify2.agentSessionManager.getSession(sessionId);
236095
236152
  return reply.code(200).send({
@@ -236100,6 +236157,7 @@ var routes12 = async (fastify2) => {
236100
236157
  status: session?.status || "running",
236101
236158
  permissionMode: session?.permissionMode || "edit",
236102
236159
  agentType: session?.agentType || "claude-code",
236160
+ model: session?.model ?? null,
236103
236161
  processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
236104
236162
  },
236105
236163
  messages: []
@@ -236156,6 +236214,7 @@ var routes12 = async (fastify2) => {
236156
236214
  status: session.status,
236157
236215
  permissionMode: session.permissionMode,
236158
236216
  agentType: session.agentType || "claude-code",
236217
+ model: session.model ?? null,
236159
236218
  processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id)
236160
236219
  },
236161
236220
  messages
@@ -237518,77 +237577,268 @@ var command_routes_default = (0, import_fastify_plugin19.default)(routes18, { na
237518
237577
  var import_fastify_plugin20 = __toESM(require_plugin2(), 1);
237519
237578
 
237520
237579
  // src/utils/review-brief.ts
237521
- var AI_TIMEOUT_MS2 = 15e3;
237522
- var PER_MESSAGE_MAX_CHARS = 1500;
237523
- var TOTAL_INPUT_MAX_CHARS = 24e3;
237524
- var HEAD_CHARS = 8e3;
237525
- var TAIL_CHARS = 15e3;
237580
+ var BASE_TIMEOUT_MS = 15e3;
237581
+ var TIMEOUT_MS_PER_1K_CHARS = 120;
237582
+ var MAX_TIMEOUT_MS = 6e4;
237583
+ var PER_MESSAGE_MAX_CHARS = 6e3;
237584
+ var COMPACT_TRIGGER_CHARS = 12e4;
237585
+ var COMPACT_SLICE_BUDGETS = [96e3, 24e3, 6e3];
237586
+ var RECENT_VERBATIM_RATIO = 0.3;
237587
+ var MIN_RECENT_VERBATIM_CHARS = 2e3;
237588
+ var MAX_FOLD_CALLS = 8;
237589
+ var COMPACT_SUMMARY_MAX_CHARS = 6e3;
237590
+ var COMPACT_SUMMARY_MAX_TOKENS = 1500;
237591
+ var REVERSALS_MAX_CHARS = 2e3;
237592
+ var REVERSALS_MAX_TOKENS = 500;
237526
237593
  var BRIEF_MAX_CHARS = 4e3;
237594
+ var BRIEF_MAX_TOKENS = 1e3;
237595
+ var REVERSAL_MIN_CHARS = 6e3;
237596
+ var SEP = "\n\n";
237597
+ var COMPACTION_NOTE = "_Note: the source conversation was compressed before distillation \u2014 older turns were summarized rather than read verbatim._";
237598
+ var BRIEF_TRUNCATION_NOTE = "\n\n_[brief truncated at the length limit \u2014 treat it as incomplete]_";
237527
237599
  var SYSTEM_PROMPT2 = [
237528
237600
  "You distill a coding-agent conversation into an intent brief for an independent code reviewer.",
237529
237601
  "The reviewer will NOT see the conversation \u2014 only your brief plus the actual code, so capture what the code alone cannot show:",
237530
237602
  "1. The original request and its goal.",
237531
- "2. Constraints and explicit user decisions, including approaches the user rejected.",
237603
+ "2. Constraints and decisions, including approaches that were rejected and why. A rejection counts whether the user made it or the agent made it under a principle the user set.",
237532
237604
  "3. Trade-offs or limitations that were acknowledged and accepted.",
237533
- "Do NOT include the agent's reasoning, self-assessment, or claims that the work is correct or complete \u2014 the reviewer must judge that independently.",
237534
- "Write concise markdown bullets under those numbered headings, under 400 words total, in the same language as the conversation. Reply with the brief only."
237605
+ "4. Behaviour established by actually running something \u2014 observed output, exit codes, error text, which stream an error arrived on. Report what was observed; that is evidence the reviewer cannot recover by reading code.",
237606
+ "Later statements supersede earlier ones on the same topic. An option explored at length early is often rejected later in a single sentence, and the rejection wins. Where the conversation ends in a decision table or summary, that is authoritative wherever it conflicts with earlier prose.",
237607
+ "The input may arrive as numbered compressed slices followed by verbatim recent turns. Slice numbers are chronological: a later slice overrules an earlier one.",
237608
+ "Decisions the agent made that the user did not contradict are binding \u2014 silence is agreement.",
237609
+ "Do NOT include the agent's self-assessment or any claim that the work is correct or complete \u2014 the reviewer must judge that independently.",
237610
+ "Every statement must trace to something in the conversation. If a heading has nothing real to report, say so rather than inventing plausible-sounding content.",
237611
+ "Write concise markdown bullets under those numbered headings, under 500 words total, in the same language as the conversation. Reply with the brief only."
237535
237612
  ].join("\n");
237536
- function capText(text2, max) {
237537
- return text2.length > max ? text2.slice(0, max) + "\u2026" : text2;
237613
+ var REVERSAL_SYSTEM_PROMPT = [
237614
+ "You read a coding-agent conversation and report ONLY the conclusions that were later reversed, dropped, or replaced.",
237615
+ "A design conversation converges by killing its own hypotheses: an option explored in depth early is often rejected later in one sentence, so weigh finality over how much was written.",
237616
+ "A reversal counts whether the user or the agent made it. Ignore rewording that leaves the decision intact.",
237617
+ "Output one line per reversal: SUPERSEDED: <what was concluded earlier> -> <what replaced it> (<why>)",
237618
+ "Report the most decisive reversals first and stay under 200 words \u2014 a list that runs long will be discarded whole, so prioritise rather than pad.",
237619
+ "If nothing was reversed, reply with exactly: NONE"
237620
+ ].join("\n");
237621
+ var COMPACT_SYSTEM_PROMPT = [
237622
+ "You compress one slice of a coding-agent conversation. A later pass will distill the whole thing into an intent brief for a code reviewer.",
237623
+ "You are told which slice of how many this is. Other slices are compressed separately and you cannot see them, so never present a conclusion here as final \u2014 a later slice may overturn it.",
237624
+ "Preserve, in this order of priority:",
237625
+ "1. Reversals and corrections \u2014 any conclusion that was dropped, replaced, or corrected. Quote the deciding sentence verbatim.",
237626
+ "2. Constraints and decisions the user stated, and approaches that were rejected, with the reason.",
237627
+ "3. Behaviour established by actually running something: observed output, exit codes, error text, which stream an error arrived on.",
237628
+ "4. Trade-offs that were acknowledged and accepted.",
237629
+ "Drop: exploration narrative, restated background, tool mechanics, and anything a reviewer could recover by reading the code.",
237630
+ "Stay under 800 words. An over-long slice is discarded whole rather than cut short, so drop lower-priority material instead of running over.",
237631
+ "Keep the conversation's own language. Reply with the compressed slice only, no preamble."
237632
+ ].join("\n");
237633
+ function timeoutForInput(chars) {
237634
+ return Math.min(MAX_TIMEOUT_MS, BASE_TIMEOUT_MS + Math.floor(chars / 1e3) * TIMEOUT_MS_PER_1K_CHARS);
237635
+ }
237636
+ function isSizeRelatedFailure(error48) {
237637
+ const err = error48;
237638
+ if (err?.name === "AbortError" || err?.name === "TimeoutError") return true;
237639
+ const message = err?.message ?? "";
237640
+ return /context[ _-]?length|maximum context|too many tokens|token limit|prompt is too long|request too large|timed? ?out/i.test(
237641
+ message
237642
+ );
237538
237643
  }
237539
- function serializeConversationForBrief(messages) {
237540
- const lines = [];
237644
+ function withinLimit(text2, max, label) {
237645
+ if (text2.length <= max) return true;
237646
+ console.warn(`[ReviewBrief] ${label} ran to ${text2.length} chars (limit ${max}) \u2014 discarding rather than truncating`);
237647
+ return false;
237648
+ }
237649
+ function capMessage(text2) {
237650
+ return text2.length > PER_MESSAGE_MAX_CHARS ? text2.slice(0, PER_MESSAGE_MAX_CHARS) + "\u2026" : text2;
237651
+ }
237652
+ function toEntries(messages) {
237653
+ const entries = [];
237541
237654
  for (const msg of messages) {
237542
237655
  if (!msg) continue;
237543
237656
  if (msg.type === "user" && !msg.event) {
237544
237657
  const text2 = extractUserText(msg.content).trim();
237545
- if (text2) lines.push(`User: ${capText(text2, PER_MESSAGE_MAX_CHARS)}`);
237658
+ if (text2) entries.push({ role: "User", text: capMessage(text2) });
237546
237659
  } else if (msg.type === "assistant" && typeof msg.content === "string") {
237547
237660
  const text2 = msg.content.trim();
237548
- if (text2) lines.push(`Agent: ${capText(text2, PER_MESSAGE_MAX_CHARS)}`);
237661
+ if (text2) entries.push({ role: "Agent", text: capMessage(text2) });
237662
+ }
237663
+ }
237664
+ return entries;
237665
+ }
237666
+ function render(entry) {
237667
+ return `${entry.role}: ${entry.text}`;
237668
+ }
237669
+ function renderAll(entries) {
237670
+ return entries.map(render).join(SEP);
237671
+ }
237672
+ function serializeConversationForBrief(messages) {
237673
+ return renderAll(toEntries(messages));
237674
+ }
237675
+ function splitRecent(entries, maxChars) {
237676
+ let size = 0;
237677
+ let i = entries.length;
237678
+ while (i > 0) {
237679
+ const len = render(entries[i - 1]).length + SEP.length;
237680
+ if (size + len > maxChars) break;
237681
+ size += len;
237682
+ i--;
237683
+ }
237684
+ return { older: entries.slice(0, i), recent: entries.slice(i) };
237685
+ }
237686
+ function chunkEntries(entries, maxChars) {
237687
+ const chunks = [];
237688
+ let current = [];
237689
+ let size = 0;
237690
+ for (const entry of entries) {
237691
+ const len = render(entry).length + SEP.length;
237692
+ if (current.length > 0 && size + len > maxChars) {
237693
+ chunks.push(current);
237694
+ current = [];
237695
+ size = 0;
237549
237696
  }
237697
+ current.push(entry);
237698
+ size += len;
237550
237699
  }
237551
- const full = lines.join("\n\n");
237552
- if (full.length <= TOTAL_INPUT_MAX_CHARS) return full;
237553
- return `${full.slice(0, HEAD_CHARS)}
237700
+ if (current.length > 0) chunks.push(current);
237701
+ return chunks;
237702
+ }
237703
+ function telemetryFor(functionId, userId) {
237704
+ return userId ? {
237705
+ isEnabled: true,
237706
+ functionId,
237707
+ metadata: { userId, tags: ["vibedeckx", functionId] }
237708
+ } : void 0;
237709
+ }
237710
+ async function compactChunk(model, chunk, part, total, userId) {
237711
+ try {
237712
+ const result = await generateText({
237713
+ model,
237714
+ system: COMPACT_SYSTEM_PROMPT,
237715
+ prompt: `Slice ${part} of ${total}.
237554
237716
 
237555
- [\u2026 middle of the conversation omitted \u2026]
237717
+ ${chunk}`,
237718
+ timeout: timeoutForInput(chunk.length),
237719
+ maxOutputTokens: COMPACT_SUMMARY_MAX_TOKENS,
237720
+ experimental_telemetry: telemetryFor("review-intent-brief-compact", userId)
237721
+ });
237722
+ const text2 = (result.text ?? "").trim();
237723
+ if (text2.length === 0) return null;
237724
+ return withinLimit(text2, COMPACT_SUMMARY_MAX_CHARS, `slice ${part}/${total} summary`) ? text2 : null;
237725
+ } catch (error48) {
237726
+ if (isSizeRelatedFailure(error48)) throw error48;
237727
+ console.warn(`[ReviewBrief] compaction of slice ${part}/${total} failed:`, error48.message);
237728
+ return null;
237729
+ }
237730
+ }
237731
+ async function compactConversation(model, messages, options = {}) {
237732
+ const sliceChars = options.sliceChars ?? COMPACT_SLICE_BUDGETS[0];
237733
+ const recentChars = Math.max(MIN_RECENT_VERBATIM_CHARS, Math.floor(sliceChars * RECENT_VERBATIM_RATIO));
237734
+ const entries = toEntries(messages);
237735
+ if (entries.length === 0) return null;
237736
+ const { older, recent } = splitRecent(entries, recentChars);
237737
+ if (older.length === 0) return null;
237738
+ const chunks = chunkEntries(older, sliceChars);
237739
+ if (chunks.length > MAX_FOLD_CALLS) {
237740
+ console.warn(
237741
+ `[ReviewBrief] conversation needs ${chunks.length} slices of ${sliceChars} chars (max ${MAX_FOLD_CALLS}) \u2014 falling back to tier 2`
237742
+ );
237743
+ return null;
237744
+ }
237745
+ const settled = await Promise.allSettled(
237746
+ chunks.map((chunk, i) => compactChunk(model, renderAll(chunk), i + 1, chunks.length, options.userId))
237747
+ );
237748
+ const sizeRejection = settled.find((r) => r.status === "rejected" && isSizeRelatedFailure(r.reason));
237749
+ if (sizeRejection) throw sizeRejection.reason;
237750
+ if (settled.some((r) => r.status === "rejected" || r.value === null)) return null;
237751
+ const parts = settled.map(
237752
+ (r, i) => `[Compressed slice ${i + 1} of ${chunks.length}]
237753
+ ${r.value}`
237754
+ );
237755
+ if (recent.length > 0) parts.push(`[Recent turns, verbatim]
237756
+ ${renderAll(recent)}`);
237757
+ return parts.join(SEP);
237758
+ }
237759
+ function buildBriefPrompt(conversation, reversals) {
237760
+ const preamble = reversals ? "Conclusions this conversation reversed. The right-hand side is the current state \u2014 never report the left-hand side as what was decided:\n" + reversals + SEP : "";
237761
+ return `${preamble}Distill this conversation into an intent brief:
237556
237762
 
237557
- ${full.slice(-TAIL_CHARS)}`;
237763
+ ${conversation}`;
237764
+ }
237765
+ function appendCompactionNote(brief, compacted) {
237766
+ return compacted ? `${brief}${SEP}${COMPACTION_NOTE}` : brief;
237767
+ }
237768
+ async function extractReversalsWithModel(model, conversation, options = {}) {
237769
+ if (conversation.trim().length < REVERSAL_MIN_CHARS) return null;
237770
+ try {
237771
+ const result = await generateText({
237772
+ model,
237773
+ system: REVERSAL_SYSTEM_PROMPT,
237774
+ prompt: `Report the reversals in this conversation:
237775
+
237776
+ ${conversation}`,
237777
+ timeout: timeoutForInput(conversation.length),
237778
+ maxOutputTokens: REVERSALS_MAX_TOKENS,
237779
+ experimental_telemetry: telemetryFor("review-intent-brief-reversals", options.userId)
237780
+ });
237781
+ const text2 = (result.text ?? "").trim();
237782
+ if (text2.length === 0 || /^none\b/i.test(text2)) return null;
237783
+ return withinLimit(text2, REVERSALS_MAX_CHARS, "reversal list") ? text2 : null;
237784
+ } catch (error48) {
237785
+ if (!isSizeRelatedFailure(error48)) throw error48;
237786
+ console.warn("[ReviewBrief] reversal pass hit a size limit \u2014 skipping it:", error48.message);
237787
+ return null;
237788
+ }
237558
237789
  }
237559
237790
  async function generateIntentBriefWithModel(model, conversation, options = {}) {
237560
237791
  if (conversation.trim().length === 0) return null;
237561
- const telemetry = options.userId ? {
237562
- isEnabled: true,
237563
- functionId: "review-intent-brief",
237564
- metadata: {
237565
- userId: options.userId,
237566
- tags: ["vibedeckx", "review-intent-brief"]
237567
- }
237568
- } : void 0;
237569
237792
  try {
237570
237793
  const result = await generateText({
237571
237794
  model,
237572
237795
  system: SYSTEM_PROMPT2,
237573
- prompt: `Distill this conversation into an intent brief:
237574
-
237575
- ${conversation}`,
237576
- timeout: AI_TIMEOUT_MS2,
237577
- experimental_telemetry: telemetry
237796
+ prompt: buildBriefPrompt(conversation, options.reversals ?? null),
237797
+ timeout: timeoutForInput(conversation.length),
237798
+ maxOutputTokens: BRIEF_MAX_TOKENS,
237799
+ experimental_telemetry: telemetryFor("review-intent-brief", options.userId)
237578
237800
  });
237579
237801
  const text2 = (result.text ?? "").trim();
237580
- return text2.length > 0 ? capText(text2, BRIEF_MAX_CHARS) : null;
237802
+ if (text2.length === 0) return null;
237803
+ return text2.length <= BRIEF_MAX_CHARS ? text2 : text2.slice(0, BRIEF_MAX_CHARS) + BRIEF_TRUNCATION_NOTE;
237581
237804
  } catch (error48) {
237805
+ if (options.rethrowSizeFailures && isSizeRelatedFailure(error48)) throw error48;
237582
237806
  console.warn("[ReviewBrief] AI generation failed:", error48.message);
237583
237807
  return null;
237584
237808
  }
237585
237809
  }
237810
+ async function distill(model, conversation, userId, rethrowSizeFailures) {
237811
+ const reversals = await extractReversalsWithModel(model, conversation, { userId });
237812
+ return generateIntentBriefWithModel(model, conversation, { userId, reversals, rethrowSizeFailures });
237813
+ }
237586
237814
  async function generateIntentBrief(storage, userId, messages) {
237587
237815
  try {
237588
237816
  if (!await isChatModelConfigured(storage, userId)) return null;
237589
237817
  const conversation = serializeConversationForBrief(messages);
237590
237818
  if (!conversation) return null;
237591
- return await generateIntentBriefWithModel(await resolveFastChatModel(storage, userId), conversation, { userId });
237819
+ const model = await resolveFastChatModel(storage, userId);
237820
+ if (conversation.length <= COMPACT_TRIGGER_CHARS) {
237821
+ try {
237822
+ const brief = await distill(model, conversation, userId, true);
237823
+ return brief === null ? null : appendCompactionNote(brief, false);
237824
+ } catch (error48) {
237825
+ if (!isSizeRelatedFailure(error48)) throw error48;
237826
+ console.warn("[ReviewBrief] full conversation rejected as too large \u2014 compacting and retrying");
237827
+ }
237828
+ }
237829
+ for (const sliceChars of COMPACT_SLICE_BUDGETS) {
237830
+ try {
237831
+ const compacted = await compactConversation(model, messages, { userId, sliceChars });
237832
+ if (compacted === null) return null;
237833
+ const brief = await distill(model, compacted, userId, true);
237834
+ return brief === null ? null : appendCompactionNote(brief, true);
237835
+ } catch (error48) {
237836
+ if (!isSizeRelatedFailure(error48)) throw error48;
237837
+ console.warn(`[ReviewBrief] ${sliceChars}-char slices still too large \u2014 retrying smaller`);
237838
+ }
237839
+ }
237840
+ console.warn("[ReviewBrief] could not compact the conversation small enough \u2014 falling back to tier 2");
237841
+ return null;
237592
237842
  } catch (error48) {
237593
237843
  console.warn("[ReviewBrief] generation failed:", error48.message);
237594
237844
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedeckx/darwin-arm64",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "description": "Vibedeckx platform binaries for macOS ARM64",
5
5
  "os": [
6
6
  "darwin"