@vibedeckx/darwin-arm64 0.2.17 → 0.2.19
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 +405 -49
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -186274,7 +186274,7 @@ var createSearchCacheRepos = (kdb, _h) => ({
|
|
|
186274
186274
|
});
|
|
186275
186275
|
|
|
186276
186276
|
// src/storage/repositories/workflow-runs.ts
|
|
186277
|
-
var ACTIVE = ["waiting_reviewer", "waiting_feedback", "sending_feedback"];
|
|
186277
|
+
var ACTIVE = ["waiting_reviewer", "waiting_feedback", "discussing", "sending_feedback"];
|
|
186278
186278
|
var asRun = (row) => row;
|
|
186279
186279
|
var createWorkflowRunRepos = (kdb) => ({
|
|
186280
186280
|
workflowRuns: {
|
|
@@ -202331,7 +202331,7 @@ import { existsSync as existsSync3 } from "fs";
|
|
|
202331
202331
|
// src/notification-milestones.ts
|
|
202332
202332
|
var sessionResultReadyId = (sessionId, turnEndEntryIndex) => `session:${sessionId}:turn:${turnEndEntryIndex}:result-ready`;
|
|
202333
202333
|
var sessionFailedId = (sessionId, turnEndEntryIndex) => `session:${sessionId}:turn:${turnEndEntryIndex}:failed`;
|
|
202334
|
-
var reviewReadyId = (workflowRunId) => `workflow:${workflowRunId}:review-ready`;
|
|
202334
|
+
var reviewReadyId = (workflowRunId, turnEndEntryIndex) => `workflow:${workflowRunId}:turn:${turnEndEntryIndex}:review-ready`;
|
|
202335
202335
|
var workflowFailedId = (workflowRunId, stateVersion) => `workflow:${workflowRunId}:failed:${stateVersion}`;
|
|
202336
202336
|
function findTurnOpeningUserEntry(entries, beforeIndex) {
|
|
202337
202337
|
let opening;
|
|
@@ -225173,6 +225173,19 @@ var AgentSessionManager = class {
|
|
|
225173
225173
|
console.error(`[AgentSession] Error in ${label} handler for ${session.id}:`, err);
|
|
225174
225174
|
});
|
|
225175
225175
|
}
|
|
225176
|
+
/**
|
|
225177
|
+
* Same serial chain as `enqueueSessionWork`, for a caller that needs the
|
|
225178
|
+
* result back. The chain itself absorbs the outcome (success or failure) so
|
|
225179
|
+
* one queued step can never break the next; the caller gets the real promise.
|
|
225180
|
+
*/
|
|
225181
|
+
runSerialForResult(session, work) {
|
|
225182
|
+
const result = session.eventChain.then(work);
|
|
225183
|
+
session.eventChain = result.then(
|
|
225184
|
+
() => void 0,
|
|
225185
|
+
() => void 0
|
|
225186
|
+
);
|
|
225187
|
+
return result;
|
|
225188
|
+
}
|
|
225176
225189
|
clearGraceTimer(session) {
|
|
225177
225190
|
if (session.graceTimer) {
|
|
225178
225191
|
clearTimeout(session.graceTimer);
|
|
@@ -226051,6 +226064,86 @@ var AgentSessionManager = class {
|
|
|
226051
226064
|
}
|
|
226052
226065
|
return "ok";
|
|
226053
226066
|
}
|
|
226067
|
+
/**
|
|
226068
|
+
* Set (or clear, with null) a session's model. The model is a spawn
|
|
226069
|
+
* argument, so it takes effect on the next process the session spawns —
|
|
226070
|
+
* immediately for a dormant session (a branch, or one that was stopped),
|
|
226071
|
+
* and after retiring the idle process for a session that has one.
|
|
226072
|
+
*
|
|
226073
|
+
* Refused ("busy") under exactly the rule switchAgentType uses: a turn in
|
|
226074
|
+
* flight on a session that has history. Both are respawn-shaped changes, so
|
|
226075
|
+
* a divergent rule here would only mean the model chip and the agent chip
|
|
226076
|
+
* lock at different moments in the same header row.
|
|
226077
|
+
*
|
|
226078
|
+
* The name is never validated — an unknown one is passed to the CLI and
|
|
226079
|
+
* fails there, same as at creation.
|
|
226080
|
+
*
|
|
226081
|
+
* The whole transition runs on the session's serial event chain and writes
|
|
226082
|
+
* the row BEFORE touching memory, so the three places a model lives — the
|
|
226083
|
+
* row, `session.model`, and the process spawned from it — can never disagree:
|
|
226084
|
+
* a failed write ("error") leaves all three exactly as they were.
|
|
226085
|
+
*/
|
|
226086
|
+
async setModel(sessionId, model) {
|
|
226087
|
+
const session = this.sessions.get(sessionId);
|
|
226088
|
+
if (!session) return "not_found";
|
|
226089
|
+
const normalized = model?.trim() ? model.trim() : null;
|
|
226090
|
+
return this.runSerialForResult(session, async () => {
|
|
226091
|
+
if (session.model === normalized) return "ok";
|
|
226092
|
+
if (this.isModelChangeTooLate(session)) return "busy";
|
|
226093
|
+
console.log(`[AgentSession] Setting session ${sessionId} model ${session.model ?? "default"} \u2192 ${normalized ?? "default"}`);
|
|
226094
|
+
const previous = session.model;
|
|
226095
|
+
if (!session.skipDb) {
|
|
226096
|
+
try {
|
|
226097
|
+
await this.storage.agentSessions.updateModel(sessionId, normalized);
|
|
226098
|
+
} catch (err) {
|
|
226099
|
+
console.error(`[AgentSession] Failed to persist model for ${sessionId}:`, err);
|
|
226100
|
+
return "error";
|
|
226101
|
+
}
|
|
226102
|
+
if (this.isModelChangeTooLate(session)) {
|
|
226103
|
+
try {
|
|
226104
|
+
await this.storage.agentSessions.updateModel(sessionId, previous);
|
|
226105
|
+
} catch (err) {
|
|
226106
|
+
console.error(`[AgentSession] Failed to roll back model for ${sessionId}:`, err);
|
|
226107
|
+
}
|
|
226108
|
+
return "busy";
|
|
226109
|
+
}
|
|
226110
|
+
}
|
|
226111
|
+
session.model = normalized;
|
|
226112
|
+
if (session.process) {
|
|
226113
|
+
const proc = session.process;
|
|
226114
|
+
session.process = null;
|
|
226115
|
+
this.killProcess(proc);
|
|
226116
|
+
this.emitProcessAlive(session, false);
|
|
226117
|
+
await this.finalizeStreamingEntry(session);
|
|
226118
|
+
session.store.currentAssistantIndex = null;
|
|
226119
|
+
session.buffer = "";
|
|
226120
|
+
this.resetCompletion(session);
|
|
226121
|
+
session.dormant = true;
|
|
226122
|
+
if (session.status !== "stopped") {
|
|
226123
|
+
session.status = "stopped";
|
|
226124
|
+
if (!session.skipDb) {
|
|
226125
|
+
try {
|
|
226126
|
+
await this.storage.agentSessions.updateStatus(sessionId, "stopped");
|
|
226127
|
+
} catch (err) {
|
|
226128
|
+
console.error(`[AgentSession] Failed to persist stopped status for ${sessionId}:`, err);
|
|
226129
|
+
}
|
|
226130
|
+
}
|
|
226131
|
+
this.broadcastPatch(sessionId, ConversationPatch.updateStatus("stopped"));
|
|
226132
|
+
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: "stopped" });
|
|
226133
|
+
}
|
|
226134
|
+
}
|
|
226135
|
+
return "ok";
|
|
226136
|
+
});
|
|
226137
|
+
}
|
|
226138
|
+
/**
|
|
226139
|
+
* True once the model can no longer reach the CLI: a turn is in flight on a
|
|
226140
|
+
* session that has history. Same rule switchAgentType refuses on — both are
|
|
226141
|
+
* respawn-shaped changes, and a divergent rule would only mean the model
|
|
226142
|
+
* chip and the agent chip lock at different moments in the same header row.
|
|
226143
|
+
*/
|
|
226144
|
+
isModelChangeTooLate(session) {
|
|
226145
|
+
return session.status === "running" && session.store.entries.some(Boolean);
|
|
226146
|
+
}
|
|
226054
226147
|
/**
|
|
226055
226148
|
* Switch permission mode for a session (preserves conversation history)
|
|
226056
226149
|
*/
|
|
@@ -226811,6 +226904,9 @@ function runUpdatedEventFromRemoteFrame(parsed, sessionId, remoteInfo) {
|
|
|
226811
226904
|
const run2 = mapRemoteRun(bare, remoteInfo.remoteServerId, projectId);
|
|
226812
226905
|
return { type: "workflow:run-updated", projectId, branch: run2.branch, run: run2 };
|
|
226813
226906
|
}
|
|
226907
|
+
function runUpdatedFrameForSubscribers(evt) {
|
|
226908
|
+
return JSON.stringify({ workflowRunUpdated: evt.run });
|
|
226909
|
+
}
|
|
226814
226910
|
|
|
226815
226911
|
// src/routes/notification-outbox-routes.ts
|
|
226816
226912
|
var import_fastify_plugin = __toESM(require_plugin2(), 1);
|
|
@@ -226871,7 +226967,8 @@ var routes = async (fastify2) => {
|
|
|
226871
226967
|
events: [],
|
|
226872
226968
|
headCursor,
|
|
226873
226969
|
nextCursor: headCursor,
|
|
226874
|
-
hasMore: false
|
|
226970
|
+
hasMore: false,
|
|
226971
|
+
sessionTitle: null
|
|
226875
226972
|
});
|
|
226876
226973
|
continue;
|
|
226877
226974
|
}
|
|
@@ -226881,12 +226978,14 @@ var routes = async (fastify2) => {
|
|
|
226881
226978
|
validated.limit
|
|
226882
226979
|
);
|
|
226883
226980
|
const nextCursor = events.length > 0 ? events[events.length - 1].seq : request.after;
|
|
226981
|
+
const session = events.length > 0 ? await fastify2.storage.agentSessions.getById(request.sessionId) : void 0;
|
|
226884
226982
|
sessions.push({
|
|
226885
226983
|
sessionId: request.sessionId,
|
|
226886
226984
|
events,
|
|
226887
226985
|
headCursor,
|
|
226888
226986
|
nextCursor,
|
|
226889
|
-
hasMore: nextCursor < headCursor
|
|
226987
|
+
hasMore: nextCursor < headCursor,
|
|
226988
|
+
sessionTitle: session?.title ?? null
|
|
226890
226989
|
});
|
|
226891
226990
|
}
|
|
226892
226991
|
return reply.code(200).send({ sessions });
|
|
@@ -226901,7 +227000,10 @@ var WATCH_WINDOW_MS = 30 * 60 * 1e3;
|
|
|
226901
227000
|
var WATCH_EXTEND_THROTTLE_MS = 5 * 60 * 1e3;
|
|
226902
227001
|
var MAX_TRACKED_SESSIONS = 5e3;
|
|
226903
227002
|
var SYNC_INTERVAL_MS = 6e4;
|
|
227003
|
+
var COMPLETION_NUDGE_DEBOUNCE_MS = 250;
|
|
227004
|
+
var MAX_CONCURRENT_SERVERS = 4;
|
|
226904
227005
|
var MAX_PAGES_PER_SYNC = 50;
|
|
227006
|
+
var MAX_SESSION_TITLE_LENGTH = 200;
|
|
226905
227007
|
function localWorkflowRunId(remoteServerId, projectId, remoteRunId) {
|
|
226906
227008
|
return `remote-${remoteServerId}-${projectId}-${remoteRunId}`;
|
|
226907
227009
|
}
|
|
@@ -226913,10 +227015,19 @@ var RemoteNotificationSync = class {
|
|
|
226913
227015
|
notificationService;
|
|
226914
227016
|
reverseConnectManager;
|
|
226915
227017
|
proxy;
|
|
227018
|
+
nudgeDebounceMs;
|
|
226916
227019
|
timer = null;
|
|
226917
227020
|
stopped = false;
|
|
226918
|
-
/**
|
|
226919
|
-
|
|
227021
|
+
/**
|
|
227022
|
+
* Per-server work queues. Two syncs of the SAME server must not interleave
|
|
227023
|
+
* (they share its cursors), but two different servers have nothing in
|
|
227024
|
+
* common — serializing them globally only meant an unreachable worker's
|
|
227025
|
+
* retry budget delayed every healthy worker's import behind it.
|
|
227026
|
+
*/
|
|
227027
|
+
serverChains = /* @__PURE__ */ new Map();
|
|
227028
|
+
/** Local session ids whose completion is waiting for the debounce to fire. */
|
|
227029
|
+
nudgeSessions = /* @__PURE__ */ new Set();
|
|
227030
|
+
nudgeTimer = null;
|
|
226920
227031
|
/**
|
|
226921
227032
|
* Remote sessions this front currently believes are RUNNING. They are polled
|
|
226922
227033
|
* regardless of their persisted watch window: an agent turn can easily run
|
|
@@ -226932,6 +227043,7 @@ var RemoteNotificationSync = class {
|
|
|
226932
227043
|
this.notificationService = deps.notificationService;
|
|
226933
227044
|
this.reverseConnectManager = deps.reverseConnectManager;
|
|
226934
227045
|
this.proxy = deps.proxy ?? proxyToRemoteAuto;
|
|
227046
|
+
this.nudgeDebounceMs = deps.nudgeDebounceMs ?? COMPLETION_NUDGE_DEBOUNCE_MS;
|
|
226935
227047
|
}
|
|
226936
227048
|
start() {
|
|
226937
227049
|
if (this.timer || this.stopped) return;
|
|
@@ -226944,6 +227056,9 @@ var RemoteNotificationSync = class {
|
|
|
226944
227056
|
this.stopped = true;
|
|
226945
227057
|
if (this.timer) clearInterval(this.timer);
|
|
226946
227058
|
this.timer = null;
|
|
227059
|
+
if (this.nudgeTimer) clearTimeout(this.nudgeTimer);
|
|
227060
|
+
this.nudgeTimer = null;
|
|
227061
|
+
this.nudgeSessions.clear();
|
|
226947
227062
|
this.activeRemoteSessions.clear();
|
|
226948
227063
|
this.lastWatchExtend.clear();
|
|
226949
227064
|
}
|
|
@@ -226967,6 +227082,7 @@ var RemoteNotificationSync = class {
|
|
|
226967
227082
|
} else if (event.type === "session:taskCompleted" || event.type === "session:finished") {
|
|
226968
227083
|
this.activeRemoteSessions.delete(sessionId);
|
|
226969
227084
|
}
|
|
227085
|
+
if (isTurnTerminalEvent(event)) this.scheduleCompletionSync(sessionId);
|
|
226970
227086
|
const now2 = Date.now();
|
|
226971
227087
|
const last = this.lastWatchExtend.get(sessionId) ?? 0;
|
|
226972
227088
|
if (now2 - last < WATCH_EXTEND_THROTTLE_MS) return;
|
|
@@ -226980,10 +227096,65 @@ var RemoteNotificationSync = class {
|
|
|
226980
227096
|
if (now2 - at > WATCH_WINDOW_MS) this.lastWatchExtend.delete(sessionId);
|
|
226981
227097
|
}
|
|
226982
227098
|
}
|
|
226983
|
-
/**
|
|
227099
|
+
/**
|
|
227100
|
+
* Fire-and-forget sweep. No longer a global queue: the per-server chains
|
|
227101
|
+
* inside syncAll/syncServer own the mutual exclusion that actually matters
|
|
227102
|
+
* (one server's cursors), so callers here don't wait on unrelated servers.
|
|
227103
|
+
*/
|
|
226984
227104
|
enqueue(work) {
|
|
226985
227105
|
if (this.stopped) return;
|
|
226986
|
-
|
|
227106
|
+
void work().catch((err) => console.warn("[RemoteNotifications] sync failed:", err));
|
|
227107
|
+
}
|
|
227108
|
+
/**
|
|
227109
|
+
* Queue `work` behind whatever is already running for this server, and
|
|
227110
|
+
* nothing else. Errors are contained so one server's failure can't reject
|
|
227111
|
+
* an unrelated caller awaiting the same chain.
|
|
227112
|
+
*/
|
|
227113
|
+
runOnServer(remoteServerId, work) {
|
|
227114
|
+
const previous = this.serverChains.get(remoteServerId) ?? Promise.resolve();
|
|
227115
|
+
const next = previous.then(work).catch((err) => console.warn(`[RemoteNotifications] sync for ${remoteServerId} failed:`, err)).finally(() => {
|
|
227116
|
+
if (this.serverChains.get(remoteServerId) === next) this.serverChains.delete(remoteServerId);
|
|
227117
|
+
});
|
|
227118
|
+
this.serverChains.set(remoteServerId, next);
|
|
227119
|
+
return next;
|
|
227120
|
+
}
|
|
227121
|
+
/**
|
|
227122
|
+
* Debounced, per-server pull for sessions whose turn just ended.
|
|
227123
|
+
*
|
|
227124
|
+
* Coalescing state is intentionally separate from `lastWatchExtend`: that
|
|
227125
|
+
* map throttles DB writes on a 5-minute scale, which would swallow nearly
|
|
227126
|
+
* every completion.
|
|
227127
|
+
*/
|
|
227128
|
+
scheduleCompletionSync(localSessionId) {
|
|
227129
|
+
if (this.stopped) return;
|
|
227130
|
+
this.nudgeSessions.add(localSessionId);
|
|
227131
|
+
if (this.nudgeTimer) return;
|
|
227132
|
+
this.nudgeTimer = setTimeout(() => {
|
|
227133
|
+
this.nudgeTimer = null;
|
|
227134
|
+
const batch = [...this.nudgeSessions];
|
|
227135
|
+
this.nudgeSessions.clear();
|
|
227136
|
+
void this.syncSessions(batch).catch(
|
|
227137
|
+
(err) => console.warn("[RemoteNotifications] completion sync failed:", err)
|
|
227138
|
+
);
|
|
227139
|
+
}, this.nudgeDebounceMs);
|
|
227140
|
+
this.nudgeTimer.unref?.();
|
|
227141
|
+
}
|
|
227142
|
+
/**
|
|
227143
|
+
* Pull exactly the mappings named, grouped per server. Deliberately does NOT
|
|
227144
|
+
* go through `candidates()`: an explicit completion signal outranks the
|
|
227145
|
+
* watch window, which may well have lapsed during a long turn.
|
|
227146
|
+
*/
|
|
227147
|
+
async syncSessions(localSessionIds) {
|
|
227148
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
227149
|
+
for (const localSessionId of localSessionIds) {
|
|
227150
|
+
const mapping = await this.storage.remoteSessionMappings.getByLocal(localSessionId).catch(() => void 0);
|
|
227151
|
+
if (!mapping) continue;
|
|
227152
|
+
const group2 = byServer.get(mapping.remote_server_id);
|
|
227153
|
+
if (group2) group2.push(mapping);
|
|
227154
|
+
else byServer.set(mapping.remote_server_id, [mapping]);
|
|
227155
|
+
}
|
|
227156
|
+
if (byServer.size === 0) return;
|
|
227157
|
+
await this.dispatchByServer([...byServer]);
|
|
226987
227158
|
}
|
|
226988
227159
|
/** Extend a mapping's periodic-poll window (create / send / workflow start / live activity). */
|
|
226989
227160
|
async extendWatch(localSessionId) {
|
|
@@ -227033,17 +227204,33 @@ var RemoteNotificationSync = class {
|
|
|
227033
227204
|
);
|
|
227034
227205
|
return true;
|
|
227035
227206
|
}
|
|
227036
|
-
/** Sweep every mapped remote server. */
|
|
227207
|
+
/** Sweep every mapped remote server, up to MAX_CONCURRENT_SERVERS at a time. */
|
|
227037
227208
|
async syncAll(opts) {
|
|
227038
227209
|
const mappings = await this.candidates(opts);
|
|
227039
|
-
|
|
227040
|
-
await this.syncMappings(serverId, group2);
|
|
227041
|
-
}
|
|
227210
|
+
await this.dispatchByServer([...groupByServer(mappings)]);
|
|
227042
227211
|
}
|
|
227043
227212
|
/** Sweep one server — used when a reverse connection comes online. */
|
|
227044
227213
|
async syncServer(remoteServerId, opts) {
|
|
227045
227214
|
const mappings = (await this.candidates(opts)).filter((m2) => m2.remote_server_id === remoteServerId);
|
|
227046
|
-
if (mappings.length > 0) await this.
|
|
227215
|
+
if (mappings.length > 0) await this.dispatchByServer([[remoteServerId, mappings]]);
|
|
227216
|
+
}
|
|
227217
|
+
/**
|
|
227218
|
+
* Run one group per server, bounded. Each group still queues on its own
|
|
227219
|
+
* server chain, so this composes with a nudge that arrives mid-sweep: same
|
|
227220
|
+
* server → it waits its turn; different server → it runs immediately.
|
|
227221
|
+
*/
|
|
227222
|
+
async dispatchByServer(groups) {
|
|
227223
|
+
let next = 0;
|
|
227224
|
+
const workers = Array.from(
|
|
227225
|
+
{ length: Math.min(MAX_CONCURRENT_SERVERS, groups.length) },
|
|
227226
|
+
async () => {
|
|
227227
|
+
for (let i = next++; i < groups.length; i = next++) {
|
|
227228
|
+
const [serverId, group2] = groups[i];
|
|
227229
|
+
await this.runOnServer(serverId, () => this.syncMappings(serverId, group2));
|
|
227230
|
+
}
|
|
227231
|
+
}
|
|
227232
|
+
);
|
|
227233
|
+
await Promise.all(workers);
|
|
227047
227234
|
}
|
|
227048
227235
|
/**
|
|
227049
227236
|
* Mappings this sweep should poll: the persisted watch set, plus any session
|
|
@@ -227138,6 +227325,7 @@ var RemoteNotificationSync = class {
|
|
|
227138
227325
|
console.warn(`[RemoteNotifications] project ${mapping.project_id} not found; skipping ${mapping.local_session_id}`);
|
|
227139
227326
|
return false;
|
|
227140
227327
|
}
|
|
227328
|
+
const sessionTitle = typeof session.sessionTitle === "string" ? session.sessionTitle.slice(0, MAX_SESSION_TITLE_LENGTH) : null;
|
|
227141
227329
|
for (const event of session.events) {
|
|
227142
227330
|
const notification = await this.notificationService.buildNotification(event, {
|
|
227143
227331
|
id: localNotificationId(remoteServerId, event.id),
|
|
@@ -227145,7 +227333,7 @@ var RemoteNotificationSync = class {
|
|
|
227145
227333
|
projectId: mapping.project_id,
|
|
227146
227334
|
sessionId: mapping.local_session_id,
|
|
227147
227335
|
workflowRunId: event.workflow_run_id ? localWorkflowRunId(remoteServerId, mapping.project_id, event.workflow_run_id) : null
|
|
227148
|
-
});
|
|
227336
|
+
}, { sessionTitle });
|
|
227149
227337
|
const { inserted } = await this.storage.notifications.importRemote({
|
|
227150
227338
|
notification,
|
|
227151
227339
|
remoteServerId,
|
|
@@ -227190,6 +227378,10 @@ var RemoteNotificationSync = class {
|
|
|
227190
227378
|
};
|
|
227191
227379
|
}
|
|
227192
227380
|
};
|
|
227381
|
+
function isTurnTerminalEvent(event) {
|
|
227382
|
+
if (event.type === "session:taskCompleted" || event.type === "session:finished") return true;
|
|
227383
|
+
return event.type === "session:status" && (event.status === "stopped" || event.status === "error");
|
|
227384
|
+
}
|
|
227193
227385
|
function groupByServer(mappings) {
|
|
227194
227386
|
const groups = /* @__PURE__ */ new Map();
|
|
227195
227387
|
for (const mapping of mappings) {
|
|
@@ -227359,9 +227551,10 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, wsOptions, rev
|
|
|
227359
227551
|
}
|
|
227360
227552
|
}
|
|
227361
227553
|
} else if ("workflowRunUpdated" in parsed) {
|
|
227362
|
-
|
|
227363
|
-
|
|
227364
|
-
|
|
227554
|
+
const evt = runUpdatedEventFromRemoteFrame(parsed, sessionId, remoteInfo);
|
|
227555
|
+
if (evt) {
|
|
227556
|
+
eventBus?.emit(evt);
|
|
227557
|
+
cache2.broadcast(sessionId, runUpdatedFrameForSubscribers(evt));
|
|
227365
227558
|
}
|
|
227366
227559
|
} else if ("processAlive" in parsed) {
|
|
227367
227560
|
cache2.broadcast(sessionId, raw);
|
|
@@ -229950,12 +230143,18 @@ function selfReportSection(report) {
|
|
|
229950
230143
|
if (!report) return null;
|
|
229951
230144
|
return [
|
|
229952
230145
|
"\n## Author's self-report (unverified)",
|
|
229953
|
-
"The implementing agent described its own work as follows. Treat every claim as unverified \u2014
|
|
230146
|
+
"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.",
|
|
229954
230147
|
"<author-self-report>",
|
|
229955
230148
|
report,
|
|
229956
230149
|
"</author-self-report>"
|
|
229957
230150
|
].join("\n");
|
|
229958
230151
|
}
|
|
230152
|
+
var VERDICT_INSTRUCTIONS = [
|
|
230153
|
+
"\nEnd your final message with:",
|
|
230154
|
+
"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.",
|
|
230155
|
+
"2. Blocking findings \u2014 what must change before shipping, each specific and actionable (say explicitly when there are none).",
|
|
230156
|
+
"3. Non-blocking notes \u2014 style and polish, briefly, clearly separated from the blocking list."
|
|
230157
|
+
];
|
|
229959
230158
|
function buildReviewerPrompt(opts) {
|
|
229960
230159
|
const intent = opts.originalIntent !== opts.taskContext ? opts.originalIntent : null;
|
|
229961
230160
|
const brief = opts.intentBrief || null;
|
|
@@ -229994,7 +230193,12 @@ Confine your review to these files and changes. Other uncommitted or pre-existin
|
|
|
229994
230193
|
"- Inspect the actual workspace state yourself: read the relevant files, run `git diff`, `git status` and `git log`.",
|
|
229995
230194
|
reviewTargetPromptLine(opts.target),
|
|
229996
230195
|
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.",
|
|
229997
|
-
|
|
230196
|
+
// These two only make sense against a distilled brief: tier 2 has no
|
|
230197
|
+
// [settled]/[tentative] marks and no stated scope, and implying it does
|
|
230198
|
+
// would suppress findings on the strength of data that doesn't exist.
|
|
230199
|
+
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,
|
|
230200
|
+
brief ? "- Do not propose enhancements beyond the brief's stated scope \u2014 scope expansion is a product decision, not a review finding." : null,
|
|
230201
|
+
...VERDICT_INSTRUCTIONS,
|
|
229998
230202
|
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)"
|
|
229999
230203
|
].filter((l) => l !== null).join("\n");
|
|
230000
230204
|
}
|
|
@@ -230018,17 +230222,30 @@ ${opts.reviewFocus}` : null,
|
|
|
230018
230222
|
"- Check for regressions and remaining correctness or test gaps.",
|
|
230019
230223
|
"- Do NOT modify files \u2014 remain in read-only review mode.",
|
|
230020
230224
|
reviewTargetPromptLine(opts.target),
|
|
230021
|
-
|
|
230225
|
+
...VERDICT_INSTRUCTIONS
|
|
230022
230226
|
].filter((line) => line !== null).join("\n");
|
|
230023
230227
|
}
|
|
230024
230228
|
function buildFeedbackMessage(feedback) {
|
|
230025
230229
|
return [
|
|
230026
230230
|
"[Review Feedback]",
|
|
230027
|
-
"A reviewer agent examined your last completed work.
|
|
230231
|
+
"A reviewer agent examined your last completed work. It was in read-only mode and saw only this turn's diff plus a distilled brief of the conversation, so it may be working from wrong premises. Treat its findings as input to verify, not as conclusions.",
|
|
230232
|
+
"",
|
|
230233
|
+
"- Verify each item against the code before changing anything.",
|
|
230234
|
+
"- Push back when you have grounds: say what the item gets wrong and leave the code as it is. Do not change code to be agreeable.",
|
|
230235
|
+
"- Address blocking findings first; non-blocking notes need not be done this turn.",
|
|
230236
|
+
"- Do not widen the scope: no fixes the reviewer did not raise, no abstractions for hypothetical cases.",
|
|
230237
|
+
"- Close with a per-item account: fixed, or not fixed and why.",
|
|
230028
230238
|
"",
|
|
230029
230239
|
feedback
|
|
230030
230240
|
].join("\n");
|
|
230031
230241
|
}
|
|
230242
|
+
var FINAL_VERDICT_PROMPT = [
|
|
230243
|
+
"[Final verdict request]",
|
|
230244
|
+
"\u8BF7\u628A\u8BA8\u8BBA\u540E\u7684\u6700\u7EC8 review \u610F\u89C1\u5B8C\u6574\u8F93\u51FA\u4E3A\u9762\u5411\u4F5C\u8005\u7684\u5B9A\u7A3F\u3002\u8FD9\u6BB5\u8F93\u51FA\u5C06\u539F\u6837\u53D1\u9001\u7ED9\u4F5C\u8005\u2014\u2014\u4E0D\u8981\u5305\u542B\u5BF9\u8BDD\u6027\u5185\u5BB9\uFF1A\u4E0D\u8981\u5F15\u7528\u8BA8\u8BBA\u8FC7\u7A0B\u672C\u8EAB\uFF0C\u4E0D\u8981\u5411\u6211\u63D0\u95EE\u3002",
|
|
230245
|
+
"- \u5438\u6536\u8BA8\u8BBA\u4E2D\u8FBE\u6210\u4E00\u81F4\u7684\u4FEE\u6B63\uFF1A\u64A4\u56DE\u7684\u6761\u76EE\u4E0D\u518D\u51FA\u73B0\uFF0C\u4FEE\u8BA2\u8FC7\u7684\u6761\u76EE\u4EE5\u4FEE\u8BA2\u540E\u7684\u5F62\u5F0F\u7ED9\u51FA\u3002",
|
|
230246
|
+
"- \u4FDD\u6301\u5177\u4F53\uFF1A\u6BCF\u6761\u6307\u660E\u6587\u4EF6/\u4F4D\u7F6E\u4E0E\u95EE\u9898\uFF0C\u4EE5\u53CA\u671F\u671B\u7684\u4FEE\u590D\u65B9\u5411\u3002",
|
|
230247
|
+
...VERDICT_INSTRUCTIONS
|
|
230248
|
+
].join("\n");
|
|
230032
230249
|
var REVIEWER_AGENT_TYPES = /* @__PURE__ */ new Set(["claude-code", "codex"]);
|
|
230033
230250
|
var WorkflowEngine = class {
|
|
230034
230251
|
constructor(storage, agentOps) {
|
|
@@ -230350,7 +230567,7 @@ var WorkflowEngine = class {
|
|
|
230350
230567
|
...driftNote ? { error: driftNote } : {}
|
|
230351
230568
|
},
|
|
230352
230569
|
{
|
|
230353
|
-
id: reviewReadyId(run2.id),
|
|
230570
|
+
id: reviewReadyId(run2.id, boundary),
|
|
230354
230571
|
kind: "review_ready",
|
|
230355
230572
|
project_id: run2.project_id,
|
|
230356
230573
|
branch: run2.branch,
|
|
@@ -230397,12 +230614,49 @@ var WorkflowEngine = class {
|
|
|
230397
230614
|
this.emitRunUpdated(done);
|
|
230398
230615
|
return done;
|
|
230399
230616
|
}
|
|
230617
|
+
/**
|
|
230618
|
+
* Discussion → verdict: CAS the run back onto the reviewer track and inject
|
|
230619
|
+
* the final-verdict prompt. From waiting_reviewer the existing
|
|
230620
|
+
* handleTaskCompleted path reopens the gate with the new turn's output.
|
|
230621
|
+
* Send failure rolls the claim back so the finalize button stays actionable —
|
|
230622
|
+
* mirror of approveFeedback's no-auto-retry contract.
|
|
230623
|
+
*/
|
|
230624
|
+
async requestFinalVerdict(runId) {
|
|
230625
|
+
const run2 = await this.storage.workflowRuns.getById(runId);
|
|
230626
|
+
if (!run2 || run2.status !== "discussing" || !run2.reviewer_session_id) {
|
|
230627
|
+
throw new WorkflowError("bad-state", "run \u4E0D\u5728\u8BA8\u8BBA\u72B6\u6001");
|
|
230628
|
+
}
|
|
230629
|
+
const reviewerSession = await this.storage.agentSessions.getById(run2.reviewer_session_id);
|
|
230630
|
+
if (reviewerSession && reviewerSession.status === "running") {
|
|
230631
|
+
throw new WorkflowError("session-busy", "reviewer \u6B63\u5728\u56DE\u590D\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u5176\u5B8C\u6210\u540E\u518D\u751F\u6210\u7EC8\u7A3F");
|
|
230632
|
+
}
|
|
230633
|
+
const claimed = await this.storage.workflowRuns.transition(runId, "discussing", "waiting_reviewer", { error: null });
|
|
230634
|
+
if (!claimed) throw new WorkflowError("bad-state", "run \u72B6\u6001\u5DF2\u53D8\u5316\uFF08\u53EF\u80FD\u5DF2\u88AB\u5904\u7406\uFF09");
|
|
230635
|
+
const project = await this.storage.projects.getById(run2.project_id);
|
|
230636
|
+
const sent = await this.agentOps.sendUserMessage(run2.reviewer_session_id, FINAL_VERDICT_PROMPT, project?.path ?? void 0, void 0, REVIEWER_TURN).catch(() => false);
|
|
230637
|
+
if (!sent) {
|
|
230638
|
+
const rolledBack = await this.storage.workflowRuns.transition(runId, "waiting_reviewer", "discussing", {
|
|
230639
|
+
error: "\u53D1\u9001\u5931\u8D25\uFF1Areviewer session \u53EF\u80FD\u672A\u8FD0\u884C\u3002\u8BF7\u5728\u5176\u7A97\u53E3\u4E2D\u5524\u9192\u540E\u91CD\u8BD5\uFF0C\u6216\u7ED3\u675F\u672C\u6B21 review\u3002"
|
|
230640
|
+
});
|
|
230641
|
+
if (!rolledBack) {
|
|
230642
|
+
console.warn(
|
|
230643
|
+
`requestFinalVerdict: rollback CAS lost for run ${runId} \u2014 run moved concurrently (e.g. cancelled) while the send was in flight; rollback skipped`
|
|
230644
|
+
);
|
|
230645
|
+
}
|
|
230646
|
+
const rolled = await this.storage.workflowRuns.getById(runId);
|
|
230647
|
+
if (rolled) this.emitRunUpdated(rolled);
|
|
230648
|
+
throw new WorkflowError("send-failed", "\u5411 reviewer \u53D1\u9001\u7EC8\u7A3F\u8BF7\u6C42\u5931\u8D25");
|
|
230649
|
+
}
|
|
230650
|
+
const updated = await this.storage.workflowRuns.getById(runId);
|
|
230651
|
+
this.emitRunUpdated(updated);
|
|
230652
|
+
return updated;
|
|
230653
|
+
}
|
|
230400
230654
|
async cancelRun(runId, reason) {
|
|
230401
230655
|
const run2 = await this.storage.workflowRuns.getById(runId);
|
|
230402
230656
|
if (!run2) return void 0;
|
|
230403
230657
|
if (TERMINAL_STATUSES.has(run2.status)) return run2;
|
|
230404
230658
|
const patch = reason ? { error: reason } : void 0;
|
|
230405
|
-
const cancelled = await this.storage.workflowRuns.transition(runId, "waiting_reviewer", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_feedback", "cancelled", patch);
|
|
230659
|
+
const cancelled = await this.storage.workflowRuns.transition(runId, "waiting_reviewer", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_feedback", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "discussing", "cancelled", patch);
|
|
230406
230660
|
if (!cancelled) {
|
|
230407
230661
|
const current = await this.storage.workflowRuns.getById(runId);
|
|
230408
230662
|
if (current?.status === "sending_feedback") {
|
|
@@ -230420,6 +230674,10 @@ var WorkflowEngine = class {
|
|
|
230420
230674
|
/**
|
|
230421
230675
|
* Human takeover (spec §3.4): user sent a message directly to a run session.
|
|
230422
230676
|
*
|
|
230677
|
+
* Reviewer 分流:向 reviewer 发消息不再是接管——它开启一轮讨论,把 run
|
|
230678
|
+
* 移入 `discussing`(gate 收起),等待显式的 requestFinalVerdict 重新出稿。
|
|
230679
|
+
* 只有向 source session 发消息才算接管,结束 run。
|
|
230680
|
+
*
|
|
230423
230681
|
* Never-throws contract: this is called inline from the agent-session
|
|
230424
230682
|
* `/message` route BEFORE the user's message is delivered
|
|
230425
230683
|
* (agentOps.sendUserMessage). A throw here would abort delivery of that
|
|
@@ -230434,8 +230692,23 @@ var WorkflowEngine = class {
|
|
|
230434
230692
|
async handleExternalUserMessage(sessionId) {
|
|
230435
230693
|
const p2 = this.participants.get(sessionId);
|
|
230436
230694
|
if (!p2) return;
|
|
230695
|
+
if (p2.role === "reviewer") {
|
|
230696
|
+
try {
|
|
230697
|
+
const moved = await this.storage.workflowRuns.transition(p2.runId, "waiting_feedback", "discussing", { error: null }) || await this.storage.workflowRuns.transition(p2.runId, "waiting_reviewer", "discussing", { error: null });
|
|
230698
|
+
if (moved) {
|
|
230699
|
+
const updated = await this.storage.workflowRuns.getById(p2.runId);
|
|
230700
|
+
if (updated) this.emitRunUpdated(updated);
|
|
230701
|
+
}
|
|
230702
|
+
} catch (err) {
|
|
230703
|
+
console.error(
|
|
230704
|
+
`[WorkflowEngine] handleExternalUserMessage: failed moving run ${p2.runId} to discussing; swallowed to honor never-throws contract`,
|
|
230705
|
+
err
|
|
230706
|
+
);
|
|
230707
|
+
}
|
|
230708
|
+
return;
|
|
230709
|
+
}
|
|
230437
230710
|
try {
|
|
230438
|
-
await this.cancelRun(p2.runId, "\u7528\u6237\u63A5\u7BA1\uFF1A\u76F4\u63A5\u5411
|
|
230711
|
+
await this.cancelRun(p2.runId, "\u7528\u6237\u63A5\u7BA1\uFF1A\u76F4\u63A5\u5411 source session \u53D1\u9001\u4E86\u6D88\u606F\uFF0Creview \u5DF2\u7ED3\u675F\u3002");
|
|
230439
230712
|
} catch (err) {
|
|
230440
230713
|
if (err instanceof WorkflowError && err.code === "bad-state") {
|
|
230441
230714
|
console.warn(
|
|
@@ -232320,8 +232593,12 @@ var NotificationService = class {
|
|
|
232320
232593
|
* Assemble the persisted row. `ids` is separate from the event because remote
|
|
232321
232594
|
* imports substitute front-local identities (see RemoteNotificationSync) while
|
|
232322
232595
|
* reusing this exact copy generation.
|
|
232596
|
+
*
|
|
232597
|
+
* `opts.sessionTitle` covers the remote-import case: the front has a mapping
|
|
232598
|
+
* but no local session row, so the worker's query-time title is the only way
|
|
232599
|
+
* to label the notification with the session's name.
|
|
232323
232600
|
*/
|
|
232324
|
-
async buildNotification(event, ids) {
|
|
232601
|
+
async buildNotification(event, ids, opts) {
|
|
232325
232602
|
const session = ids.sessionId ? await this.storage.agentSessions.getById(ids.sessionId) : void 0;
|
|
232326
232603
|
const project = await this.storage.projects.getById(ids.projectId);
|
|
232327
232604
|
return {
|
|
@@ -232334,7 +232611,7 @@ var NotificationService = class {
|
|
|
232334
232611
|
workflow_run_id: ids.workflowRunId,
|
|
232335
232612
|
title: notificationTitle(event.kind),
|
|
232336
232613
|
body: notificationBody({
|
|
232337
|
-
sessionTitle: session?.title,
|
|
232614
|
+
sessionTitle: session?.title ?? opts?.sessionTitle,
|
|
232338
232615
|
branch: event.branch,
|
|
232339
232616
|
projectName: project?.name
|
|
232340
232617
|
}),
|
|
@@ -235626,8 +235903,8 @@ import { randomUUID as randomUUID15 } from "crypto";
|
|
|
235626
235903
|
|
|
235627
235904
|
// src/protocol/model-suggestions.ts
|
|
235628
235905
|
var MODEL_SUGGESTIONS = {
|
|
235629
|
-
"claude-code": ["opus", "sonnet", "haiku"],
|
|
235630
|
-
codex: ["gpt-5.6-sol", "gpt-5.6-
|
|
235906
|
+
"claude-code": ["opus", "sonnet", "haiku", "fable"],
|
|
235907
|
+
codex: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"]
|
|
235631
235908
|
};
|
|
235632
235909
|
|
|
235633
235910
|
// src/routes/agent-session-routes.ts
|
|
@@ -236449,6 +236726,54 @@ var routes12 = async (fastify2) => {
|
|
|
236449
236726
|
return reply.code(200).send({ success: true, agentType });
|
|
236450
236727
|
}
|
|
236451
236728
|
);
|
|
236729
|
+
fastify2.post(
|
|
236730
|
+
"/api/agent-sessions/:sessionId/model",
|
|
236731
|
+
async (req, reply) => {
|
|
236732
|
+
const body = req.body || {};
|
|
236733
|
+
if (body.model === void 0) {
|
|
236734
|
+
return reply.code(400).send({ error: "model is required (null for the CLI default)" });
|
|
236735
|
+
}
|
|
236736
|
+
const { model } = body;
|
|
236737
|
+
if (model !== null && typeof model !== "string") {
|
|
236738
|
+
return reply.code(400).send({ error: "model must be a string or null" });
|
|
236739
|
+
}
|
|
236740
|
+
const userId = requireAuth(req, reply);
|
|
236741
|
+
if (userId === null) return;
|
|
236742
|
+
if (req.params.sessionId.startsWith("remote-")) {
|
|
236743
|
+
const remoteInfo = await getAuthorizedRemoteSessionInfo(req.params.sessionId, userId);
|
|
236744
|
+
if (!remoteInfo) {
|
|
236745
|
+
return reply.code(404).send({ error: "Remote session not found" });
|
|
236746
|
+
}
|
|
236747
|
+
const result = await proxyAuto(
|
|
236748
|
+
remoteInfo.remoteServerId,
|
|
236749
|
+
remoteInfo.remoteUrl,
|
|
236750
|
+
remoteInfo.remoteApiKey,
|
|
236751
|
+
"POST",
|
|
236752
|
+
`/api/agent-sessions/${remoteInfo.remoteSessionId}/model`,
|
|
236753
|
+
{ model }
|
|
236754
|
+
);
|
|
236755
|
+
return reply.code(proxyStatus(result)).send(result.data);
|
|
236756
|
+
}
|
|
236757
|
+
const row = await fastify2.storage.agentSessions.getById(req.params.sessionId);
|
|
236758
|
+
if (!row || !await fastify2.storage.projects.getById(row.project_id, userId)) {
|
|
236759
|
+
return reply.code(404).send({ error: "Session not found" });
|
|
236760
|
+
}
|
|
236761
|
+
const outcome = await fastify2.agentSessionManager.setModel(req.params.sessionId, model);
|
|
236762
|
+
if (outcome === "not_found") {
|
|
236763
|
+
return reply.code(404).send({ error: "Session not found" });
|
|
236764
|
+
}
|
|
236765
|
+
if (outcome === "busy") {
|
|
236766
|
+
return reply.code(409).send({ error: "Agent is currently running \u2014 stop it before changing the model" });
|
|
236767
|
+
}
|
|
236768
|
+
if (outcome === "error") {
|
|
236769
|
+
return reply.code(500).send({ error: "Couldn't save the model \u2014 the session kept its current one" });
|
|
236770
|
+
}
|
|
236771
|
+
return reply.code(200).send({
|
|
236772
|
+
success: true,
|
|
236773
|
+
model: fastify2.agentSessionManager.getSession(req.params.sessionId)?.model ?? null
|
|
236774
|
+
});
|
|
236775
|
+
}
|
|
236776
|
+
);
|
|
236452
236777
|
fastify2.post(
|
|
236453
236778
|
"/api/agent-sessions/:sessionId/branch",
|
|
236454
236779
|
async (req, reply) => {
|
|
@@ -237578,6 +237903,7 @@ var import_fastify_plugin20 = __toESM(require_plugin2(), 1);
|
|
|
237578
237903
|
|
|
237579
237904
|
// src/utils/review-brief.ts
|
|
237580
237905
|
var BASE_TIMEOUT_MS = 15e3;
|
|
237906
|
+
var JUDGMENT_TIMEOUT_MS = 9e4;
|
|
237581
237907
|
var TIMEOUT_MS_PER_1K_CHARS = 120;
|
|
237582
237908
|
var MAX_TIMEOUT_MS = 6e4;
|
|
237583
237909
|
var PER_MESSAGE_MAX_CHARS = 6e3;
|
|
@@ -237587,11 +237913,11 @@ var RECENT_VERBATIM_RATIO = 0.3;
|
|
|
237587
237913
|
var MIN_RECENT_VERBATIM_CHARS = 2e3;
|
|
237588
237914
|
var MAX_FOLD_CALLS = 8;
|
|
237589
237915
|
var COMPACT_SUMMARY_MAX_CHARS = 6e3;
|
|
237590
|
-
var COMPACT_SUMMARY_MAX_TOKENS =
|
|
237916
|
+
var COMPACT_SUMMARY_MAX_TOKENS = 3e3;
|
|
237591
237917
|
var REVERSALS_MAX_CHARS = 2e3;
|
|
237592
|
-
var REVERSALS_MAX_TOKENS =
|
|
237918
|
+
var REVERSALS_MAX_TOKENS = 2e3;
|
|
237593
237919
|
var BRIEF_MAX_CHARS = 4e3;
|
|
237594
|
-
var BRIEF_MAX_TOKENS =
|
|
237920
|
+
var BRIEF_MAX_TOKENS = 3e3;
|
|
237595
237921
|
var REVERSAL_MIN_CHARS = 6e3;
|
|
237596
237922
|
var SEP = "\n\n";
|
|
237597
237923
|
var COMPACTION_NOTE = "_Note: the source conversation was compressed before distillation \u2014 older turns were summarized rather than read verbatim._";
|
|
@@ -237599,13 +237925,17 @@ var BRIEF_TRUNCATION_NOTE = "\n\n_[brief truncated at the length limit \u2014 tr
|
|
|
237599
237925
|
var SYSTEM_PROMPT2 = [
|
|
237600
237926
|
"You distill a coding-agent conversation into an intent brief for an independent code reviewer.",
|
|
237601
237927
|
"The reviewer will NOT see the conversation \u2014 only your brief plus the actual code, so capture what the code alone cannot show:",
|
|
237928
|
+
"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.",
|
|
237602
237929
|
"1. The original request and its goal.",
|
|
237603
|
-
"2.
|
|
237604
|
-
"3.
|
|
237605
|
-
"4.
|
|
237930
|
+
"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.",
|
|
237931
|
+
"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.",
|
|
237932
|
+
"4. Non-goals, trade-offs and limitations that were acknowledged and accepted.",
|
|
237933
|
+
"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.",
|
|
237934
|
+
"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.",
|
|
237935
|
+
"Within each heading, order items by how much they matter to the dominant question, not by when they were said.",
|
|
237606
237936
|
"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
237937
|
"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
|
|
237938
|
+
"Decisions the agent made that the user did not contradict are worth reporting, but user silence only ever yields tentative status.",
|
|
237609
237939
|
"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
237940
|
"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
237941
|
"Write concise markdown bullets under those numbered headings, under 500 words total, in the same language as the conversation. Reply with the brief only."
|
|
@@ -237623,7 +237953,7 @@ var COMPACT_SYSTEM_PROMPT = [
|
|
|
237623
237953
|
"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
237954
|
"Preserve, in this order of priority:",
|
|
237625
237955
|
"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.",
|
|
237956
|
+
"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.",
|
|
237627
237957
|
"3. Behaviour established by actually running something: observed output, exit codes, error text, which stream an error arrived on.",
|
|
237628
237958
|
"4. Trade-offs that were acknowledged and accepted.",
|
|
237629
237959
|
"Drop: exploration narrative, restated background, tool mechanics, and anything a reviewer could recover by reading the code.",
|
|
@@ -237719,8 +238049,16 @@ ${chunk}`,
|
|
|
237719
238049
|
maxOutputTokens: COMPACT_SUMMARY_MAX_TOKENS,
|
|
237720
238050
|
experimental_telemetry: telemetryFor("review-intent-brief-compact", userId)
|
|
237721
238051
|
});
|
|
237722
|
-
const
|
|
237723
|
-
if (
|
|
238052
|
+
const { text: rawText, finishReason } = result;
|
|
238053
|
+
if (finishReason === "length") {
|
|
238054
|
+
console.warn(`[ReviewBrief] slice ${part}/${total} summary hit the token cap \u2014 discarding`);
|
|
238055
|
+
return null;
|
|
238056
|
+
}
|
|
238057
|
+
const text2 = (rawText ?? "").trim();
|
|
238058
|
+
if (text2.length === 0) {
|
|
238059
|
+
console.warn(`[ReviewBrief] slice ${part}/${total} came back empty (finishReason: ${finishReason ?? "unknown"})`);
|
|
238060
|
+
return null;
|
|
238061
|
+
}
|
|
237724
238062
|
return withinLimit(text2, COMPACT_SUMMARY_MAX_CHARS, `slice ${part}/${total} summary`) ? text2 : null;
|
|
237725
238063
|
} catch (error48) {
|
|
237726
238064
|
if (isSizeRelatedFailure(error48)) throw error48;
|
|
@@ -237774,11 +238112,16 @@ async function extractReversalsWithModel(model, conversation, options = {}) {
|
|
|
237774
238112
|
prompt: `Report the reversals in this conversation:
|
|
237775
238113
|
|
|
237776
238114
|
${conversation}`,
|
|
237777
|
-
timeout:
|
|
238115
|
+
timeout: JUDGMENT_TIMEOUT_MS,
|
|
237778
238116
|
maxOutputTokens: REVERSALS_MAX_TOKENS,
|
|
237779
238117
|
experimental_telemetry: telemetryFor("review-intent-brief-reversals", options.userId)
|
|
237780
238118
|
});
|
|
237781
|
-
const
|
|
238119
|
+
const { text: rawText, finishReason } = result;
|
|
238120
|
+
if (finishReason === "length") {
|
|
238121
|
+
console.warn("[ReviewBrief] reversal list hit the token cap \u2014 discarding");
|
|
238122
|
+
return null;
|
|
238123
|
+
}
|
|
238124
|
+
const text2 = (rawText ?? "").trim();
|
|
237782
238125
|
if (text2.length === 0 || /^none\b/i.test(text2)) return null;
|
|
237783
238126
|
return withinLimit(text2, REVERSALS_MAX_CHARS, "reversal list") ? text2 : null;
|
|
237784
238127
|
} catch (error48) {
|
|
@@ -237794,12 +238137,17 @@ async function generateIntentBriefWithModel(model, conversation, options = {}) {
|
|
|
237794
238137
|
model,
|
|
237795
238138
|
system: SYSTEM_PROMPT2,
|
|
237796
238139
|
prompt: buildBriefPrompt(conversation, options.reversals ?? null),
|
|
237797
|
-
timeout:
|
|
238140
|
+
timeout: JUDGMENT_TIMEOUT_MS,
|
|
237798
238141
|
maxOutputTokens: BRIEF_MAX_TOKENS,
|
|
237799
238142
|
experimental_telemetry: telemetryFor("review-intent-brief", options.userId)
|
|
237800
238143
|
});
|
|
237801
|
-
const
|
|
237802
|
-
|
|
238144
|
+
const { text: rawText, finishReason } = result;
|
|
238145
|
+
const text2 = (rawText ?? "").trim();
|
|
238146
|
+
if (text2.length === 0) {
|
|
238147
|
+
console.warn(`[ReviewBrief] model returned empty text (finishReason: ${finishReason ?? "unknown"})`);
|
|
238148
|
+
return null;
|
|
238149
|
+
}
|
|
238150
|
+
if (finishReason === "length") return text2.slice(0, BRIEF_MAX_CHARS) + BRIEF_TRUNCATION_NOTE;
|
|
237803
238151
|
return text2.length <= BRIEF_MAX_CHARS ? text2 : text2.slice(0, BRIEF_MAX_CHARS) + BRIEF_TRUNCATION_NOTE;
|
|
237804
238152
|
} catch (error48) {
|
|
237805
238153
|
if (options.rethrowSizeFailures && isSizeRelatedFailure(error48)) throw error48;
|
|
@@ -237813,13 +238161,17 @@ async function distill(model, conversation, userId, rethrowSizeFailures) {
|
|
|
237813
238161
|
}
|
|
237814
238162
|
async function generateIntentBrief(storage, userId, messages) {
|
|
237815
238163
|
try {
|
|
237816
|
-
|
|
238164
|
+
const config2 = await getChatProviderConfig(storage, userId);
|
|
238165
|
+
const mainOk = isModelConfigured(config2, config2.main);
|
|
238166
|
+
const fastOk = isModelConfigured(config2, config2.fast);
|
|
238167
|
+
if (!mainOk && !fastOk) return null;
|
|
237817
238168
|
const conversation = serializeConversationForBrief(messages);
|
|
237818
238169
|
if (!conversation) return null;
|
|
237819
|
-
const
|
|
238170
|
+
const distillModel = mainOk ? await resolveChatModel(storage, userId) : await resolveFastChatModel(storage, userId);
|
|
238171
|
+
const compactModel = fastOk ? await resolveFastChatModel(storage, userId) : distillModel;
|
|
237820
238172
|
if (conversation.length <= COMPACT_TRIGGER_CHARS) {
|
|
237821
238173
|
try {
|
|
237822
|
-
const brief = await distill(
|
|
238174
|
+
const brief = await distill(distillModel, conversation, userId, true);
|
|
237823
238175
|
return brief === null ? null : appendCompactionNote(brief, false);
|
|
237824
238176
|
} catch (error48) {
|
|
237825
238177
|
if (!isSizeRelatedFailure(error48)) throw error48;
|
|
@@ -237828,9 +238180,9 @@ async function generateIntentBrief(storage, userId, messages) {
|
|
|
237828
238180
|
}
|
|
237829
238181
|
for (const sliceChars of COMPACT_SLICE_BUDGETS) {
|
|
237830
238182
|
try {
|
|
237831
|
-
const compacted = await compactConversation(
|
|
238183
|
+
const compacted = await compactConversation(compactModel, messages, { userId, sliceChars });
|
|
237832
238184
|
if (compacted === null) return null;
|
|
237833
|
-
const brief = await distill(
|
|
238185
|
+
const brief = await distill(distillModel, compacted, userId, true);
|
|
237834
238186
|
return brief === null ? null : appendCompactionNote(brief, true);
|
|
237835
238187
|
} catch (error48) {
|
|
237836
238188
|
if (!isSizeRelatedFailure(error48)) throw error48;
|
|
@@ -238229,7 +238581,11 @@ async function routes19(fastify2) {
|
|
|
238229
238581
|
const run2 = await fastify2.workflowEngine.cancelRun(req.params.id);
|
|
238230
238582
|
return reply.send({ run: run2 });
|
|
238231
238583
|
}
|
|
238232
|
-
|
|
238584
|
+
if (action === "finalize") {
|
|
238585
|
+
const run2 = await fastify2.workflowEngine.requestFinalVerdict(req.params.id);
|
|
238586
|
+
return reply.send({ run: run2 });
|
|
238587
|
+
}
|
|
238588
|
+
return reply.code(400).send({ error: "action must be approve, cancel or finalize" });
|
|
238233
238589
|
} catch (err) {
|
|
238234
238590
|
const status = errStatus(err);
|
|
238235
238591
|
if (status) return reply.code(status).send({ error: err.message });
|