@vibedeckx/darwin-arm64 0.2.16 → 0.2.18
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 +478 -99
- 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:
|
|
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 ?
|
|
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
|
|
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(),
|
|
@@ -226865,6 +226901,8 @@ var WATCH_WINDOW_MS = 30 * 60 * 1e3;
|
|
|
226865
226901
|
var WATCH_EXTEND_THROTTLE_MS = 5 * 60 * 1e3;
|
|
226866
226902
|
var MAX_TRACKED_SESSIONS = 5e3;
|
|
226867
226903
|
var SYNC_INTERVAL_MS = 6e4;
|
|
226904
|
+
var COMPLETION_NUDGE_DEBOUNCE_MS = 250;
|
|
226905
|
+
var MAX_CONCURRENT_SERVERS = 4;
|
|
226868
226906
|
var MAX_PAGES_PER_SYNC = 50;
|
|
226869
226907
|
function localWorkflowRunId(remoteServerId, projectId, remoteRunId) {
|
|
226870
226908
|
return `remote-${remoteServerId}-${projectId}-${remoteRunId}`;
|
|
@@ -226877,10 +226915,19 @@ var RemoteNotificationSync = class {
|
|
|
226877
226915
|
notificationService;
|
|
226878
226916
|
reverseConnectManager;
|
|
226879
226917
|
proxy;
|
|
226918
|
+
nudgeDebounceMs;
|
|
226880
226919
|
timer = null;
|
|
226881
226920
|
stopped = false;
|
|
226882
|
-
/**
|
|
226883
|
-
|
|
226921
|
+
/**
|
|
226922
|
+
* Per-server work queues. Two syncs of the SAME server must not interleave
|
|
226923
|
+
* (they share its cursors), but two different servers have nothing in
|
|
226924
|
+
* common — serializing them globally only meant an unreachable worker's
|
|
226925
|
+
* retry budget delayed every healthy worker's import behind it.
|
|
226926
|
+
*/
|
|
226927
|
+
serverChains = /* @__PURE__ */ new Map();
|
|
226928
|
+
/** Local session ids whose completion is waiting for the debounce to fire. */
|
|
226929
|
+
nudgeSessions = /* @__PURE__ */ new Set();
|
|
226930
|
+
nudgeTimer = null;
|
|
226884
226931
|
/**
|
|
226885
226932
|
* Remote sessions this front currently believes are RUNNING. They are polled
|
|
226886
226933
|
* regardless of their persisted watch window: an agent turn can easily run
|
|
@@ -226896,6 +226943,7 @@ var RemoteNotificationSync = class {
|
|
|
226896
226943
|
this.notificationService = deps.notificationService;
|
|
226897
226944
|
this.reverseConnectManager = deps.reverseConnectManager;
|
|
226898
226945
|
this.proxy = deps.proxy ?? proxyToRemoteAuto;
|
|
226946
|
+
this.nudgeDebounceMs = deps.nudgeDebounceMs ?? COMPLETION_NUDGE_DEBOUNCE_MS;
|
|
226899
226947
|
}
|
|
226900
226948
|
start() {
|
|
226901
226949
|
if (this.timer || this.stopped) return;
|
|
@@ -226908,6 +226956,9 @@ var RemoteNotificationSync = class {
|
|
|
226908
226956
|
this.stopped = true;
|
|
226909
226957
|
if (this.timer) clearInterval(this.timer);
|
|
226910
226958
|
this.timer = null;
|
|
226959
|
+
if (this.nudgeTimer) clearTimeout(this.nudgeTimer);
|
|
226960
|
+
this.nudgeTimer = null;
|
|
226961
|
+
this.nudgeSessions.clear();
|
|
226911
226962
|
this.activeRemoteSessions.clear();
|
|
226912
226963
|
this.lastWatchExtend.clear();
|
|
226913
226964
|
}
|
|
@@ -226931,6 +226982,7 @@ var RemoteNotificationSync = class {
|
|
|
226931
226982
|
} else if (event.type === "session:taskCompleted" || event.type === "session:finished") {
|
|
226932
226983
|
this.activeRemoteSessions.delete(sessionId);
|
|
226933
226984
|
}
|
|
226985
|
+
if (isTurnTerminalEvent(event)) this.scheduleCompletionSync(sessionId);
|
|
226934
226986
|
const now2 = Date.now();
|
|
226935
226987
|
const last = this.lastWatchExtend.get(sessionId) ?? 0;
|
|
226936
226988
|
if (now2 - last < WATCH_EXTEND_THROTTLE_MS) return;
|
|
@@ -226944,10 +226996,65 @@ var RemoteNotificationSync = class {
|
|
|
226944
226996
|
if (now2 - at > WATCH_WINDOW_MS) this.lastWatchExtend.delete(sessionId);
|
|
226945
226997
|
}
|
|
226946
226998
|
}
|
|
226947
|
-
/**
|
|
226999
|
+
/**
|
|
227000
|
+
* Fire-and-forget sweep. No longer a global queue: the per-server chains
|
|
227001
|
+
* inside syncAll/syncServer own the mutual exclusion that actually matters
|
|
227002
|
+
* (one server's cursors), so callers here don't wait on unrelated servers.
|
|
227003
|
+
*/
|
|
226948
227004
|
enqueue(work) {
|
|
226949
227005
|
if (this.stopped) return;
|
|
226950
|
-
|
|
227006
|
+
void work().catch((err) => console.warn("[RemoteNotifications] sync failed:", err));
|
|
227007
|
+
}
|
|
227008
|
+
/**
|
|
227009
|
+
* Queue `work` behind whatever is already running for this server, and
|
|
227010
|
+
* nothing else. Errors are contained so one server's failure can't reject
|
|
227011
|
+
* an unrelated caller awaiting the same chain.
|
|
227012
|
+
*/
|
|
227013
|
+
runOnServer(remoteServerId, work) {
|
|
227014
|
+
const previous = this.serverChains.get(remoteServerId) ?? Promise.resolve();
|
|
227015
|
+
const next = previous.then(work).catch((err) => console.warn(`[RemoteNotifications] sync for ${remoteServerId} failed:`, err)).finally(() => {
|
|
227016
|
+
if (this.serverChains.get(remoteServerId) === next) this.serverChains.delete(remoteServerId);
|
|
227017
|
+
});
|
|
227018
|
+
this.serverChains.set(remoteServerId, next);
|
|
227019
|
+
return next;
|
|
227020
|
+
}
|
|
227021
|
+
/**
|
|
227022
|
+
* Debounced, per-server pull for sessions whose turn just ended.
|
|
227023
|
+
*
|
|
227024
|
+
* Coalescing state is intentionally separate from `lastWatchExtend`: that
|
|
227025
|
+
* map throttles DB writes on a 5-minute scale, which would swallow nearly
|
|
227026
|
+
* every completion.
|
|
227027
|
+
*/
|
|
227028
|
+
scheduleCompletionSync(localSessionId) {
|
|
227029
|
+
if (this.stopped) return;
|
|
227030
|
+
this.nudgeSessions.add(localSessionId);
|
|
227031
|
+
if (this.nudgeTimer) return;
|
|
227032
|
+
this.nudgeTimer = setTimeout(() => {
|
|
227033
|
+
this.nudgeTimer = null;
|
|
227034
|
+
const batch = [...this.nudgeSessions];
|
|
227035
|
+
this.nudgeSessions.clear();
|
|
227036
|
+
void this.syncSessions(batch).catch(
|
|
227037
|
+
(err) => console.warn("[RemoteNotifications] completion sync failed:", err)
|
|
227038
|
+
);
|
|
227039
|
+
}, this.nudgeDebounceMs);
|
|
227040
|
+
this.nudgeTimer.unref?.();
|
|
227041
|
+
}
|
|
227042
|
+
/**
|
|
227043
|
+
* Pull exactly the mappings named, grouped per server. Deliberately does NOT
|
|
227044
|
+
* go through `candidates()`: an explicit completion signal outranks the
|
|
227045
|
+
* watch window, which may well have lapsed during a long turn.
|
|
227046
|
+
*/
|
|
227047
|
+
async syncSessions(localSessionIds) {
|
|
227048
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
227049
|
+
for (const localSessionId of localSessionIds) {
|
|
227050
|
+
const mapping = await this.storage.remoteSessionMappings.getByLocal(localSessionId).catch(() => void 0);
|
|
227051
|
+
if (!mapping) continue;
|
|
227052
|
+
const group2 = byServer.get(mapping.remote_server_id);
|
|
227053
|
+
if (group2) group2.push(mapping);
|
|
227054
|
+
else byServer.set(mapping.remote_server_id, [mapping]);
|
|
227055
|
+
}
|
|
227056
|
+
if (byServer.size === 0) return;
|
|
227057
|
+
await this.dispatchByServer([...byServer]);
|
|
226951
227058
|
}
|
|
226952
227059
|
/** Extend a mapping's periodic-poll window (create / send / workflow start / live activity). */
|
|
226953
227060
|
async extendWatch(localSessionId) {
|
|
@@ -226997,17 +227104,33 @@ var RemoteNotificationSync = class {
|
|
|
226997
227104
|
);
|
|
226998
227105
|
return true;
|
|
226999
227106
|
}
|
|
227000
|
-
/** Sweep every mapped remote server. */
|
|
227107
|
+
/** Sweep every mapped remote server, up to MAX_CONCURRENT_SERVERS at a time. */
|
|
227001
227108
|
async syncAll(opts) {
|
|
227002
227109
|
const mappings = await this.candidates(opts);
|
|
227003
|
-
|
|
227004
|
-
await this.syncMappings(serverId, group2);
|
|
227005
|
-
}
|
|
227110
|
+
await this.dispatchByServer([...groupByServer(mappings)]);
|
|
227006
227111
|
}
|
|
227007
227112
|
/** Sweep one server — used when a reverse connection comes online. */
|
|
227008
227113
|
async syncServer(remoteServerId, opts) {
|
|
227009
227114
|
const mappings = (await this.candidates(opts)).filter((m2) => m2.remote_server_id === remoteServerId);
|
|
227010
|
-
if (mappings.length > 0) await this.
|
|
227115
|
+
if (mappings.length > 0) await this.dispatchByServer([[remoteServerId, mappings]]);
|
|
227116
|
+
}
|
|
227117
|
+
/**
|
|
227118
|
+
* Run one group per server, bounded. Each group still queues on its own
|
|
227119
|
+
* server chain, so this composes with a nudge that arrives mid-sweep: same
|
|
227120
|
+
* server → it waits its turn; different server → it runs immediately.
|
|
227121
|
+
*/
|
|
227122
|
+
async dispatchByServer(groups) {
|
|
227123
|
+
let next = 0;
|
|
227124
|
+
const workers = Array.from(
|
|
227125
|
+
{ length: Math.min(MAX_CONCURRENT_SERVERS, groups.length) },
|
|
227126
|
+
async () => {
|
|
227127
|
+
for (let i = next++; i < groups.length; i = next++) {
|
|
227128
|
+
const [serverId, group2] = groups[i];
|
|
227129
|
+
await this.runOnServer(serverId, () => this.syncMappings(serverId, group2));
|
|
227130
|
+
}
|
|
227131
|
+
}
|
|
227132
|
+
);
|
|
227133
|
+
await Promise.all(workers);
|
|
227011
227134
|
}
|
|
227012
227135
|
/**
|
|
227013
227136
|
* Mappings this sweep should poll: the persisted watch set, plus any session
|
|
@@ -227154,6 +227277,10 @@ var RemoteNotificationSync = class {
|
|
|
227154
227277
|
};
|
|
227155
227278
|
}
|
|
227156
227279
|
};
|
|
227280
|
+
function isTurnTerminalEvent(event) {
|
|
227281
|
+
if (event.type === "session:taskCompleted" || event.type === "session:finished") return true;
|
|
227282
|
+
return event.type === "session:status" && (event.status === "stopped" || event.status === "error");
|
|
227283
|
+
}
|
|
227157
227284
|
function groupByServer(mappings) {
|
|
227158
227285
|
const groups = /* @__PURE__ */ new Map();
|
|
227159
227286
|
for (const mapping of mappings) {
|
|
@@ -227166,7 +227293,7 @@ function groupByServer(mappings) {
|
|
|
227166
227293
|
|
|
227167
227294
|
// src/remote-agent-sessions.ts
|
|
227168
227295
|
async function createRemoteAgentSession(deps, params) {
|
|
227169
|
-
const { projectId, agentMode, remoteConfig, branch, permissionMode, agentType, force, userId } = params;
|
|
227296
|
+
const { projectId, agentMode, remoteConfig, branch, permissionMode, agentType, model, force, userId } = params;
|
|
227170
227297
|
const remoteSessionId = randomUUID3();
|
|
227171
227298
|
const localSessionId = `remote-${agentMode}-${projectId}-${remoteSessionId}`;
|
|
227172
227299
|
const crossRemoteMcp = await mintCrossRemoteMcpConfig(
|
|
@@ -227188,7 +227315,7 @@ async function createRemoteAgentSession(deps, params) {
|
|
|
227188
227315
|
remoteConfig.server_api_key || "",
|
|
227189
227316
|
"POST",
|
|
227190
227317
|
`/api/path/agent-sessions/new`,
|
|
227191
|
-
{ path: remoteConfig.remote_path, branch, permissionMode, agentType, force, sessionId: remoteSessionId, crossRemoteMcp },
|
|
227318
|
+
{ path: remoteConfig.remote_path, branch, permissionMode, agentType, force, sessionId: remoteSessionId, crossRemoteMcp, model },
|
|
227192
227319
|
{ reverseConnectManager: deps.reverseConnectManager ?? void 0 }
|
|
227193
227320
|
);
|
|
227194
227321
|
if (!result.ok) {
|
|
@@ -229914,12 +230041,18 @@ function selfReportSection(report) {
|
|
|
229914
230041
|
if (!report) return null;
|
|
229915
230042
|
return [
|
|
229916
230043
|
"\n## Author's self-report (unverified)",
|
|
229917
|
-
"The implementing agent described its own work as follows. Treat every claim as unverified \u2014
|
|
230044
|
+
"The implementing agent described its own work as follows. Treat every claim as unverified \u2014 verify first the claims that bear on whether the work achieves its goal, and look for problems the self-report does not mention.",
|
|
229918
230045
|
"<author-self-report>",
|
|
229919
230046
|
report,
|
|
229920
230047
|
"</author-self-report>"
|
|
229921
230048
|
].join("\n");
|
|
229922
230049
|
}
|
|
230050
|
+
var VERDICT_INSTRUCTIONS = [
|
|
230051
|
+
"\nEnd your final message with:",
|
|
230052
|
+
"1. Verdict \u2014 exactly one of: ship / needs-changes / cannot-verify. Use cannot-verify when you could not gather enough evidence to judge, rather than guessing.",
|
|
230053
|
+
"2. Blocking findings \u2014 what must change before shipping, each specific and actionable (say explicitly when there are none).",
|
|
230054
|
+
"3. Non-blocking notes \u2014 style and polish, briefly, clearly separated from the blocking list."
|
|
230055
|
+
];
|
|
229923
230056
|
function buildReviewerPrompt(opts) {
|
|
229924
230057
|
const intent = opts.originalIntent !== opts.taskContext ? opts.originalIntent : null;
|
|
229925
230058
|
const brief = opts.intentBrief || null;
|
|
@@ -229958,7 +230091,12 @@ Confine your review to these files and changes. Other uncommitted or pre-existin
|
|
|
229958
230091
|
"- Inspect the actual workspace state yourself: read the relevant files, run `git diff`, `git status` and `git log`.",
|
|
229959
230092
|
reviewTargetPromptLine(opts.target),
|
|
229960
230093
|
noDiffWithAnalysis ? "- Judge correctness and completeness against the task. For this analysis/plan turn the work under review is the reasoning and the proposal, not code quality of a diff. Be specific: reference files and lines." : "- Judge correctness, completeness against the task, and code quality. Be specific: reference files and lines.",
|
|
229961
|
-
|
|
230094
|
+
// These two only make sense against a distilled brief: tier 2 has no
|
|
230095
|
+
// [settled]/[tentative] marks and no stated scope, and implying it does
|
|
230096
|
+
// would suppress findings on the strength of data that doesn't exist.
|
|
230097
|
+
brief ? "- Where the brief marks a decision, non-goal, or accepted limitation as [settled], do not re-raise the choice itself as a finding; DO report concrete consequences it causes \u2014 failure of the core goal, or a correctness, security, or data loss problem. Items marked [tentative] (or unmarked) get normal review. A violated hard constraint is always blocking." : null,
|
|
230098
|
+
brief ? "- Do not propose enhancements beyond the brief's stated scope \u2014 scope expansion is a product decision, not a review finding." : null,
|
|
230099
|
+
...VERDICT_INSTRUCTIONS,
|
|
229962
230100
|
brief ? opts.authorSelfReport ? "\n(review context: distilled intent brief + author self-report + live workspace)" : "\n(review context: distilled intent brief + live workspace)" : hasExcerpt ? "\n(review context: deterministic excerpt of the source conversation + live workspace)" : "\n(review context: live workspace only \u2014 the source conversation was unavailable)"
|
|
229963
230101
|
].filter((l) => l !== null).join("\n");
|
|
229964
230102
|
}
|
|
@@ -229982,7 +230120,7 @@ ${opts.reviewFocus}` : null,
|
|
|
229982
230120
|
"- Check for regressions and remaining correctness or test gaps.",
|
|
229983
230121
|
"- Do NOT modify files \u2014 remain in read-only review mode.",
|
|
229984
230122
|
reviewTargetPromptLine(opts.target),
|
|
229985
|
-
|
|
230123
|
+
...VERDICT_INSTRUCTIONS
|
|
229986
230124
|
].filter((line) => line !== null).join("\n");
|
|
229987
230125
|
}
|
|
229988
230126
|
function buildFeedbackMessage(feedback) {
|
|
@@ -235587,6 +235725,14 @@ function resolveUserId(authResult) {
|
|
|
235587
235725
|
|
|
235588
235726
|
// src/routes/agent-session-routes.ts
|
|
235589
235727
|
import { randomUUID as randomUUID15 } from "crypto";
|
|
235728
|
+
|
|
235729
|
+
// src/protocol/model-suggestions.ts
|
|
235730
|
+
var MODEL_SUGGESTIONS = {
|
|
235731
|
+
"claude-code": ["opus", "sonnet", "haiku", "fable"],
|
|
235732
|
+
codex: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"]
|
|
235733
|
+
};
|
|
235734
|
+
|
|
235735
|
+
// src/routes/agent-session-routes.ts
|
|
235590
235736
|
async function resolveProjectPath(projectId, storage) {
|
|
235591
235737
|
if (projectId.startsWith("path:")) {
|
|
235592
235738
|
return projectId.slice(5);
|
|
@@ -235650,6 +235796,7 @@ var routes12 = async (fastify2) => {
|
|
|
235650
235796
|
status: session?.status || "stopped",
|
|
235651
235797
|
permissionMode: session?.permissionMode || "edit",
|
|
235652
235798
|
agentType: session?.agentType || "claude-code",
|
|
235799
|
+
model: session?.model ?? null,
|
|
235653
235800
|
title: dbRow?.title ?? null
|
|
235654
235801
|
},
|
|
235655
235802
|
messages
|
|
@@ -235660,15 +235807,23 @@ var routes12 = async (fastify2) => {
|
|
|
235660
235807
|
fastify2.agentSessionManager.emitSessionTitle(projectId, branch, sessionId, title);
|
|
235661
235808
|
}
|
|
235662
235809
|
fastify2.get("/api/agent-providers", async (_req, reply) => {
|
|
235663
|
-
const providers2 = getAllProviders().map((provider) =>
|
|
235664
|
-
type
|
|
235665
|
-
|
|
235666
|
-
|
|
235667
|
-
|
|
235810
|
+
const providers2 = getAllProviders().map((provider) => {
|
|
235811
|
+
const type = provider.getAgentType();
|
|
235812
|
+
return {
|
|
235813
|
+
type,
|
|
235814
|
+
displayName: provider.getDisplayName(),
|
|
235815
|
+
available: provider.isAvailable?.() ?? provider.detectBinary() !== null,
|
|
235816
|
+
// Suggestions only. The picker also accepts free text, and nothing
|
|
235817
|
+
// validates against this list — the session's real capabilities depend
|
|
235818
|
+
// on the CLI version and account tier of whichever machine spawns it,
|
|
235819
|
+
// which this server cannot know.
|
|
235820
|
+
models: MODEL_SUGGESTIONS[type] ?? []
|
|
235821
|
+
};
|
|
235822
|
+
});
|
|
235668
235823
|
return reply.code(200).send({ providers: providers2 });
|
|
235669
235824
|
});
|
|
235670
235825
|
fastify2.post("/api/path/agent-sessions", async (req, reply) => {
|
|
235671
|
-
const { path: projectPath, branch, permissionMode, agentType, force } = req.body;
|
|
235826
|
+
const { path: projectPath, branch, permissionMode, agentType, force, model } = req.body;
|
|
235672
235827
|
if (!projectPath) {
|
|
235673
235828
|
return reply.code(400).send({ error: "Path is required" });
|
|
235674
235829
|
}
|
|
@@ -235716,6 +235871,7 @@ var routes12 = async (fastify2) => {
|
|
|
235716
235871
|
status: effectiveStatus,
|
|
235717
235872
|
permissionMode: session?.permissionMode || "edit",
|
|
235718
235873
|
agentType: session?.agentType || "claude-code",
|
|
235874
|
+
model: session?.model ?? null,
|
|
235719
235875
|
processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
|
|
235720
235876
|
},
|
|
235721
235877
|
messages
|
|
@@ -235760,7 +235916,7 @@ var routes12 = async (fastify2) => {
|
|
|
235760
235916
|
}
|
|
235761
235917
|
);
|
|
235762
235918
|
fastify2.post("/api/path/agent-sessions/new", async (req, reply) => {
|
|
235763
|
-
const { path: projectPath, branch, permissionMode, agentType, force, sessionId, crossRemoteMcp } = req.body;
|
|
235919
|
+
const { path: projectPath, branch, permissionMode, agentType, force, sessionId, crossRemoteMcp, model } = req.body;
|
|
235764
235920
|
if (!projectPath) {
|
|
235765
235921
|
return reply.code(400).send({ error: "Path is required" });
|
|
235766
235922
|
}
|
|
@@ -235788,7 +235944,7 @@ var routes12 = async (fastify2) => {
|
|
|
235788
235944
|
agentType || "claude-code",
|
|
235789
235945
|
false,
|
|
235790
235946
|
force === true,
|
|
235791
|
-
{ sessionId, crossRemoteMcp }
|
|
235947
|
+
{ sessionId, crossRemoteMcp, model }
|
|
235792
235948
|
);
|
|
235793
235949
|
const session = fastify2.agentSessionManager.getSession(createdSessionId);
|
|
235794
235950
|
return reply.code(200).send({
|
|
@@ -235799,6 +235955,7 @@ var routes12 = async (fastify2) => {
|
|
|
235799
235955
|
status: session?.status || "running",
|
|
235800
235956
|
permissionMode: session?.permissionMode || "edit",
|
|
235801
235957
|
agentType: session?.agentType || "claude-code",
|
|
235958
|
+
model: session?.model ?? null,
|
|
235802
235959
|
processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(session.id) : false
|
|
235803
235960
|
},
|
|
235804
235961
|
messages: []
|
|
@@ -235901,7 +236058,7 @@ var routes12 = async (fastify2) => {
|
|
|
235901
236058
|
if (!project) {
|
|
235902
236059
|
return reply.code(404).send({ error: "Project not found" });
|
|
235903
236060
|
}
|
|
235904
|
-
const { branch, permissionMode, agentType } = req.body;
|
|
236061
|
+
const { branch, permissionMode, agentType, model } = req.body;
|
|
235905
236062
|
let agentMode = project.agent_mode;
|
|
235906
236063
|
let useRemoteAgent = agentMode !== "local";
|
|
235907
236064
|
if (useRemoteAgent && agentMode === "remote") {
|
|
@@ -235936,7 +236093,7 @@ var routes12 = async (fastify2) => {
|
|
|
235936
236093
|
remoteConfig.server_api_key || "",
|
|
235937
236094
|
"POST",
|
|
235938
236095
|
`/api/path/agent-sessions`,
|
|
235939
|
-
{ path: remoteConfig.remote_path, branch, permissionMode, agentType }
|
|
236096
|
+
{ path: remoteConfig.remote_path, branch, permissionMode, agentType, model }
|
|
235940
236097
|
);
|
|
235941
236098
|
console.log(`[API] Remote proxy result: ok=${result.ok}, status=${result.status}, data=${JSON.stringify(result.data).substring(0, 500)}`);
|
|
235942
236099
|
if (result.ok) {
|
|
@@ -236014,6 +236171,7 @@ var routes12 = async (fastify2) => {
|
|
|
236014
236171
|
status: effectiveStatus,
|
|
236015
236172
|
permissionMode: session?.permissionMode || "edit",
|
|
236016
236173
|
agentType: session?.agentType || "claude-code",
|
|
236174
|
+
model: session?.model ?? null,
|
|
236017
236175
|
processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
|
|
236018
236176
|
},
|
|
236019
236177
|
messages
|
|
@@ -236030,7 +236188,7 @@ var routes12 = async (fastify2) => {
|
|
|
236030
236188
|
if (!project) {
|
|
236031
236189
|
return reply.code(404).send({ error: "Project not found" });
|
|
236032
236190
|
}
|
|
236033
|
-
const { branch, permissionMode, agentType, force } = req.body;
|
|
236191
|
+
const { branch, permissionMode, agentType, force, model } = req.body;
|
|
236034
236192
|
const agentMode = project.agent_mode;
|
|
236035
236193
|
const useRemoteAgent = agentMode !== "local";
|
|
236036
236194
|
if (useRemoteAgent) {
|
|
@@ -236056,7 +236214,8 @@ var routes12 = async (fastify2) => {
|
|
|
236056
236214
|
permissionMode: permissionMode || "edit",
|
|
236057
236215
|
agentType,
|
|
236058
236216
|
force: force === true,
|
|
236059
|
-
userId
|
|
236217
|
+
userId,
|
|
236218
|
+
model
|
|
236060
236219
|
}
|
|
236061
236220
|
);
|
|
236062
236221
|
if (created.ok) {
|
|
@@ -236089,7 +236248,7 @@ var routes12 = async (fastify2) => {
|
|
|
236089
236248
|
agentType || "claude-code",
|
|
236090
236249
|
false,
|
|
236091
236250
|
force === true,
|
|
236092
|
-
{ sessionId: preSessionId, crossRemoteMcp }
|
|
236251
|
+
{ sessionId: preSessionId, crossRemoteMcp, model }
|
|
236093
236252
|
);
|
|
236094
236253
|
const session = fastify2.agentSessionManager.getSession(sessionId);
|
|
236095
236254
|
return reply.code(200).send({
|
|
@@ -236100,6 +236259,7 @@ var routes12 = async (fastify2) => {
|
|
|
236100
236259
|
status: session?.status || "running",
|
|
236101
236260
|
permissionMode: session?.permissionMode || "edit",
|
|
236102
236261
|
agentType: session?.agentType || "claude-code",
|
|
236262
|
+
model: session?.model ?? null,
|
|
236103
236263
|
processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
|
|
236104
236264
|
},
|
|
236105
236265
|
messages: []
|
|
@@ -236156,6 +236316,7 @@ var routes12 = async (fastify2) => {
|
|
|
236156
236316
|
status: session.status,
|
|
236157
236317
|
permissionMode: session.permissionMode,
|
|
236158
236318
|
agentType: session.agentType || "claude-code",
|
|
236319
|
+
model: session.model ?? null,
|
|
236159
236320
|
processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id)
|
|
236160
236321
|
},
|
|
236161
236322
|
messages
|
|
@@ -237518,77 +237679,295 @@ var command_routes_default = (0, import_fastify_plugin19.default)(routes18, { na
|
|
|
237518
237679
|
var import_fastify_plugin20 = __toESM(require_plugin2(), 1);
|
|
237519
237680
|
|
|
237520
237681
|
// src/utils/review-brief.ts
|
|
237521
|
-
var
|
|
237522
|
-
var
|
|
237523
|
-
var
|
|
237524
|
-
var
|
|
237525
|
-
var
|
|
237682
|
+
var BASE_TIMEOUT_MS = 15e3;
|
|
237683
|
+
var JUDGMENT_TIMEOUT_MS = 9e4;
|
|
237684
|
+
var TIMEOUT_MS_PER_1K_CHARS = 120;
|
|
237685
|
+
var MAX_TIMEOUT_MS = 6e4;
|
|
237686
|
+
var PER_MESSAGE_MAX_CHARS = 6e3;
|
|
237687
|
+
var COMPACT_TRIGGER_CHARS = 12e4;
|
|
237688
|
+
var COMPACT_SLICE_BUDGETS = [96e3, 24e3, 6e3];
|
|
237689
|
+
var RECENT_VERBATIM_RATIO = 0.3;
|
|
237690
|
+
var MIN_RECENT_VERBATIM_CHARS = 2e3;
|
|
237691
|
+
var MAX_FOLD_CALLS = 8;
|
|
237692
|
+
var COMPACT_SUMMARY_MAX_CHARS = 6e3;
|
|
237693
|
+
var COMPACT_SUMMARY_MAX_TOKENS = 3e3;
|
|
237694
|
+
var REVERSALS_MAX_CHARS = 2e3;
|
|
237695
|
+
var REVERSALS_MAX_TOKENS = 2e3;
|
|
237526
237696
|
var BRIEF_MAX_CHARS = 4e3;
|
|
237697
|
+
var BRIEF_MAX_TOKENS = 3e3;
|
|
237698
|
+
var REVERSAL_MIN_CHARS = 6e3;
|
|
237699
|
+
var SEP = "\n\n";
|
|
237700
|
+
var COMPACTION_NOTE = "_Note: the source conversation was compressed before distillation \u2014 older turns were summarized rather than read verbatim._";
|
|
237701
|
+
var BRIEF_TRUNCATION_NOTE = "\n\n_[brief truncated at the length limit \u2014 treat it as incomplete]_";
|
|
237527
237702
|
var SYSTEM_PROMPT2 = [
|
|
237528
237703
|
"You distill a coding-agent conversation into an intent brief for an independent code reviewer.",
|
|
237529
237704
|
"The reviewer will NOT see the conversation \u2014 only your brief plus the actual code, so capture what the code alone cannot show:",
|
|
237705
|
+
"0. Dominant question \u2014 1 or 2 sentences: the single question the reviewer must answer to judge this work (what must actually work, toward what goal). Derive it from the conversation's own emphasis \u2014 what the user repeated, finally decided, or reversed course over \u2014 never from your own engineering judgment. If the conversation does not clearly support one, write 'unclear' instead of inventing one.",
|
|
237530
237706
|
"1. The original request and its goal.",
|
|
237531
|
-
"2.
|
|
237532
|
-
"3.
|
|
237533
|
-
"
|
|
237534
|
-
"
|
|
237707
|
+
"2. Hard constraints \u2014 requirements whose violation the user would treat as failure regardless of everything else (security, data loss, an explicit compatibility or behaviour promise). Only list ones the user actually stated; an empty section is fine.",
|
|
237708
|
+
"3. 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.",
|
|
237709
|
+
"4. Non-goals, trade-offs and limitations that were acknowledged and accepted.",
|
|
237710
|
+
"5. 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.",
|
|
237711
|
+
"Tag every item under headings 3 and 4 with exactly [settled] or [tentative] \u2014 the bare tag; keep any justification to a few words or omit it, always after the tag, never inside the brackets. Settled ONLY when the user explicitly confirmed it, instructed it, or reversed course to it; an agent proposal the user never answered is tentative \u2014 silence is never settled.",
|
|
237712
|
+
"Within each heading, order items by how much they matter to the dominant question, not by when they were said.",
|
|
237713
|
+
"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.",
|
|
237714
|
+
"The input may arrive as numbered compressed slices followed by verbatim recent turns. Slice numbers are chronological: a later slice overrules an earlier one.",
|
|
237715
|
+
"Decisions the agent made that the user did not contradict are worth reporting, but user silence only ever yields tentative status.",
|
|
237716
|
+
"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.",
|
|
237717
|
+
"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.",
|
|
237718
|
+
"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
237719
|
].join("\n");
|
|
237536
|
-
|
|
237537
|
-
|
|
237720
|
+
var REVERSAL_SYSTEM_PROMPT = [
|
|
237721
|
+
"You read a coding-agent conversation and report ONLY the conclusions that were later reversed, dropped, or replaced.",
|
|
237722
|
+
"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.",
|
|
237723
|
+
"A reversal counts whether the user or the agent made it. Ignore rewording that leaves the decision intact.",
|
|
237724
|
+
"Output one line per reversal: SUPERSEDED: <what was concluded earlier> -> <what replaced it> (<why>)",
|
|
237725
|
+
"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.",
|
|
237726
|
+
"If nothing was reversed, reply with exactly: NONE"
|
|
237727
|
+
].join("\n");
|
|
237728
|
+
var COMPACT_SYSTEM_PROMPT = [
|
|
237729
|
+
"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.",
|
|
237730
|
+
"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.",
|
|
237731
|
+
"Preserve, in this order of priority:",
|
|
237732
|
+
"1. Reversals and corrections \u2014 any conclusion that was dropped, replaced, or corrected. Quote the deciding sentence verbatim.",
|
|
237733
|
+
"2. Constraints and decisions the user stated, and approaches that were rejected, with the reason. Preserve who made each decision \u2014 an explicit user confirmation must stay distinguishable from an agent proposal the user never answered; quote the user's deciding sentence when there is one.",
|
|
237734
|
+
"3. Behaviour established by actually running something: observed output, exit codes, error text, which stream an error arrived on.",
|
|
237735
|
+
"4. Trade-offs that were acknowledged and accepted.",
|
|
237736
|
+
"Drop: exploration narrative, restated background, tool mechanics, and anything a reviewer could recover by reading the code.",
|
|
237737
|
+
"Stay under 800 words. An over-long slice is discarded whole rather than cut short, so drop lower-priority material instead of running over.",
|
|
237738
|
+
"Keep the conversation's own language. Reply with the compressed slice only, no preamble."
|
|
237739
|
+
].join("\n");
|
|
237740
|
+
function timeoutForInput(chars) {
|
|
237741
|
+
return Math.min(MAX_TIMEOUT_MS, BASE_TIMEOUT_MS + Math.floor(chars / 1e3) * TIMEOUT_MS_PER_1K_CHARS);
|
|
237742
|
+
}
|
|
237743
|
+
function isSizeRelatedFailure(error48) {
|
|
237744
|
+
const err = error48;
|
|
237745
|
+
if (err?.name === "AbortError" || err?.name === "TimeoutError") return true;
|
|
237746
|
+
const message = err?.message ?? "";
|
|
237747
|
+
return /context[ _-]?length|maximum context|too many tokens|token limit|prompt is too long|request too large|timed? ?out/i.test(
|
|
237748
|
+
message
|
|
237749
|
+
);
|
|
237538
237750
|
}
|
|
237539
|
-
function
|
|
237540
|
-
|
|
237751
|
+
function withinLimit(text2, max, label) {
|
|
237752
|
+
if (text2.length <= max) return true;
|
|
237753
|
+
console.warn(`[ReviewBrief] ${label} ran to ${text2.length} chars (limit ${max}) \u2014 discarding rather than truncating`);
|
|
237754
|
+
return false;
|
|
237755
|
+
}
|
|
237756
|
+
function capMessage(text2) {
|
|
237757
|
+
return text2.length > PER_MESSAGE_MAX_CHARS ? text2.slice(0, PER_MESSAGE_MAX_CHARS) + "\u2026" : text2;
|
|
237758
|
+
}
|
|
237759
|
+
function toEntries(messages) {
|
|
237760
|
+
const entries = [];
|
|
237541
237761
|
for (const msg of messages) {
|
|
237542
237762
|
if (!msg) continue;
|
|
237543
237763
|
if (msg.type === "user" && !msg.event) {
|
|
237544
237764
|
const text2 = extractUserText(msg.content).trim();
|
|
237545
|
-
if (text2)
|
|
237765
|
+
if (text2) entries.push({ role: "User", text: capMessage(text2) });
|
|
237546
237766
|
} else if (msg.type === "assistant" && typeof msg.content === "string") {
|
|
237547
237767
|
const text2 = msg.content.trim();
|
|
237548
|
-
if (text2)
|
|
237768
|
+
if (text2) entries.push({ role: "Agent", text: capMessage(text2) });
|
|
237549
237769
|
}
|
|
237550
237770
|
}
|
|
237551
|
-
|
|
237552
|
-
if (full.length <= TOTAL_INPUT_MAX_CHARS) return full;
|
|
237553
|
-
return `${full.slice(0, HEAD_CHARS)}
|
|
237554
|
-
|
|
237555
|
-
[\u2026 middle of the conversation omitted \u2026]
|
|
237556
|
-
|
|
237557
|
-
${full.slice(-TAIL_CHARS)}`;
|
|
237771
|
+
return entries;
|
|
237558
237772
|
}
|
|
237559
|
-
|
|
237560
|
-
|
|
237561
|
-
|
|
237562
|
-
|
|
237563
|
-
|
|
237564
|
-
|
|
237565
|
-
|
|
237566
|
-
|
|
237773
|
+
function render(entry) {
|
|
237774
|
+
return `${entry.role}: ${entry.text}`;
|
|
237775
|
+
}
|
|
237776
|
+
function renderAll(entries) {
|
|
237777
|
+
return entries.map(render).join(SEP);
|
|
237778
|
+
}
|
|
237779
|
+
function serializeConversationForBrief(messages) {
|
|
237780
|
+
return renderAll(toEntries(messages));
|
|
237781
|
+
}
|
|
237782
|
+
function splitRecent(entries, maxChars) {
|
|
237783
|
+
let size = 0;
|
|
237784
|
+
let i = entries.length;
|
|
237785
|
+
while (i > 0) {
|
|
237786
|
+
const len = render(entries[i - 1]).length + SEP.length;
|
|
237787
|
+
if (size + len > maxChars) break;
|
|
237788
|
+
size += len;
|
|
237789
|
+
i--;
|
|
237790
|
+
}
|
|
237791
|
+
return { older: entries.slice(0, i), recent: entries.slice(i) };
|
|
237792
|
+
}
|
|
237793
|
+
function chunkEntries(entries, maxChars) {
|
|
237794
|
+
const chunks = [];
|
|
237795
|
+
let current = [];
|
|
237796
|
+
let size = 0;
|
|
237797
|
+
for (const entry of entries) {
|
|
237798
|
+
const len = render(entry).length + SEP.length;
|
|
237799
|
+
if (current.length > 0 && size + len > maxChars) {
|
|
237800
|
+
chunks.push(current);
|
|
237801
|
+
current = [];
|
|
237802
|
+
size = 0;
|
|
237567
237803
|
}
|
|
237804
|
+
current.push(entry);
|
|
237805
|
+
size += len;
|
|
237806
|
+
}
|
|
237807
|
+
if (current.length > 0) chunks.push(current);
|
|
237808
|
+
return chunks;
|
|
237809
|
+
}
|
|
237810
|
+
function telemetryFor(functionId, userId) {
|
|
237811
|
+
return userId ? {
|
|
237812
|
+
isEnabled: true,
|
|
237813
|
+
functionId,
|
|
237814
|
+
metadata: { userId, tags: ["vibedeckx", functionId] }
|
|
237568
237815
|
} : void 0;
|
|
237816
|
+
}
|
|
237817
|
+
async function compactChunk(model, chunk, part, total, userId) {
|
|
237569
237818
|
try {
|
|
237570
237819
|
const result = await generateText({
|
|
237571
237820
|
model,
|
|
237572
|
-
system:
|
|
237573
|
-
prompt: `
|
|
237821
|
+
system: COMPACT_SYSTEM_PROMPT,
|
|
237822
|
+
prompt: `Slice ${part} of ${total}.
|
|
237823
|
+
|
|
237824
|
+
${chunk}`,
|
|
237825
|
+
timeout: timeoutForInput(chunk.length),
|
|
237826
|
+
maxOutputTokens: COMPACT_SUMMARY_MAX_TOKENS,
|
|
237827
|
+
experimental_telemetry: telemetryFor("review-intent-brief-compact", userId)
|
|
237828
|
+
});
|
|
237829
|
+
const { text: rawText, finishReason } = result;
|
|
237830
|
+
if (finishReason === "length") {
|
|
237831
|
+
console.warn(`[ReviewBrief] slice ${part}/${total} summary hit the token cap \u2014 discarding`);
|
|
237832
|
+
return null;
|
|
237833
|
+
}
|
|
237834
|
+
const text2 = (rawText ?? "").trim();
|
|
237835
|
+
if (text2.length === 0) {
|
|
237836
|
+
console.warn(`[ReviewBrief] slice ${part}/${total} came back empty (finishReason: ${finishReason ?? "unknown"})`);
|
|
237837
|
+
return null;
|
|
237838
|
+
}
|
|
237839
|
+
return withinLimit(text2, COMPACT_SUMMARY_MAX_CHARS, `slice ${part}/${total} summary`) ? text2 : null;
|
|
237840
|
+
} catch (error48) {
|
|
237841
|
+
if (isSizeRelatedFailure(error48)) throw error48;
|
|
237842
|
+
console.warn(`[ReviewBrief] compaction of slice ${part}/${total} failed:`, error48.message);
|
|
237843
|
+
return null;
|
|
237844
|
+
}
|
|
237845
|
+
}
|
|
237846
|
+
async function compactConversation(model, messages, options = {}) {
|
|
237847
|
+
const sliceChars = options.sliceChars ?? COMPACT_SLICE_BUDGETS[0];
|
|
237848
|
+
const recentChars = Math.max(MIN_RECENT_VERBATIM_CHARS, Math.floor(sliceChars * RECENT_VERBATIM_RATIO));
|
|
237849
|
+
const entries = toEntries(messages);
|
|
237850
|
+
if (entries.length === 0) return null;
|
|
237851
|
+
const { older, recent } = splitRecent(entries, recentChars);
|
|
237852
|
+
if (older.length === 0) return null;
|
|
237853
|
+
const chunks = chunkEntries(older, sliceChars);
|
|
237854
|
+
if (chunks.length > MAX_FOLD_CALLS) {
|
|
237855
|
+
console.warn(
|
|
237856
|
+
`[ReviewBrief] conversation needs ${chunks.length} slices of ${sliceChars} chars (max ${MAX_FOLD_CALLS}) \u2014 falling back to tier 2`
|
|
237857
|
+
);
|
|
237858
|
+
return null;
|
|
237859
|
+
}
|
|
237860
|
+
const settled = await Promise.allSettled(
|
|
237861
|
+
chunks.map((chunk, i) => compactChunk(model, renderAll(chunk), i + 1, chunks.length, options.userId))
|
|
237862
|
+
);
|
|
237863
|
+
const sizeRejection = settled.find((r) => r.status === "rejected" && isSizeRelatedFailure(r.reason));
|
|
237864
|
+
if (sizeRejection) throw sizeRejection.reason;
|
|
237865
|
+
if (settled.some((r) => r.status === "rejected" || r.value === null)) return null;
|
|
237866
|
+
const parts = settled.map(
|
|
237867
|
+
(r, i) => `[Compressed slice ${i + 1} of ${chunks.length}]
|
|
237868
|
+
${r.value}`
|
|
237869
|
+
);
|
|
237870
|
+
if (recent.length > 0) parts.push(`[Recent turns, verbatim]
|
|
237871
|
+
${renderAll(recent)}`);
|
|
237872
|
+
return parts.join(SEP);
|
|
237873
|
+
}
|
|
237874
|
+
function buildBriefPrompt(conversation, reversals) {
|
|
237875
|
+
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 : "";
|
|
237876
|
+
return `${preamble}Distill this conversation into an intent brief:
|
|
237877
|
+
|
|
237878
|
+
${conversation}`;
|
|
237879
|
+
}
|
|
237880
|
+
function appendCompactionNote(brief, compacted) {
|
|
237881
|
+
return compacted ? `${brief}${SEP}${COMPACTION_NOTE}` : brief;
|
|
237882
|
+
}
|
|
237883
|
+
async function extractReversalsWithModel(model, conversation, options = {}) {
|
|
237884
|
+
if (conversation.trim().length < REVERSAL_MIN_CHARS) return null;
|
|
237885
|
+
try {
|
|
237886
|
+
const result = await generateText({
|
|
237887
|
+
model,
|
|
237888
|
+
system: REVERSAL_SYSTEM_PROMPT,
|
|
237889
|
+
prompt: `Report the reversals in this conversation:
|
|
237574
237890
|
|
|
237575
237891
|
${conversation}`,
|
|
237576
|
-
timeout:
|
|
237577
|
-
|
|
237892
|
+
timeout: JUDGMENT_TIMEOUT_MS,
|
|
237893
|
+
maxOutputTokens: REVERSALS_MAX_TOKENS,
|
|
237894
|
+
experimental_telemetry: telemetryFor("review-intent-brief-reversals", options.userId)
|
|
237578
237895
|
});
|
|
237579
|
-
const
|
|
237580
|
-
|
|
237896
|
+
const { text: rawText, finishReason } = result;
|
|
237897
|
+
if (finishReason === "length") {
|
|
237898
|
+
console.warn("[ReviewBrief] reversal list hit the token cap \u2014 discarding");
|
|
237899
|
+
return null;
|
|
237900
|
+
}
|
|
237901
|
+
const text2 = (rawText ?? "").trim();
|
|
237902
|
+
if (text2.length === 0 || /^none\b/i.test(text2)) return null;
|
|
237903
|
+
return withinLimit(text2, REVERSALS_MAX_CHARS, "reversal list") ? text2 : null;
|
|
237904
|
+
} catch (error48) {
|
|
237905
|
+
if (!isSizeRelatedFailure(error48)) throw error48;
|
|
237906
|
+
console.warn("[ReviewBrief] reversal pass hit a size limit \u2014 skipping it:", error48.message);
|
|
237907
|
+
return null;
|
|
237908
|
+
}
|
|
237909
|
+
}
|
|
237910
|
+
async function generateIntentBriefWithModel(model, conversation, options = {}) {
|
|
237911
|
+
if (conversation.trim().length === 0) return null;
|
|
237912
|
+
try {
|
|
237913
|
+
const result = await generateText({
|
|
237914
|
+
model,
|
|
237915
|
+
system: SYSTEM_PROMPT2,
|
|
237916
|
+
prompt: buildBriefPrompt(conversation, options.reversals ?? null),
|
|
237917
|
+
timeout: JUDGMENT_TIMEOUT_MS,
|
|
237918
|
+
maxOutputTokens: BRIEF_MAX_TOKENS,
|
|
237919
|
+
experimental_telemetry: telemetryFor("review-intent-brief", options.userId)
|
|
237920
|
+
});
|
|
237921
|
+
const { text: rawText, finishReason } = result;
|
|
237922
|
+
const text2 = (rawText ?? "").trim();
|
|
237923
|
+
if (text2.length === 0) {
|
|
237924
|
+
console.warn(`[ReviewBrief] model returned empty text (finishReason: ${finishReason ?? "unknown"})`);
|
|
237925
|
+
return null;
|
|
237926
|
+
}
|
|
237927
|
+
if (finishReason === "length") return text2.slice(0, BRIEF_MAX_CHARS) + BRIEF_TRUNCATION_NOTE;
|
|
237928
|
+
return text2.length <= BRIEF_MAX_CHARS ? text2 : text2.slice(0, BRIEF_MAX_CHARS) + BRIEF_TRUNCATION_NOTE;
|
|
237581
237929
|
} catch (error48) {
|
|
237930
|
+
if (options.rethrowSizeFailures && isSizeRelatedFailure(error48)) throw error48;
|
|
237582
237931
|
console.warn("[ReviewBrief] AI generation failed:", error48.message);
|
|
237583
237932
|
return null;
|
|
237584
237933
|
}
|
|
237585
237934
|
}
|
|
237935
|
+
async function distill(model, conversation, userId, rethrowSizeFailures) {
|
|
237936
|
+
const reversals = await extractReversalsWithModel(model, conversation, { userId });
|
|
237937
|
+
return generateIntentBriefWithModel(model, conversation, { userId, reversals, rethrowSizeFailures });
|
|
237938
|
+
}
|
|
237586
237939
|
async function generateIntentBrief(storage, userId, messages) {
|
|
237587
237940
|
try {
|
|
237588
|
-
|
|
237941
|
+
const config2 = await getChatProviderConfig(storage, userId);
|
|
237942
|
+
const mainOk = isModelConfigured(config2, config2.main);
|
|
237943
|
+
const fastOk = isModelConfigured(config2, config2.fast);
|
|
237944
|
+
if (!mainOk && !fastOk) return null;
|
|
237589
237945
|
const conversation = serializeConversationForBrief(messages);
|
|
237590
237946
|
if (!conversation) return null;
|
|
237591
|
-
|
|
237947
|
+
const distillModel = mainOk ? await resolveChatModel(storage, userId) : await resolveFastChatModel(storage, userId);
|
|
237948
|
+
const compactModel = fastOk ? await resolveFastChatModel(storage, userId) : distillModel;
|
|
237949
|
+
if (conversation.length <= COMPACT_TRIGGER_CHARS) {
|
|
237950
|
+
try {
|
|
237951
|
+
const brief = await distill(distillModel, conversation, userId, true);
|
|
237952
|
+
return brief === null ? null : appendCompactionNote(brief, false);
|
|
237953
|
+
} catch (error48) {
|
|
237954
|
+
if (!isSizeRelatedFailure(error48)) throw error48;
|
|
237955
|
+
console.warn("[ReviewBrief] full conversation rejected as too large \u2014 compacting and retrying");
|
|
237956
|
+
}
|
|
237957
|
+
}
|
|
237958
|
+
for (const sliceChars of COMPACT_SLICE_BUDGETS) {
|
|
237959
|
+
try {
|
|
237960
|
+
const compacted = await compactConversation(compactModel, messages, { userId, sliceChars });
|
|
237961
|
+
if (compacted === null) return null;
|
|
237962
|
+
const brief = await distill(distillModel, compacted, userId, true);
|
|
237963
|
+
return brief === null ? null : appendCompactionNote(brief, true);
|
|
237964
|
+
} catch (error48) {
|
|
237965
|
+
if (!isSizeRelatedFailure(error48)) throw error48;
|
|
237966
|
+
console.warn(`[ReviewBrief] ${sliceChars}-char slices still too large \u2014 retrying smaller`);
|
|
237967
|
+
}
|
|
237968
|
+
}
|
|
237969
|
+
console.warn("[ReviewBrief] could not compact the conversation small enough \u2014 falling back to tier 2");
|
|
237970
|
+
return null;
|
|
237592
237971
|
} catch (error48) {
|
|
237593
237972
|
console.warn("[ReviewBrief] generation failed:", error48.message);
|
|
237594
237973
|
return null;
|