@vibedeckx/darwin-arm64 0.2.15 → 0.2.16
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 +1360 -187
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -13356,12 +13356,12 @@ var require_semver = __commonJS({
|
|
|
13356
13356
|
}
|
|
13357
13357
|
function replaceHyphen(comp, options) {
|
|
13358
13358
|
const r = HYPHENRANGE_REGEXP;
|
|
13359
|
-
return comp.replace(r, (_3, from, fM, fm,
|
|
13359
|
+
return comp.replace(r, (_3, from, fM, fm, fp34, fpr, fb, to, tM, tm, tp, tpr) => {
|
|
13360
13360
|
if (isX(fM)) {
|
|
13361
13361
|
from = "";
|
|
13362
13362
|
} else if (isX(fm)) {
|
|
13363
13363
|
from = `>=${fM}.0.0${options?.includePrerelease ? "-0" : ""}`;
|
|
13364
|
-
} else if (isX(
|
|
13364
|
+
} else if (isX(fp34)) {
|
|
13365
13365
|
from = `>=${fM}.${fm}.0${options?.includePrerelease ? "-0" : ""}`;
|
|
13366
13366
|
} else if (fpr) {
|
|
13367
13367
|
from = `>=${from}`;
|
|
@@ -87525,12 +87525,12 @@ var require_range = __commonJS({
|
|
|
87525
87525
|
debug("replaceGTE0", comp, options);
|
|
87526
87526
|
return comp.trim().replace(re2[options.includePrerelease ? t.GTE0PRE : t.GTE0], "");
|
|
87527
87527
|
};
|
|
87528
|
-
var hyphenReplace = (incPr) => ($0, from, fM, fm,
|
|
87528
|
+
var hyphenReplace = (incPr) => ($0, from, fM, fm, fp34, fpr, fb, to, tM, tm, tp, tpr) => {
|
|
87529
87529
|
if (isX(fM)) {
|
|
87530
87530
|
from = "";
|
|
87531
87531
|
} else if (isX(fm)) {
|
|
87532
87532
|
from = `>=${fM}.0.0${incPr ? "-0" : ""}`;
|
|
87533
|
-
} else if (isX(
|
|
87533
|
+
} else if (isX(fp34)) {
|
|
87534
87534
|
from = `>=${fM}.${fm}.0${incPr ? "-0" : ""}`;
|
|
87535
87535
|
} else if (fpr) {
|
|
87536
87536
|
from = `>=${from}`;
|
|
@@ -90111,8 +90111,8 @@ var require_pretty_print = __commonJS({
|
|
|
90111
90111
|
serializedRoute += serializeMetaData(route.metaData);
|
|
90112
90112
|
return serializedRoute;
|
|
90113
90113
|
}
|
|
90114
|
-
function mergeSimilarRoutes(
|
|
90115
|
-
return
|
|
90114
|
+
function mergeSimilarRoutes(routes33) {
|
|
90115
|
+
return routes33.reduce((mergedRoutes, route) => {
|
|
90116
90116
|
for (const nodeRoute of mergedRoutes) {
|
|
90117
90117
|
if (deepEqual(route.opts.constraints, nodeRoute.opts.constraints) && deepEqual(route.metaData, nodeRoute.metaData)) {
|
|
90118
90118
|
nodeRoute.method += ", " + route.method;
|
|
@@ -90124,18 +90124,18 @@ var require_pretty_print = __commonJS({
|
|
|
90124
90124
|
}, []);
|
|
90125
90125
|
}
|
|
90126
90126
|
function serializeNode(node, prefix, options) {
|
|
90127
|
-
let
|
|
90127
|
+
let routes33 = node.routes;
|
|
90128
90128
|
if (options.method === void 0) {
|
|
90129
|
-
|
|
90129
|
+
routes33 = routes33.map(normalizeRoute);
|
|
90130
90130
|
}
|
|
90131
|
-
|
|
90131
|
+
routes33 = routes33.map((route) => {
|
|
90132
90132
|
route.metaData = getRouteMetaData(route, options);
|
|
90133
90133
|
return route;
|
|
90134
90134
|
});
|
|
90135
90135
|
if (options.method === void 0) {
|
|
90136
|
-
|
|
90136
|
+
routes33 = mergeSimilarRoutes(routes33);
|
|
90137
90137
|
}
|
|
90138
|
-
return
|
|
90138
|
+
return routes33.map(serializeRoute).join(`
|
|
90139
90139
|
${prefix}`);
|
|
90140
90140
|
}
|
|
90141
90141
|
function buildObjectTree(node, tree, prefix, options) {
|
|
@@ -91402,9 +91402,9 @@ var require_find_my_way = __commonJS({
|
|
|
91402
91402
|
pathIndex = paramEndIndex;
|
|
91403
91403
|
}
|
|
91404
91404
|
};
|
|
91405
|
-
Router.prototype._rebuild = function(
|
|
91405
|
+
Router.prototype._rebuild = function(routes33) {
|
|
91406
91406
|
this.reset();
|
|
91407
|
-
for (const route of
|
|
91407
|
+
for (const route of routes33) {
|
|
91408
91408
|
const { method, path: path22, opts, handler, store } = route;
|
|
91409
91409
|
this._on(method, path22, opts, handler, store);
|
|
91410
91410
|
}
|
|
@@ -100612,8 +100612,8 @@ var require_commonjs6 = __commonJS({
|
|
|
100612
100612
|
return this.#fullpath = this.name;
|
|
100613
100613
|
}
|
|
100614
100614
|
const pv = p2.fullpath();
|
|
100615
|
-
const
|
|
100616
|
-
return this.#fullpath =
|
|
100615
|
+
const fp34 = pv + (!p2.parent ? "" : this.sep) + name25;
|
|
100616
|
+
return this.#fullpath = fp34;
|
|
100617
100617
|
}
|
|
100618
100618
|
/**
|
|
100619
100619
|
* On platforms other than windows, this is identical to fullpath.
|
|
@@ -105329,7 +105329,7 @@ var require_static = __commonJS({
|
|
|
105329
105329
|
var { fileURLToPath: fileURLToPath4 } = __require("node:url");
|
|
105330
105330
|
var { statSync: statSync2 } = __require("node:fs");
|
|
105331
105331
|
var { glob } = require_commonjs7();
|
|
105332
|
-
var
|
|
105332
|
+
var fp34 = require_plugin2();
|
|
105333
105333
|
var send = require_send2();
|
|
105334
105334
|
var encodingNegotiator = require_accept_negotiator();
|
|
105335
105335
|
var contentDisposition = require_content_disposition();
|
|
@@ -105433,7 +105433,7 @@ var require_static = __commonJS({
|
|
|
105433
105433
|
} else {
|
|
105434
105434
|
const indexes = new Set(opts.index === void 0 ? ["index.html"] : [].concat(opts.index));
|
|
105435
105435
|
const indexDirs = /* @__PURE__ */ new Map();
|
|
105436
|
-
const
|
|
105436
|
+
const routes33 = /* @__PURE__ */ new Set();
|
|
105437
105437
|
const roots = Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root];
|
|
105438
105438
|
for (let rootPath of roots) {
|
|
105439
105439
|
rootPath = rootPath.split(path22.win32.sep).join(path22.posix.sep);
|
|
@@ -105449,10 +105449,10 @@ var require_static = __commonJS({
|
|
|
105449
105449
|
for (let file2 of files) {
|
|
105450
105450
|
file2 = file2.split(path22.win32.sep).join(path22.posix.sep);
|
|
105451
105451
|
const route = prefix + file2;
|
|
105452
|
-
if (
|
|
105452
|
+
if (routes33.has(route)) {
|
|
105453
105453
|
continue;
|
|
105454
105454
|
}
|
|
105455
|
-
|
|
105455
|
+
routes33.add(route);
|
|
105456
105456
|
setUpHeadAndGet(routeOpts, route, `/${file2}`, rootPath);
|
|
105457
105457
|
const key = path22.posix.basename(route);
|
|
105458
105458
|
if (indexes.has(key) && !indexDirs.has(key)) {
|
|
@@ -105747,7 +105747,7 @@ var require_static = __commonJS({
|
|
|
105747
105747
|
throw err;
|
|
105748
105748
|
}
|
|
105749
105749
|
}
|
|
105750
|
-
module.exports =
|
|
105750
|
+
module.exports = fp34(fastifyStatic2, {
|
|
105751
105751
|
fastify: "5.x",
|
|
105752
105752
|
name: "@fastify/static"
|
|
105753
105753
|
});
|
|
@@ -112390,7 +112390,7 @@ var require_websocket2 = __commonJS({
|
|
|
112390
112390
|
var { ServerResponse } = __require("node:http");
|
|
112391
112391
|
var { PassThrough } = __require("node:stream");
|
|
112392
112392
|
var { randomBytes: randomBytes3 } = __require("node:crypto");
|
|
112393
|
-
var
|
|
112393
|
+
var fp34 = require_plugin2();
|
|
112394
112394
|
var WebSocket2 = require_ws();
|
|
112395
112395
|
var Duplexify = require_duplexify();
|
|
112396
112396
|
var kWs = /* @__PURE__ */ Symbol("ws-socket");
|
|
@@ -112576,7 +112576,7 @@ var require_websocket2 = __commonJS({
|
|
|
112576
112576
|
}
|
|
112577
112577
|
next();
|
|
112578
112578
|
}
|
|
112579
|
-
module.exports =
|
|
112579
|
+
module.exports = fp34(fastifyWebsocket2, {
|
|
112580
112580
|
fastify: "5.x",
|
|
112581
112581
|
name: "@fastify/websocket"
|
|
112582
112582
|
});
|
|
@@ -115020,7 +115020,7 @@ var require_multipart2 = __commonJS({
|
|
|
115020
115020
|
"use strict";
|
|
115021
115021
|
var Busboy = require_main();
|
|
115022
115022
|
var os2 = __require("node:os");
|
|
115023
|
-
var
|
|
115023
|
+
var fp34 = require_plugin2();
|
|
115024
115024
|
var { createWriteStream: createWriteStream2 } = __require("node:fs");
|
|
115025
115025
|
var { unlink: unlink2 } = __require("node:fs/promises");
|
|
115026
115026
|
var path22 = __require("node:path");
|
|
@@ -115469,7 +115469,7 @@ var require_multipart2 = __commonJS({
|
|
|
115469
115469
|
}
|
|
115470
115470
|
});
|
|
115471
115471
|
}
|
|
115472
|
-
module.exports =
|
|
115472
|
+
module.exports = fp34(fastifyMultipart2, {
|
|
115473
115473
|
fastify: "5.x",
|
|
115474
115474
|
name: "@fastify/multipart"
|
|
115475
115475
|
});
|
|
@@ -169019,11 +169019,11 @@ function buildCommand(builderArgs) {
|
|
|
169019
169019
|
}
|
|
169020
169020
|
};
|
|
169021
169021
|
}
|
|
169022
|
-
function* generateRouteMapHelpLines(
|
|
169022
|
+
function* generateRouteMapHelpLines(routes33, docs, args) {
|
|
169023
169023
|
const { brief, fullDescription, hideRoute } = docs;
|
|
169024
169024
|
const { headers } = args.text;
|
|
169025
169025
|
yield args.ansiColor ? `\x1B[1m${headers.usage}\x1B[22m` : headers.usage;
|
|
169026
|
-
for (const [name25, route] of Object.entries(
|
|
169026
|
+
for (const [name25, route] of Object.entries(routes33)) {
|
|
169027
169027
|
if (!hideRoute || !hideRoute[name25] || args.includeHidden) {
|
|
169028
169028
|
const externalRouteName = args.config.caseStyle === "convert-camel-to-kebab" ? convertCamelCaseToKebabCase(name25) : name25;
|
|
169029
169029
|
yield ` ${route.formatUsageLine({
|
|
@@ -169053,7 +169053,7 @@ function* generateRouteMapHelpLines(routes31, docs, args) {
|
|
|
169053
169053
|
}
|
|
169054
169054
|
yield "";
|
|
169055
169055
|
yield args.ansiColor ? `\x1B[1m${headers.commands}\x1B[22m` : headers.commands;
|
|
169056
|
-
const visibleRoutes = Object.entries(
|
|
169056
|
+
const visibleRoutes = Object.entries(routes33).filter(
|
|
169057
169057
|
([name25]) => !hideRoute || !hideRoute[name25] || args.includeHidden
|
|
169058
169058
|
);
|
|
169059
169059
|
const rows = visibleRoutes.map(([internalRouteName, route]) => {
|
|
@@ -169081,24 +169081,24 @@ function* generateRouteMapHelpLines(routes31, docs, args) {
|
|
|
169081
169081
|
}
|
|
169082
169082
|
}
|
|
169083
169083
|
function buildRouteMap({
|
|
169084
|
-
routes:
|
|
169084
|
+
routes: routes33,
|
|
169085
169085
|
defaultCommand: defaultCommandRoute,
|
|
169086
169086
|
docs,
|
|
169087
169087
|
aliases
|
|
169088
169088
|
}) {
|
|
169089
|
-
if (Object.entries(
|
|
169089
|
+
if (Object.entries(routes33).length === 0) {
|
|
169090
169090
|
throw new InternalError("Route map must contain at least one route");
|
|
169091
169091
|
}
|
|
169092
169092
|
const activeAliases = aliases ?? {};
|
|
169093
169093
|
const aliasesByRoute = /* @__PURE__ */ new Map();
|
|
169094
169094
|
for (const [alias, routeName] of Object.entries(activeAliases)) {
|
|
169095
|
-
if (alias in
|
|
169095
|
+
if (alias in routes33) {
|
|
169096
169096
|
throw new InternalError(`Cannot use "${alias}" as an alias when a route with that name already exists`);
|
|
169097
169097
|
}
|
|
169098
169098
|
const routeAliases = aliasesByRoute.get(routeName) ?? [];
|
|
169099
169099
|
aliasesByRoute.set(routeName, [...routeAliases, alias]);
|
|
169100
169100
|
}
|
|
169101
|
-
const defaultCommand = defaultCommandRoute ?
|
|
169101
|
+
const defaultCommand = defaultCommandRoute ? routes33[defaultCommandRoute] : void 0;
|
|
169102
169102
|
if (defaultCommand && defaultCommand.kind === RouteMapSymbol) {
|
|
169103
169103
|
throw new InternalError(
|
|
169104
169104
|
`Cannot use "${defaultCommandRoute}" as the default command because it is not a Command`
|
|
@@ -169107,7 +169107,7 @@ function buildRouteMap({
|
|
|
169107
169107
|
const resolveRouteName = (input) => {
|
|
169108
169108
|
if (input in activeAliases) {
|
|
169109
169109
|
return activeAliases[input];
|
|
169110
|
-
} else if (input in
|
|
169110
|
+
} else if (input in routes33) {
|
|
169111
169111
|
return input;
|
|
169112
169112
|
}
|
|
169113
169113
|
};
|
|
@@ -169125,7 +169125,7 @@ function buildRouteMap({
|
|
|
169125
169125
|
return `${args.prefix.join(" ")} ${routeNames.join("|")} ...`;
|
|
169126
169126
|
},
|
|
169127
169127
|
formatHelp: (config2) => {
|
|
169128
|
-
const lines = [...generateRouteMapHelpLines(
|
|
169128
|
+
const lines = [...generateRouteMapHelpLines(routes33, docs, config2)];
|
|
169129
169129
|
const text2 = lines.join("\n");
|
|
169130
169130
|
return text2 + "\n";
|
|
169131
169131
|
},
|
|
@@ -169168,11 +169168,11 @@ function buildRouteMap({
|
|
|
169168
169168
|
},
|
|
169169
169169
|
getRoutingTargetForInput: (input) => {
|
|
169170
169170
|
const routeName = input in activeAliases ? activeAliases[input] : input;
|
|
169171
|
-
return
|
|
169171
|
+
return routes33[routeName];
|
|
169172
169172
|
},
|
|
169173
169173
|
getAllEntries() {
|
|
169174
169174
|
const hiddenRoutes = docs.hideRoute;
|
|
169175
|
-
return Object.entries(
|
|
169175
|
+
return Object.entries(routes33).map(([originalRouteName, target]) => {
|
|
169176
169176
|
return {
|
|
169177
169177
|
name: {
|
|
169178
169178
|
original: originalRouteName,
|
|
@@ -185055,6 +185055,14 @@ var createCoreRepos = (kdb, h) => ({
|
|
|
185055
185055
|
let query = kdb.deleteFrom("projects").where("id", "=", id);
|
|
185056
185056
|
if (userId) query = query.where("user_id", "=", userId);
|
|
185057
185057
|
await query.execute();
|
|
185058
|
+
},
|
|
185059
|
+
// Deliberately unscoped: the notification importer runs without a request
|
|
185060
|
+
// context and must DERIVE ownership from the mapped local project rather
|
|
185061
|
+
// than trust any worker-supplied tenant id. Returns "" for solo-mode rows
|
|
185062
|
+
// (the column's default), which callers map to the "local" sentinel.
|
|
185063
|
+
getOwnerId: async (projectId) => {
|
|
185064
|
+
const row = await kdb.selectFrom("projects").select("user_id").where("id", "=", projectId).executeTakeFirst();
|
|
185065
|
+
return row?.user_id;
|
|
185058
185066
|
}
|
|
185059
185067
|
},
|
|
185060
185068
|
settings: {
|
|
@@ -185584,6 +185592,15 @@ var createExecutorRepos = (kdb, h) => ({
|
|
|
185584
185592
|
});
|
|
185585
185593
|
|
|
185586
185594
|
// src/storage/repositories/agent-sessions.ts
|
|
185595
|
+
var mapRemoteSessionMapping = (row) => ({
|
|
185596
|
+
local_session_id: row.local_session_id,
|
|
185597
|
+
project_id: row.project_id,
|
|
185598
|
+
remote_server_id: row.remote_server_id,
|
|
185599
|
+
remote_session_id: row.remote_session_id,
|
|
185600
|
+
branch: row.branch,
|
|
185601
|
+
notification_sync_start: row.notification_sync_start,
|
|
185602
|
+
notification_watch_until: row.notification_watch_until
|
|
185603
|
+
});
|
|
185587
185604
|
var mapAgentSession = (row) => ({
|
|
185588
185605
|
id: row.id,
|
|
185589
185606
|
project_id: row.project_id,
|
|
@@ -185690,6 +185707,17 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
185690
185707
|
upsertEntry: async (sessionId, entryIndex, data) => {
|
|
185691
185708
|
await kdb.insertInto("agent_session_entries").values({ session_id: sessionId, entry_index: entryIndex, data }).onConflict((oc) => oc.columns(["session_id", "entry_index"]).doUpdateSet({ data })).execute();
|
|
185692
185709
|
},
|
|
185710
|
+
// One transaction, both writes idempotent — see the contract note in
|
|
185711
|
+
// types.ts for why the milestone must ride with the turn_end rather than
|
|
185712
|
+
// being written next to it.
|
|
185713
|
+
upsertTurnEndWithOutbox: async ({ sessionId, entryIndex, entryData, outbox }) => {
|
|
185714
|
+
await kdb.transaction().execute(async (trx) => {
|
|
185715
|
+
await trx.insertInto("agent_session_entries").values({ session_id: sessionId, entry_index: entryIndex, data: entryData }).onConflict((oc) => oc.columns(["session_id", "entry_index"]).doUpdateSet({ data: entryData })).execute();
|
|
185716
|
+
if (outbox) {
|
|
185717
|
+
await trx.insertInto("notification_outbox").values(outbox).onConflict((oc) => oc.column("id").doNothing()).execute();
|
|
185718
|
+
}
|
|
185719
|
+
});
|
|
185720
|
+
},
|
|
185693
185721
|
getEntries: async (sessionId) => {
|
|
185694
185722
|
return kdb.selectFrom("agent_session_entries").select(["entry_index", "data"]).where("session_id", "=", sessionId).orderBy("entry_index", "asc").execute();
|
|
185695
185723
|
},
|
|
@@ -185703,14 +185731,21 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
185703
185731
|
remoteSessionMappings: {
|
|
185704
185732
|
// ON CONFLICT SET deliberately omits title_resolved — a re-upsert (e.g.
|
|
185705
185733
|
// the remote session getting re-mapped after a reconnect) must not reset
|
|
185706
|
-
// the "AI title already generated" flag back to false.
|
|
185707
|
-
|
|
185734
|
+
// the "AI title already generated" flag back to false. It omits
|
|
185735
|
+
// notification_sync_start and notification_watch_until for the same reason:
|
|
185736
|
+
// re-mapping a session (reconnect) or reusing a reviewer for a second
|
|
185737
|
+
// review must not downgrade an established from_start policy, and must not
|
|
185738
|
+
// rewind the watch window mid-flight. The sync cursor lives in its own
|
|
185739
|
+
// table and is likewise untouched — a reused reviewer continues from the
|
|
185740
|
+
// boundary it already imported instead of replaying its whole history.
|
|
185741
|
+
upsert: async (localSessionId, projectId, remoteServerId, remoteSessionId, branch, notificationSyncStart) => {
|
|
185708
185742
|
await kdb.insertInto("remote_session_mappings").values({
|
|
185709
185743
|
local_session_id: localSessionId,
|
|
185710
185744
|
project_id: projectId,
|
|
185711
185745
|
remote_server_id: remoteServerId,
|
|
185712
185746
|
remote_session_id: remoteSessionId,
|
|
185713
|
-
branch
|
|
185747
|
+
branch,
|
|
185748
|
+
notification_sync_start: notificationSyncStart ?? "from_now"
|
|
185714
185749
|
}).onConflict((oc) => oc.column("local_session_id").doUpdateSet({
|
|
185715
185750
|
project_id: projectId,
|
|
185716
185751
|
remote_server_id: remoteServerId,
|
|
@@ -185719,10 +185754,43 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
185719
185754
|
})).execute();
|
|
185720
185755
|
},
|
|
185721
185756
|
getAll: async () => {
|
|
185722
|
-
|
|
185757
|
+
const rows = await kdb.selectFrom("remote_session_mappings").selectAll().execute();
|
|
185758
|
+
return rows.map(mapRemoteSessionMapping);
|
|
185759
|
+
},
|
|
185760
|
+
getByLocal: async (localSessionId) => {
|
|
185761
|
+
const row = await kdb.selectFrom("remote_session_mappings").selectAll().where("local_session_id", "=", localSessionId).executeTakeFirst();
|
|
185762
|
+
return row ? mapRemoteSessionMapping(row) : void 0;
|
|
185763
|
+
},
|
|
185764
|
+
getByRemote: async (remoteServerId, remoteSessionId) => {
|
|
185765
|
+
const row = await kdb.selectFrom("remote_session_mappings").selectAll().where("remote_server_id", "=", remoteServerId).where("remote_session_id", "=", remoteSessionId).executeTakeFirst();
|
|
185766
|
+
return row ? mapRemoteSessionMapping(row) : void 0;
|
|
185767
|
+
},
|
|
185768
|
+
// MAX(existing, incoming): concurrent extenders (a new turn plus live
|
|
185769
|
+
// stream activity) must not shorten a longer window one of them already set.
|
|
185770
|
+
extendNotificationWatch: async (localSessionId, until) => {
|
|
185771
|
+
await kdb.updateTable("remote_session_mappings").set({
|
|
185772
|
+
notification_watch_until: sql`MAX(COALESCE(notification_watch_until, 0), ${until})`
|
|
185773
|
+
}).where("local_session_id", "=", localSessionId).execute();
|
|
185723
185774
|
},
|
|
185775
|
+
getNotificationSyncCandidates: async ({ now: now2, includeExpired }) => {
|
|
185776
|
+
let query = kdb.selectFrom("remote_session_mappings").selectAll();
|
|
185777
|
+
if (!includeExpired) {
|
|
185778
|
+
query = query.where("notification_watch_until", ">", now2);
|
|
185779
|
+
}
|
|
185780
|
+
const rows = await query.orderBy("local_session_id", "asc").execute();
|
|
185781
|
+
return rows.map(mapRemoteSessionMapping);
|
|
185782
|
+
},
|
|
185783
|
+
// The cursor is deleted with the mapping: without a mapping the front has
|
|
185784
|
+
// no local target for the worker's events, so a stale cursor would only
|
|
185785
|
+
// suppress a legitimate re-mapping's from_start replay.
|
|
185724
185786
|
delete: async (localSessionId) => {
|
|
185725
|
-
await kdb.
|
|
185787
|
+
await kdb.transaction().execute(async (trx) => {
|
|
185788
|
+
const row = await trx.selectFrom("remote_session_mappings").select(["remote_server_id", "remote_session_id"]).where("local_session_id", "=", localSessionId).executeTakeFirst();
|
|
185789
|
+
await trx.deleteFrom("remote_session_mappings").where("local_session_id", "=", localSessionId).execute();
|
|
185790
|
+
if (row) {
|
|
185791
|
+
await trx.deleteFrom("notification_sync_cursors").where("remote_server_id", "=", row.remote_server_id).where("remote_session_id", "=", row.remote_session_id).execute();
|
|
185792
|
+
}
|
|
185793
|
+
});
|
|
185726
185794
|
},
|
|
185727
185795
|
isTitleResolved: async (localSessionId) => {
|
|
185728
185796
|
const row = await kdb.selectFrom("remote_session_mappings").select("title_resolved").where("local_session_id", "=", localSessionId).executeTakeFirst();
|
|
@@ -186248,6 +186316,18 @@ var createWorkflowRunRepos = (kdb) => ({
|
|
|
186248
186316
|
transition: async (id, from, to, patch) => {
|
|
186249
186317
|
const result = await kdb.updateTable("workflow_runs").set({ ...patch ?? {}, status: to, updated_at: sql`datetime('now')` }).where("id", "=", id).where("status", "=", from).executeTakeFirst();
|
|
186250
186318
|
return (result.numUpdatedRows ?? 0n) > 0n;
|
|
186319
|
+
},
|
|
186320
|
+
// Guarded update FIRST, then the outbox insert conditioned on it having
|
|
186321
|
+
// applied: that ordering is what makes a lost CAS silent. Both statements
|
|
186322
|
+
// share one transaction so a crash between them can't leave a notified
|
|
186323
|
+
// transition unpersisted (or vice versa).
|
|
186324
|
+
transitionWithOutbox: async (id, from, to, patch, outbox) => {
|
|
186325
|
+
return kdb.transaction().execute(async (trx) => {
|
|
186326
|
+
const result = await trx.updateTable("workflow_runs").set({ ...patch ?? {}, status: to, updated_at: sql`datetime('now')` }).where("id", "=", id).where("status", "=", from).executeTakeFirst();
|
|
186327
|
+
if ((result.numUpdatedRows ?? 0n) === 0n) return false;
|
|
186328
|
+
await trx.insertInto("notification_outbox").values(outbox).onConflict((oc) => oc.column("id").doNothing()).execute();
|
|
186329
|
+
return true;
|
|
186330
|
+
});
|
|
186251
186331
|
}
|
|
186252
186332
|
}
|
|
186253
186333
|
});
|
|
@@ -186277,6 +186357,157 @@ var createTurnSnapshotRepos = (kdb) => ({
|
|
|
186277
186357
|
}
|
|
186278
186358
|
});
|
|
186279
186359
|
|
|
186360
|
+
// src/storage/repositories/notifications.ts
|
|
186361
|
+
var mapOutbox = (row) => ({
|
|
186362
|
+
seq: row.seq,
|
|
186363
|
+
id: row.id,
|
|
186364
|
+
kind: row.kind,
|
|
186365
|
+
project_id: row.project_id,
|
|
186366
|
+
branch: row.branch,
|
|
186367
|
+
session_id: row.session_id,
|
|
186368
|
+
workflow_run_id: row.workflow_run_id,
|
|
186369
|
+
created_at: row.created_at
|
|
186370
|
+
});
|
|
186371
|
+
var mapNotification = (row) => ({
|
|
186372
|
+
id: row.id,
|
|
186373
|
+
user_id: row.user_id,
|
|
186374
|
+
kind: row.kind,
|
|
186375
|
+
project_id: row.project_id,
|
|
186376
|
+
branch: row.branch,
|
|
186377
|
+
session_id: row.session_id,
|
|
186378
|
+
workflow_run_id: row.workflow_run_id,
|
|
186379
|
+
title: row.title,
|
|
186380
|
+
body: row.body,
|
|
186381
|
+
created_at: row.created_at,
|
|
186382
|
+
read_at: row.read_at
|
|
186383
|
+
});
|
|
186384
|
+
var createNotificationRepos = (kdb) => ({
|
|
186385
|
+
notificationOutbox: {
|
|
186386
|
+
// ON CONFLICT(id) DO NOTHING is what makes retrying the *business*
|
|
186387
|
+
// transition safe: the milestone id is derived from durable state
|
|
186388
|
+
// (session + turn_end index, or workflow run id), so a retried transaction
|
|
186389
|
+
// re-derives the same id and the second insert is a no-op rather than a
|
|
186390
|
+
// second ding. `inserted: false` still returns the existing seq so callers
|
|
186391
|
+
// that want to trigger a drain can do so either way.
|
|
186392
|
+
insert: async (event) => {
|
|
186393
|
+
const result = await kdb.insertInto("notification_outbox").values(event).onConflict((oc) => oc.column("id").doNothing()).executeTakeFirst();
|
|
186394
|
+
const inserted = (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
186395
|
+
const row = await kdb.selectFrom("notification_outbox").select("seq").where("id", "=", event.id).executeTakeFirst();
|
|
186396
|
+
return { inserted, seq: row?.seq ?? null };
|
|
186397
|
+
},
|
|
186398
|
+
listAfter: async (afterSeq, limit) => {
|
|
186399
|
+
const rows = await kdb.selectFrom("notification_outbox").selectAll().where("seq", ">", afterSeq).orderBy("seq", "asc").limit(limit).execute();
|
|
186400
|
+
return rows.map(mapOutbox);
|
|
186401
|
+
},
|
|
186402
|
+
listBySessionAfter: async (sessionId, afterSeq, limit) => {
|
|
186403
|
+
const rows = await kdb.selectFrom("notification_outbox").selectAll().where("session_id", "=", sessionId).where("seq", ">", afterSeq).orderBy("seq", "asc").limit(limit).execute();
|
|
186404
|
+
return rows.map(mapOutbox);
|
|
186405
|
+
},
|
|
186406
|
+
headBySession: async (sessionId) => {
|
|
186407
|
+
const row = await kdb.selectFrom("notification_outbox").select(kdb.fn.max("seq").as("head")).where("session_id", "=", sessionId).executeTakeFirst();
|
|
186408
|
+
return row?.head ?? 0;
|
|
186409
|
+
},
|
|
186410
|
+
head: async () => {
|
|
186411
|
+
const row = await kdb.selectFrom("notification_outbox").select(kdb.fn.max("seq").as("head")).executeTakeFirst();
|
|
186412
|
+
return row?.head ?? 0;
|
|
186413
|
+
},
|
|
186414
|
+
pruneOlderThan: async (cutoffMs) => {
|
|
186415
|
+
await kdb.deleteFrom("notification_outbox").where("created_at", "<", cutoffMs).execute();
|
|
186416
|
+
}
|
|
186417
|
+
},
|
|
186418
|
+
notifications: {
|
|
186419
|
+
insert: async (notification) => {
|
|
186420
|
+
const result = await kdb.insertInto("notifications").values(notification).onConflict((oc) => oc.column("id").doNothing()).executeTakeFirst();
|
|
186421
|
+
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
186422
|
+
},
|
|
186423
|
+
// One transaction on purpose. Insert-then-crash-then-replay is safe (the id
|
|
186424
|
+
// unique constraint absorbs it), but cursor-advance-then-crash would lose
|
|
186425
|
+
// the notification permanently — so the two writes must commit together.
|
|
186426
|
+
importRemote: async ({ notification, remoteServerId, remoteSessionId, seq }) => {
|
|
186427
|
+
return kdb.transaction().execute(async (trx) => {
|
|
186428
|
+
const result = await trx.insertInto("notifications").values(notification).onConflict((oc) => oc.column("id").doNothing()).executeTakeFirst();
|
|
186429
|
+
await trx.insertInto("notification_sync_cursors").values({
|
|
186430
|
+
remote_server_id: remoteServerId,
|
|
186431
|
+
remote_session_id: remoteSessionId,
|
|
186432
|
+
last_seq: seq,
|
|
186433
|
+
updated_at: Date.now()
|
|
186434
|
+
}).onConflict((oc) => oc.columns(["remote_server_id", "remote_session_id"]).doUpdateSet({
|
|
186435
|
+
// MAX(existing, incoming) — pages can be retried out of order after
|
|
186436
|
+
// a transient remote error, and a cursor that moved backward would
|
|
186437
|
+
// re-import (and re-ding) everything after it.
|
|
186438
|
+
last_seq: sql`MAX(notification_sync_cursors.last_seq, excluded.last_seq)`,
|
|
186439
|
+
updated_at: Date.now()
|
|
186440
|
+
})).execute();
|
|
186441
|
+
return { inserted: (result.numInsertedOrUpdatedRows ?? 0n) > 0n };
|
|
186442
|
+
});
|
|
186443
|
+
},
|
|
186444
|
+
listForUser: async (userId, opts) => {
|
|
186445
|
+
let query = kdb.selectFrom("notifications").selectAll().where("user_id", "=", userId);
|
|
186446
|
+
if (opts.unreadOnly) query = query.where("read_at", "is", null);
|
|
186447
|
+
const rows = await query.orderBy("created_at", "desc").orderBy("id", "desc").limit(opts.limit).execute();
|
|
186448
|
+
return rows.map(mapNotification);
|
|
186449
|
+
},
|
|
186450
|
+
// The user_id predicate is load-bearing authorization, not a filter: a
|
|
186451
|
+
// caller must not be able to mutate — or learn the existence of — another
|
|
186452
|
+
// tenant's notification by guessing its (deterministic, guessable) id.
|
|
186453
|
+
markRead: async (id, userId) => {
|
|
186454
|
+
const result = await kdb.updateTable("notifications").set({ read_at: Date.now() }).where("id", "=", id).where("user_id", "=", userId).where("read_at", "is", null).executeTakeFirst();
|
|
186455
|
+
if ((result.numUpdatedRows ?? 0n) > 0n) return true;
|
|
186456
|
+
const existing = await kdb.selectFrom("notifications").select("id").where("id", "=", id).where("user_id", "=", userId).executeTakeFirst();
|
|
186457
|
+
return existing !== void 0;
|
|
186458
|
+
},
|
|
186459
|
+
markAllRead: async (userId) => {
|
|
186460
|
+
await kdb.updateTable("notifications").set({ read_at: Date.now() }).where("user_id", "=", userId).where("read_at", "is", null).execute();
|
|
186461
|
+
},
|
|
186462
|
+
// Unread rows are never deleted — an unread milestone is the whole point of
|
|
186463
|
+
// the feature. Read history is capped per user so it can't grow forever.
|
|
186464
|
+
cleanup: async (keepRead) => {
|
|
186465
|
+
const users = await kdb.selectFrom("notifications").select("user_id").where("read_at", "is not", null).groupBy("user_id").execute();
|
|
186466
|
+
for (const { user_id } of users) {
|
|
186467
|
+
const keep = await kdb.selectFrom("notifications").select("id").where("user_id", "=", user_id).where("read_at", "is not", null).orderBy("created_at", "desc").orderBy("id", "desc").limit(keepRead).execute();
|
|
186468
|
+
let query = kdb.deleteFrom("notifications").where("user_id", "=", user_id).where("read_at", "is not", null);
|
|
186469
|
+
if (keep.length > 0) {
|
|
186470
|
+
query = query.where("id", "not in", keep.map((r) => r.id));
|
|
186471
|
+
}
|
|
186472
|
+
await query.execute();
|
|
186473
|
+
}
|
|
186474
|
+
}
|
|
186475
|
+
},
|
|
186476
|
+
notificationSyncCursors: {
|
|
186477
|
+
get: async (remoteServerId, remoteSessionId) => {
|
|
186478
|
+
const row = await kdb.selectFrom("notification_sync_cursors").select("last_seq").where("remote_server_id", "=", remoteServerId).where("remote_session_id", "=", remoteSessionId).executeTakeFirst();
|
|
186479
|
+
return row?.last_seq;
|
|
186480
|
+
},
|
|
186481
|
+
getMany: async (remoteServerId, remoteSessionIds) => {
|
|
186482
|
+
if (remoteSessionIds.length === 0) return /* @__PURE__ */ new Map();
|
|
186483
|
+
const rows = await kdb.selectFrom("notification_sync_cursors").select(["remote_session_id", "last_seq"]).where("remote_server_id", "=", remoteServerId).where("remote_session_id", "in", remoteSessionIds).execute();
|
|
186484
|
+
return new Map(rows.map((r) => [r.remote_session_id, r.last_seq]));
|
|
186485
|
+
},
|
|
186486
|
+
set: async (remoteServerId, remoteSessionId, lastSeq) => {
|
|
186487
|
+
await kdb.insertInto("notification_sync_cursors").values({
|
|
186488
|
+
remote_server_id: remoteServerId,
|
|
186489
|
+
remote_session_id: remoteSessionId,
|
|
186490
|
+
last_seq: lastSeq,
|
|
186491
|
+
updated_at: Date.now()
|
|
186492
|
+
}).onConflict((oc) => oc.columns(["remote_server_id", "remote_session_id"]).doUpdateSet({
|
|
186493
|
+
last_seq: sql`MAX(notification_sync_cursors.last_seq, excluded.last_seq)`,
|
|
186494
|
+
updated_at: Date.now()
|
|
186495
|
+
})).execute();
|
|
186496
|
+
},
|
|
186497
|
+
// DO NOTHING, not DO UPDATE: see the contract note in types.ts for why a
|
|
186498
|
+
// baseline must never overwrite an existing cursor.
|
|
186499
|
+
initializeIfAbsent: async (remoteServerId, remoteSessionId, lastSeq) => {
|
|
186500
|
+
const result = await kdb.insertInto("notification_sync_cursors").values({
|
|
186501
|
+
remote_server_id: remoteServerId,
|
|
186502
|
+
remote_session_id: remoteSessionId,
|
|
186503
|
+
last_seq: lastSeq,
|
|
186504
|
+
updated_at: Date.now()
|
|
186505
|
+
}).onConflict((oc) => oc.columns(["remote_server_id", "remote_session_id"]).doNothing()).executeTakeFirst();
|
|
186506
|
+
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
186507
|
+
}
|
|
186508
|
+
}
|
|
186509
|
+
});
|
|
186510
|
+
|
|
186280
186511
|
// src/storage/sqlite.ts
|
|
186281
186512
|
var createDatabase = (dbPath) => {
|
|
186282
186513
|
const db = new Database(dbPath);
|
|
@@ -186975,7 +187206,66 @@ var createDatabase = (dbPath) => {
|
|
|
186975
187206
|
PRIMARY KEY (session_id, turn_end_index),
|
|
186976
187207
|
FOREIGN KEY (session_id) REFERENCES agent_sessions(id) ON DELETE CASCADE
|
|
186977
187208
|
);
|
|
187209
|
+
|
|
187210
|
+
-- Durable attention-milestone outbox of THIS server. Written in the same
|
|
187211
|
+
-- transaction as the state that proves the milestone (a turn_end entry, a
|
|
187212
|
+
-- workflow status transition), so a crash can never leave a completed turn
|
|
187213
|
+
-- without its notification. Deliberately carries no title/body: the
|
|
187214
|
+
-- user-facing server generates copy at import time from its own session
|
|
187215
|
+
-- mapping, so a stale worker-side title never enters the wire protocol.
|
|
187216
|
+
-- No FK to agent_sessions: an outbox row must outlive deletion of the
|
|
187217
|
+
-- session it describes (deleting a session mid-sync must not silently drop
|
|
187218
|
+
-- an already-produced milestone). seq is the cursor, not created_at.
|
|
187219
|
+
CREATE TABLE IF NOT EXISTS notification_outbox (
|
|
187220
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
187221
|
+
id TEXT NOT NULL UNIQUE,
|
|
187222
|
+
kind TEXT NOT NULL,
|
|
187223
|
+
project_id TEXT NOT NULL,
|
|
187224
|
+
branch TEXT,
|
|
187225
|
+
session_id TEXT NOT NULL,
|
|
187226
|
+
workflow_run_id TEXT,
|
|
187227
|
+
created_at INTEGER NOT NULL
|
|
187228
|
+
);
|
|
187229
|
+
|
|
187230
|
+
CREATE INDEX IF NOT EXISTS idx_notification_outbox_session_seq
|
|
187231
|
+
ON notification_outbox(session_id, seq);
|
|
187232
|
+
|
|
187233
|
+
-- User-scoped notification inbox of the user-facing server. The id is the
|
|
187234
|
+
-- deterministic milestone id (remote rows namespaced
|
|
187235
|
+
-- remote:{serverId}:{outboxEventId}), which is what makes replay after a
|
|
187236
|
+
-- crash-before-cursor-advance a no-op instead of a duplicate ding.
|
|
187237
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
187238
|
+
id TEXT PRIMARY KEY,
|
|
187239
|
+
user_id TEXT NOT NULL,
|
|
187240
|
+
kind TEXT NOT NULL,
|
|
187241
|
+
project_id TEXT NOT NULL,
|
|
187242
|
+
branch TEXT,
|
|
187243
|
+
session_id TEXT,
|
|
187244
|
+
workflow_run_id TEXT,
|
|
187245
|
+
title TEXT NOT NULL,
|
|
187246
|
+
body TEXT,
|
|
187247
|
+
created_at INTEGER NOT NULL,
|
|
187248
|
+
read_at INTEGER
|
|
187249
|
+
);
|
|
187250
|
+
|
|
187251
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_user_created
|
|
187252
|
+
ON notifications(user_id, created_at DESC);
|
|
187253
|
+
|
|
187254
|
+
CREATE TABLE IF NOT EXISTS notification_sync_cursors (
|
|
187255
|
+
remote_server_id TEXT NOT NULL,
|
|
187256
|
+
remote_session_id TEXT NOT NULL,
|
|
187257
|
+
last_seq INTEGER NOT NULL,
|
|
187258
|
+
updated_at INTEGER NOT NULL,
|
|
187259
|
+
PRIMARY KEY (remote_server_id, remote_session_id)
|
|
187260
|
+
);
|
|
186978
187261
|
`);
|
|
187262
|
+
const remoteMappingNotifyInfo = db.prepare("PRAGMA table_info(remote_session_mappings)").all();
|
|
187263
|
+
if (!remoteMappingNotifyInfo.some((col) => col.name === "notification_sync_start")) {
|
|
187264
|
+
db.exec("ALTER TABLE remote_session_mappings ADD COLUMN notification_sync_start TEXT NOT NULL DEFAULT 'from_now'");
|
|
187265
|
+
}
|
|
187266
|
+
if (!remoteMappingNotifyInfo.some((col) => col.name === "notification_watch_until")) {
|
|
187267
|
+
db.exec("ALTER TABLE remote_session_mappings ADD COLUMN notification_watch_until INTEGER");
|
|
187268
|
+
}
|
|
186979
187269
|
const sessionSearchCacheInfo = db.prepare("PRAGMA table_info(session_search_cache)").all();
|
|
186980
187270
|
if (!sessionSearchCacheInfo.some((col) => col.name === "written_at")) {
|
|
186981
187271
|
db.exec("ALTER TABLE session_search_cache ADD COLUMN written_at INTEGER");
|
|
@@ -187022,6 +187312,7 @@ var createSqliteStorage = async (dbPath) => {
|
|
|
187022
187312
|
...createSearchCacheRepos(kdb, h),
|
|
187023
187313
|
...createWorkflowRunRepos(kdb),
|
|
187024
187314
|
...createTurnSnapshotRepos(kdb),
|
|
187315
|
+
...createNotificationRepos(kdb),
|
|
187025
187316
|
close: async () => {
|
|
187026
187317
|
await kdb.destroy();
|
|
187027
187318
|
}
|
|
@@ -187038,7 +187329,7 @@ import path17 from "path";
|
|
|
187038
187329
|
import { fileURLToPath } from "url";
|
|
187039
187330
|
|
|
187040
187331
|
// src/plugins/shared-services.ts
|
|
187041
|
-
var
|
|
187332
|
+
var import_fastify_plugin2 = __toESM(require_plugin2(), 1);
|
|
187042
187333
|
|
|
187043
187334
|
// src/process-manager.ts
|
|
187044
187335
|
import { spawn } from "child_process";
|
|
@@ -202016,6 +202307,57 @@ import { spawn as spawn3 } from "child_process";
|
|
|
202016
202307
|
import { randomUUID } from "crypto";
|
|
202017
202308
|
import { existsSync as existsSync3 } from "fs";
|
|
202018
202309
|
|
|
202310
|
+
// src/notification-milestones.ts
|
|
202311
|
+
var sessionResultReadyId = (sessionId, turnEndEntryIndex) => `session:${sessionId}:turn:${turnEndEntryIndex}:result-ready`;
|
|
202312
|
+
var sessionFailedId = (sessionId, turnEndEntryIndex) => `session:${sessionId}:turn:${turnEndEntryIndex}:failed`;
|
|
202313
|
+
var reviewReadyId = (workflowRunId) => `workflow:${workflowRunId}:review-ready`;
|
|
202314
|
+
var workflowFailedId = (workflowRunId, stateVersion) => `workflow:${workflowRunId}:failed:${stateVersion}`;
|
|
202315
|
+
function findTurnOpeningUserEntry(entries, beforeIndex) {
|
|
202316
|
+
let opening;
|
|
202317
|
+
for (let i = Math.min(beforeIndex, entries.length) - 1; i >= 0; i--) {
|
|
202318
|
+
const entry = entries[i];
|
|
202319
|
+
if (!entry) continue;
|
|
202320
|
+
if (entry.type === "turn_end") break;
|
|
202321
|
+
if (entry.type === "user") opening = entry;
|
|
202322
|
+
}
|
|
202323
|
+
return opening;
|
|
202324
|
+
}
|
|
202325
|
+
function resolveNotificationDisposition(openingUserEntry) {
|
|
202326
|
+
if (openingUserEntry?.type !== "user") return "result";
|
|
202327
|
+
if (openingUserEntry.notificationDisposition) return openingUserEntry.notificationDisposition;
|
|
202328
|
+
return openingUserEntry.origin === "workflow" ? "internal" : "result";
|
|
202329
|
+
}
|
|
202330
|
+
var FAILURE_OUTCOMES = /* @__PURE__ */ new Set([
|
|
202331
|
+
"failed",
|
|
202332
|
+
"process_exit",
|
|
202333
|
+
"server_restart"
|
|
202334
|
+
]);
|
|
202335
|
+
function sessionMilestoneForTurnEnd(opts) {
|
|
202336
|
+
if (opts.disposition !== "result") return void 0;
|
|
202337
|
+
const base = {
|
|
202338
|
+
project_id: opts.projectId,
|
|
202339
|
+
branch: opts.branch,
|
|
202340
|
+
session_id: opts.sessionId,
|
|
202341
|
+
workflow_run_id: null,
|
|
202342
|
+
created_at: opts.createdAt
|
|
202343
|
+
};
|
|
202344
|
+
if (opts.outcome === "completed") {
|
|
202345
|
+
return {
|
|
202346
|
+
...base,
|
|
202347
|
+
id: sessionResultReadyId(opts.sessionId, opts.entryIndex),
|
|
202348
|
+
kind: "session_result_ready"
|
|
202349
|
+
};
|
|
202350
|
+
}
|
|
202351
|
+
if (FAILURE_OUTCOMES.has(opts.outcome)) {
|
|
202352
|
+
return {
|
|
202353
|
+
...base,
|
|
202354
|
+
id: sessionFailedId(opts.sessionId, opts.entryIndex),
|
|
202355
|
+
kind: "session_failed"
|
|
202356
|
+
};
|
|
202357
|
+
}
|
|
202358
|
+
return void 0;
|
|
202359
|
+
}
|
|
202360
|
+
|
|
202019
202361
|
// src/utils/cross-remote-token.ts
|
|
202020
202362
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
202021
202363
|
var CROSS_REMOTE_SECRET_SETTING = "cross_remote_token_secret";
|
|
@@ -224295,6 +224637,36 @@ var AgentSessionManager = class {
|
|
|
224295
224637
|
setEventBus(eventBus) {
|
|
224296
224638
|
this.eventBus = eventBus;
|
|
224297
224639
|
}
|
|
224640
|
+
/**
|
|
224641
|
+
* Injected by shared-services: nudges NotificationService to drain the local
|
|
224642
|
+
* outbox right after a milestone lands, so the bell is fast in the common
|
|
224643
|
+
* case. Purely a latency optimization — correctness comes from the periodic
|
|
224644
|
+
* and startup drains, because this hook can't fire if the process dies
|
|
224645
|
+
* between the commit and the import.
|
|
224646
|
+
*/
|
|
224647
|
+
setMilestoneListener(listener) {
|
|
224648
|
+
this.onMilestoneCreated = listener;
|
|
224649
|
+
}
|
|
224650
|
+
onMilestoneCreated = null;
|
|
224651
|
+
/**
|
|
224652
|
+
* Decide the disposition to persist on an outgoing user turn.
|
|
224653
|
+
*
|
|
224654
|
+
* Ordinary user input defaults to `result`. Workflow callers must be explicit:
|
|
224655
|
+
* `origin: "workflow"` alone is NOT enough to infer `internal`, because
|
|
224656
|
+
* approved review feedback is also a workflow-authored turn yet starts a
|
|
224657
|
+
* user-visible source result. An omission is therefore a caller bug — warn
|
|
224658
|
+
* loudly rather than silently picking a disposition that suppresses a real
|
|
224659
|
+
* notification.
|
|
224660
|
+
*/
|
|
224661
|
+
resolveOutgoingDisposition(sessionId, opts) {
|
|
224662
|
+
if (opts?.notificationDisposition) return opts.notificationDisposition;
|
|
224663
|
+
if (opts?.origin === "workflow") {
|
|
224664
|
+
console.warn(
|
|
224665
|
+
`[AgentSession] workflow-origin turn for ${sessionId} carries no explicit notificationDisposition; defaulting to "result"`
|
|
224666
|
+
);
|
|
224667
|
+
}
|
|
224668
|
+
return "result";
|
|
224669
|
+
}
|
|
224298
224670
|
/**
|
|
224299
224671
|
* Injected by shared-services: lets commitCompletion mark taskCompleted WS
|
|
224300
224672
|
* frames whose completion the local WorkflowEngine claims (reviewer
|
|
@@ -224570,7 +224942,8 @@ var AgentSessionManager = class {
|
|
|
224570
224942
|
bgSpawnHintsThisTurn: 0,
|
|
224571
224943
|
taskStartedThisTurn: 0,
|
|
224572
224944
|
lastActiveAt: Date.now(),
|
|
224573
|
-
turnOpenSince: null
|
|
224945
|
+
turnOpenSince: null,
|
|
224946
|
+
turnDisposition: null
|
|
224574
224947
|
};
|
|
224575
224948
|
this.sessions.set(sessionId, session);
|
|
224576
224949
|
const provider = getProvider(agentType);
|
|
@@ -225139,14 +225512,22 @@ Details:
|
|
|
225139
225512
|
${details}`;
|
|
225140
225513
|
return msg;
|
|
225141
225514
|
}
|
|
225515
|
+
/**
|
|
225516
|
+
* Allocate the next entry index, record the message in the in-memory store,
|
|
225517
|
+
* and build its ADD replay patch. Shared by `pushEntry` and `pushTurnEnd` so
|
|
225518
|
+
* the two persistence paths can't drift on index allocation or patch shape.
|
|
225519
|
+
*/
|
|
225520
|
+
stageEntry(session, message) {
|
|
225521
|
+
const index = session.store.indexProvider.next();
|
|
225522
|
+
session.store.entries[index] = message;
|
|
225523
|
+
const patch = ConversationPatch.addEntry(index, message);
|
|
225524
|
+
session.store.patches.push(patch);
|
|
225525
|
+
return { index, patch };
|
|
225526
|
+
}
|
|
225142
225527
|
async pushEntry(sessionId, message, broadcast = true, userId = "local") {
|
|
225143
225528
|
const session = this.sessions.get(sessionId);
|
|
225144
225529
|
if (!session) return -1;
|
|
225145
|
-
const {
|
|
225146
|
-
const index = store.indexProvider.next();
|
|
225147
|
-
store.entries[index] = message;
|
|
225148
|
-
const patch = ConversationPatch.addEntry(index, message);
|
|
225149
|
-
store.patches.push(patch);
|
|
225530
|
+
const { index, patch } = this.stageEntry(session, message);
|
|
225150
225531
|
if (!session.skipDb && message.type !== "assistant") {
|
|
225151
225532
|
await this.persistEntry(session, index, message, userId);
|
|
225152
225533
|
}
|
|
@@ -225155,6 +225536,60 @@ ${details}`;
|
|
|
225155
225536
|
}
|
|
225156
225537
|
return index;
|
|
225157
225538
|
}
|
|
225539
|
+
/**
|
|
225540
|
+
* Persist a `turn_end` together with the attention milestone it earns.
|
|
225541
|
+
*
|
|
225542
|
+
* Deliberately NOT routed through `pushEntry`: the milestone id embeds the
|
|
225543
|
+
* turn_end's entry index, so the index has to be allocated before the write,
|
|
225544
|
+
* and the write itself has to be the atomic turn-end/outbox operation rather
|
|
225545
|
+
* than a plain entry upsert. Ordering matches `pushEntry` (persist, then
|
|
225546
|
+
* broadcast) so the existing turn_end-before-status contract still holds.
|
|
225547
|
+
*
|
|
225548
|
+
* `entryIndexOverride` re-persists an already-staged boundary (crash-repair
|
|
225549
|
+
* replay); without it a fresh index is allocated.
|
|
225550
|
+
*/
|
|
225551
|
+
async pushTurnEnd(session, outcome, disposition, endedAt, durationMs, entryIndexOverride) {
|
|
225552
|
+
const message = {
|
|
225553
|
+
type: "turn_end",
|
|
225554
|
+
timestamp: endedAt,
|
|
225555
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
225556
|
+
outcome,
|
|
225557
|
+
notificationDisposition: disposition
|
|
225558
|
+
};
|
|
225559
|
+
let index;
|
|
225560
|
+
let patch;
|
|
225561
|
+
if (entryIndexOverride !== void 0) {
|
|
225562
|
+
index = entryIndexOverride;
|
|
225563
|
+
patch = ConversationPatch.addEntry(index, message);
|
|
225564
|
+
} else {
|
|
225565
|
+
({ index, patch } = this.stageEntry(session, message));
|
|
225566
|
+
}
|
|
225567
|
+
if (!session.skipDb) {
|
|
225568
|
+
const outbox = sessionMilestoneForTurnEnd({
|
|
225569
|
+
sessionId: session.id,
|
|
225570
|
+
projectId: session.projectId,
|
|
225571
|
+
branch: session.branch,
|
|
225572
|
+
entryIndex: index,
|
|
225573
|
+
outcome,
|
|
225574
|
+
disposition,
|
|
225575
|
+
createdAt: endedAt
|
|
225576
|
+
});
|
|
225577
|
+
try {
|
|
225578
|
+
await this.storage.agentSessions.upsertTurnEndWithOutbox({
|
|
225579
|
+
sessionId: session.id,
|
|
225580
|
+
entryIndex: index,
|
|
225581
|
+
entryData: JSON.stringify(message),
|
|
225582
|
+
outbox
|
|
225583
|
+
});
|
|
225584
|
+
await this.storage.agentSessions.touchUpdatedAt(session.id);
|
|
225585
|
+
if (outbox) this.onMilestoneCreated?.();
|
|
225586
|
+
} catch (error48) {
|
|
225587
|
+
console.error(`[AgentSession] Failed to persist turn_end ${index} for ${session.id}:`, error48);
|
|
225588
|
+
}
|
|
225589
|
+
}
|
|
225590
|
+
if (entryIndexOverride === void 0) this.broadcastPatch(session.id, patch);
|
|
225591
|
+
return index;
|
|
225592
|
+
}
|
|
225158
225593
|
/**
|
|
225159
225594
|
* Persist a single entry to the database
|
|
225160
225595
|
*/
|
|
@@ -225193,18 +225628,24 @@ ${details}`;
|
|
|
225193
225628
|
}
|
|
225194
225629
|
}
|
|
225195
225630
|
/**
|
|
225196
|
-
* Close the open turn with a persisted turn_end stop-point entry
|
|
225631
|
+
* Close the open turn with a persisted turn_end stop-point entry, plus the
|
|
225632
|
+
* attention milestone the turn's disposition earns (written in the same
|
|
225633
|
+
* storage transaction — see pushTurnEnd).
|
|
225634
|
+
*
|
|
225197
225635
|
* turn_end entries are constructed ONLY here and in repairInterruptedTurn
|
|
225198
225636
|
* (restore path). Wall clock only — see design doc for why the CLI's
|
|
225199
|
-
* payload.duration_ms is not used.
|
|
225200
|
-
* persistence on purpose (no strict path — design decision).
|
|
225637
|
+
* payload.duration_ms is not used.
|
|
225201
225638
|
*/
|
|
225202
225639
|
async endActiveTurn(session, outcome) {
|
|
225203
225640
|
if (session.turnOpenSince === null) return null;
|
|
225204
225641
|
const endedAt = Date.now();
|
|
225205
225642
|
const durationMs = endedAt - session.turnOpenSince;
|
|
225206
|
-
const
|
|
225643
|
+
const disposition = session.turnDisposition ?? resolveNotificationDisposition(
|
|
225644
|
+
findTurnOpeningUserEntry(session.store.entries, session.store.entries.length)
|
|
225645
|
+
);
|
|
225646
|
+
const index = await this.pushTurnEnd(session, outcome, disposition, endedAt, durationMs);
|
|
225207
225647
|
session.turnOpenSince = null;
|
|
225648
|
+
session.turnDisposition = null;
|
|
225208
225649
|
if (!session.skipDb && index >= 0) {
|
|
225209
225650
|
try {
|
|
225210
225651
|
const project = await this.storage.projects.getById(session.projectId);
|
|
@@ -225223,12 +225664,13 @@ ${details}`;
|
|
|
225223
225664
|
async sendUserMessage(sessionId, content, projectPath, userId = "local", opts) {
|
|
225224
225665
|
const session = this.sessions.get(sessionId);
|
|
225225
225666
|
if (!session) return false;
|
|
225667
|
+
const disposition = this.resolveOutgoingDisposition(sessionId, opts);
|
|
225226
225668
|
if (session.dormant) {
|
|
225227
225669
|
if (!projectPath) {
|
|
225228
225670
|
console.error(`[AgentSession] Cannot wake dormant session ${sessionId} without projectPath`);
|
|
225229
225671
|
return false;
|
|
225230
225672
|
}
|
|
225231
|
-
await this.wakeDormantSession(session, projectPath, content, userId, opts?.origin);
|
|
225673
|
+
await this.wakeDormantSession(session, projectPath, content, userId, opts?.origin, disposition);
|
|
225232
225674
|
return true;
|
|
225233
225675
|
}
|
|
225234
225676
|
if (!session.process?.stdin) {
|
|
@@ -225248,7 +225690,8 @@ ${details}`;
|
|
|
225248
225690
|
type: "user",
|
|
225249
225691
|
content,
|
|
225250
225692
|
timestamp: Date.now(),
|
|
225251
|
-
...opts?.origin ? { origin: opts.origin } : {}
|
|
225693
|
+
...opts?.origin ? { origin: opts.origin } : {},
|
|
225694
|
+
notificationDisposition: disposition
|
|
225252
225695
|
}, true, userId);
|
|
225253
225696
|
try {
|
|
225254
225697
|
const provider = getProvider(session.agentType);
|
|
@@ -225257,14 +225700,20 @@ ${details}`;
|
|
|
225257
225700
|
console.warn(
|
|
225258
225701
|
`[AgentSession] sendUserMessage: provider buffered the input for ${session.agentType} session ${sessionId} \u2014 no stdin payload yet`
|
|
225259
225702
|
);
|
|
225260
|
-
if (session.turnOpenSince === null)
|
|
225703
|
+
if (session.turnOpenSince === null) {
|
|
225704
|
+
session.turnOpenSince = Date.now();
|
|
225705
|
+
session.turnDisposition = disposition;
|
|
225706
|
+
}
|
|
225261
225707
|
return true;
|
|
225262
225708
|
}
|
|
225263
225709
|
console.log(
|
|
225264
225710
|
`[AgentSession] sendUserMessage: wrote ${formatted.length}B to ${session.agentType} stdin (session=${sessionId})`
|
|
225265
225711
|
);
|
|
225266
225712
|
session.process.stdin.write(formatted);
|
|
225267
|
-
if (session.turnOpenSince === null)
|
|
225713
|
+
if (session.turnOpenSince === null) {
|
|
225714
|
+
session.turnOpenSince = Date.now();
|
|
225715
|
+
session.turnDisposition = disposition;
|
|
225716
|
+
}
|
|
225268
225717
|
return true;
|
|
225269
225718
|
} catch (error48) {
|
|
225270
225719
|
console.error(`[AgentSession] Failed to send message:`, error48);
|
|
@@ -225691,7 +226140,7 @@ ${details}`;
|
|
|
225691
226140
|
/**
|
|
225692
226141
|
* Wake a dormant session: spawn process, send full context + user message
|
|
225693
226142
|
*/
|
|
225694
|
-
async wakeDormantSession(session, projectPath, userMessage, userId = "local", origin) {
|
|
226143
|
+
async wakeDormantSession(session, projectPath, userMessage, userId = "local", origin, notificationDisposition = "result") {
|
|
225695
226144
|
console.log(`[AgentSession] Waking dormant session ${session.id}`);
|
|
225696
226145
|
await this.ensureResidentCapacity(
|
|
225697
226146
|
{ projectId: session.projectId, branch: session.branch },
|
|
@@ -225712,9 +226161,11 @@ ${details}`;
|
|
|
225712
226161
|
type: "user",
|
|
225713
226162
|
content: userMessage,
|
|
225714
226163
|
timestamp: Date.now(),
|
|
225715
|
-
...origin ? { origin } : {}
|
|
226164
|
+
...origin ? { origin } : {},
|
|
226165
|
+
notificationDisposition
|
|
225716
226166
|
}, true, userId);
|
|
225717
226167
|
session.turnOpenSince = Date.now();
|
|
226168
|
+
session.turnDisposition = notificationDisposition;
|
|
225718
226169
|
setTimeout(() => {
|
|
225719
226170
|
const context2 = this.buildFullConversationContext(session.store.entries);
|
|
225720
226171
|
if (context2) {
|
|
@@ -225781,7 +226232,8 @@ ${details}`;
|
|
|
225781
226232
|
* this runs on server boot for every restored session and must never
|
|
225782
226233
|
* throw into the restore path.
|
|
225783
226234
|
*/
|
|
225784
|
-
async repairInterruptedTurn(
|
|
226235
|
+
async repairInterruptedTurn(dbSession, rows) {
|
|
226236
|
+
const sessionId = dbSession.id;
|
|
225785
226237
|
let landingType = null;
|
|
225786
226238
|
for (let i = rows.length - 1; i >= 0; i--) {
|
|
225787
226239
|
try {
|
|
@@ -225796,17 +226248,43 @@ ${details}`;
|
|
|
225796
226248
|
if (landingType === null || landingType === "turn_end") return rows;
|
|
225797
226249
|
const maxIndex = rows.reduce((m2, r) => Math.max(m2, r.entry_index), -1);
|
|
225798
226250
|
const repairIndex = maxIndex + 1;
|
|
225799
|
-
const
|
|
226251
|
+
const entries = [];
|
|
226252
|
+
for (const row of rows) {
|
|
226253
|
+
try {
|
|
226254
|
+
entries[row.entry_index] = JSON.parse(row.data);
|
|
226255
|
+
} catch {
|
|
226256
|
+
}
|
|
226257
|
+
}
|
|
226258
|
+
const disposition = resolveNotificationDisposition(
|
|
226259
|
+
findTurnOpeningUserEntry(entries, repairIndex)
|
|
226260
|
+
);
|
|
226261
|
+
const repair = {
|
|
226262
|
+
type: "turn_end",
|
|
226263
|
+
timestamp: Date.now(),
|
|
226264
|
+
outcome: "server_restart",
|
|
226265
|
+
notificationDisposition: disposition
|
|
226266
|
+
};
|
|
225800
226267
|
const data = JSON.stringify(repair);
|
|
225801
|
-
|
|
225802
|
-
|
|
226268
|
+
const outbox = sessionMilestoneForTurnEnd({
|
|
226269
|
+
sessionId,
|
|
226270
|
+
projectId: dbSession.project_id,
|
|
226271
|
+
branch: dbSession.branch || null,
|
|
226272
|
+
entryIndex: repairIndex,
|
|
226273
|
+
outcome: "server_restart",
|
|
226274
|
+
disposition,
|
|
226275
|
+
createdAt: repair.timestamp
|
|
226276
|
+
});
|
|
226277
|
+
await this.storage.agentSessions.upsertTurnEndWithOutbox({
|
|
226278
|
+
sessionId,
|
|
226279
|
+
entryIndex: repairIndex,
|
|
226280
|
+
entryData: data,
|
|
226281
|
+
outbox
|
|
226282
|
+
});
|
|
226283
|
+
console.log(`[AgentSession] Repaired interrupted turn for ${sessionId} (server_restart turn_end at ${repairIndex}, disposition=${disposition})`);
|
|
225803
226284
|
try {
|
|
225804
|
-
const
|
|
225805
|
-
if (
|
|
225806
|
-
|
|
225807
|
-
if (project?.path) {
|
|
225808
|
-
await recordTurnSnapshot(this.storage, sessionId, repairIndex, resolveWorktreePath(project.path, dbSession.branch));
|
|
225809
|
-
}
|
|
226285
|
+
const project = await this.storage.projects.getById(dbSession.project_id);
|
|
226286
|
+
if (project?.path) {
|
|
226287
|
+
await recordTurnSnapshot(this.storage, sessionId, repairIndex, resolveWorktreePath(project.path, dbSession.branch));
|
|
225810
226288
|
}
|
|
225811
226289
|
} catch (error48) {
|
|
225812
226290
|
console.warn(`[AgentSession] Turn snapshot lookup failed for ${sessionId}@${repairIndex}:`, error48);
|
|
@@ -225825,7 +226303,7 @@ ${details}`;
|
|
|
225825
226303
|
let entries = await this.storage.agentSessions.getEntries(dbSession.id);
|
|
225826
226304
|
if (entries.length === 0) continue;
|
|
225827
226305
|
if (dbSession.status === "running") {
|
|
225828
|
-
entries = await this.repairInterruptedTurn(dbSession
|
|
226306
|
+
entries = await this.repairInterruptedTurn(dbSession, entries);
|
|
225829
226307
|
}
|
|
225830
226308
|
const store = this.rebuildStoreFromRows(entries, dbSession.id);
|
|
225831
226309
|
const permissionMode = dbSession.permission_mode === "plan" ? "plan" : "edit";
|
|
@@ -225848,7 +226326,8 @@ ${details}`;
|
|
|
225848
226326
|
bgSpawnHintsThisTurn: 0,
|
|
225849
226327
|
taskStartedThisTurn: 0,
|
|
225850
226328
|
lastActiveAt: Date.now(),
|
|
225851
|
-
turnOpenSince: null
|
|
226329
|
+
turnOpenSince: null,
|
|
226330
|
+
turnDisposition: null
|
|
225852
226331
|
};
|
|
225853
226332
|
this.sessions.set(dbSession.id, runningSession);
|
|
225854
226333
|
await this.storage.agentSessions.updateStatusPreservingTimestamp(dbSession.id, "stopped");
|
|
@@ -225950,6 +226429,7 @@ ${details}`;
|
|
|
225950
226429
|
taskStartedThisTurn: 0,
|
|
225951
226430
|
lastActiveAt: Date.now(),
|
|
225952
226431
|
turnOpenSince: null,
|
|
226432
|
+
turnDisposition: null,
|
|
225953
226433
|
crossRemoteMcp: opts.crossRemoteMcp
|
|
225954
226434
|
};
|
|
225955
226435
|
this.sessions.set(newId, branched);
|
|
@@ -226296,6 +226776,394 @@ function runUpdatedEventFromRemoteFrame(parsed, sessionId, remoteInfo) {
|
|
|
226296
226776
|
return { type: "workflow:run-updated", projectId, branch: run2.branch, run: run2 };
|
|
226297
226777
|
}
|
|
226298
226778
|
|
|
226779
|
+
// src/routes/notification-outbox-routes.ts
|
|
226780
|
+
var import_fastify_plugin = __toESM(require_plugin2(), 1);
|
|
226781
|
+
var MAX_SESSIONS_PER_REQUEST = 100;
|
|
226782
|
+
var MAX_EVENTS_PER_SESSION = 100;
|
|
226783
|
+
function validateOutboxQuery(body) {
|
|
226784
|
+
const raw = body?.sessions;
|
|
226785
|
+
if (!Array.isArray(raw)) return { ok: false, error: "sessions must be an array" };
|
|
226786
|
+
if (raw.length === 0) return { ok: false, error: "sessions must not be empty" };
|
|
226787
|
+
if (raw.length > MAX_SESSIONS_PER_REQUEST) {
|
|
226788
|
+
return { ok: false, error: `sessions must contain at most ${MAX_SESSIONS_PER_REQUEST} entries` };
|
|
226789
|
+
}
|
|
226790
|
+
const seen = /* @__PURE__ */ new Set();
|
|
226791
|
+
const sessions = [];
|
|
226792
|
+
for (const entry of raw) {
|
|
226793
|
+
if (typeof entry !== "object" || entry === null) return { ok: false, error: "each session must be an object" };
|
|
226794
|
+
const { sessionId, after, headOnly } = entry;
|
|
226795
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
226796
|
+
return { ok: false, error: "sessionId must be a non-empty string" };
|
|
226797
|
+
}
|
|
226798
|
+
if (typeof after !== "number" || !Number.isInteger(after) || after < 0) {
|
|
226799
|
+
return { ok: false, error: "after must be a non-negative integer" };
|
|
226800
|
+
}
|
|
226801
|
+
if (headOnly !== void 0 && typeof headOnly !== "boolean") {
|
|
226802
|
+
return { ok: false, error: "headOnly must be a boolean" };
|
|
226803
|
+
}
|
|
226804
|
+
if (seen.has(sessionId)) return { ok: false, error: `duplicate sessionId: ${sessionId}` };
|
|
226805
|
+
seen.add(sessionId);
|
|
226806
|
+
sessions.push({ sessionId, after, ...headOnly ? { headOnly: true } : {} });
|
|
226807
|
+
}
|
|
226808
|
+
const rawLimit = body?.limitPerSession;
|
|
226809
|
+
if (rawLimit !== void 0) {
|
|
226810
|
+
if (typeof rawLimit !== "number" || !Number.isInteger(rawLimit) || rawLimit <= 0) {
|
|
226811
|
+
return { ok: false, error: "limitPerSession must be a positive integer" };
|
|
226812
|
+
}
|
|
226813
|
+
if (rawLimit > MAX_EVENTS_PER_SESSION) {
|
|
226814
|
+
return { ok: false, error: `limitPerSession must be at most ${MAX_EVENTS_PER_SESSION}` };
|
|
226815
|
+
}
|
|
226816
|
+
}
|
|
226817
|
+
return {
|
|
226818
|
+
ok: true,
|
|
226819
|
+
sessions,
|
|
226820
|
+
limit: typeof rawLimit === "number" ? rawLimit : MAX_EVENTS_PER_SESSION
|
|
226821
|
+
};
|
|
226822
|
+
}
|
|
226823
|
+
var routes = async (fastify2) => {
|
|
226824
|
+
fastify2.post("/api/notification-outbox/query", async (req, reply) => {
|
|
226825
|
+
const authResult = requireAuth(req, reply);
|
|
226826
|
+
if (authResult === null) return;
|
|
226827
|
+
const validated = validateOutboxQuery(req.body ?? {});
|
|
226828
|
+
if (!validated.ok) return reply.code(400).send({ error: validated.error });
|
|
226829
|
+
const sessions = [];
|
|
226830
|
+
for (const request of validated.sessions) {
|
|
226831
|
+
const headCursor = await fastify2.storage.notificationOutbox.headBySession(request.sessionId);
|
|
226832
|
+
if (request.headOnly) {
|
|
226833
|
+
sessions.push({
|
|
226834
|
+
sessionId: request.sessionId,
|
|
226835
|
+
events: [],
|
|
226836
|
+
headCursor,
|
|
226837
|
+
nextCursor: headCursor,
|
|
226838
|
+
hasMore: false
|
|
226839
|
+
});
|
|
226840
|
+
continue;
|
|
226841
|
+
}
|
|
226842
|
+
const events = await fastify2.storage.notificationOutbox.listBySessionAfter(
|
|
226843
|
+
request.sessionId,
|
|
226844
|
+
request.after,
|
|
226845
|
+
validated.limit
|
|
226846
|
+
);
|
|
226847
|
+
const nextCursor = events.length > 0 ? events[events.length - 1].seq : request.after;
|
|
226848
|
+
sessions.push({
|
|
226849
|
+
sessionId: request.sessionId,
|
|
226850
|
+
events,
|
|
226851
|
+
headCursor,
|
|
226852
|
+
nextCursor,
|
|
226853
|
+
hasMore: nextCursor < headCursor
|
|
226854
|
+
});
|
|
226855
|
+
}
|
|
226856
|
+
return reply.code(200).send({ sessions });
|
|
226857
|
+
});
|
|
226858
|
+
};
|
|
226859
|
+
var notification_outbox_routes_default = (0, import_fastify_plugin.default)(routes, { name: "notification-outbox-routes" });
|
|
226860
|
+
|
|
226861
|
+
// src/remote-notification-sync.ts
|
|
226862
|
+
var OUTBOX_QUERY_PATH = "/api/notification-outbox/query";
|
|
226863
|
+
var QUERY_TIMEOUT_MS = 15e3;
|
|
226864
|
+
var WATCH_WINDOW_MS = 30 * 60 * 1e3;
|
|
226865
|
+
var WATCH_EXTEND_THROTTLE_MS = 5 * 60 * 1e3;
|
|
226866
|
+
var MAX_TRACKED_SESSIONS = 5e3;
|
|
226867
|
+
var SYNC_INTERVAL_MS = 6e4;
|
|
226868
|
+
var MAX_PAGES_PER_SYNC = 50;
|
|
226869
|
+
function localWorkflowRunId(remoteServerId, projectId, remoteRunId) {
|
|
226870
|
+
return `remote-${remoteServerId}-${projectId}-${remoteRunId}`;
|
|
226871
|
+
}
|
|
226872
|
+
function localNotificationId(remoteServerId, outboxEventId) {
|
|
226873
|
+
return `remote:${remoteServerId}:${outboxEventId}`;
|
|
226874
|
+
}
|
|
226875
|
+
var RemoteNotificationSync = class {
|
|
226876
|
+
storage;
|
|
226877
|
+
notificationService;
|
|
226878
|
+
reverseConnectManager;
|
|
226879
|
+
proxy;
|
|
226880
|
+
timer = null;
|
|
226881
|
+
stopped = false;
|
|
226882
|
+
/** Serializes sweeps so a came-online sweep can't interleave with the tick. */
|
|
226883
|
+
chain = Promise.resolve();
|
|
226884
|
+
/**
|
|
226885
|
+
* Remote sessions this front currently believes are RUNNING. They are polled
|
|
226886
|
+
* regardless of their persisted watch window: an agent turn can easily run
|
|
226887
|
+
* longer than WATCH_WINDOW_MS while emitting nothing the bus can see, and a
|
|
226888
|
+
* mapping that lapsed mid-turn would not be polled when the turn finally
|
|
226889
|
+
* produces its milestone.
|
|
226890
|
+
*/
|
|
226891
|
+
activeRemoteSessions = /* @__PURE__ */ new Set();
|
|
226892
|
+
/** sessionId → last activity-driven watch extension (see WATCH_EXTEND_THROTTLE_MS). */
|
|
226893
|
+
lastWatchExtend = /* @__PURE__ */ new Map();
|
|
226894
|
+
constructor(deps) {
|
|
226895
|
+
this.storage = deps.storage;
|
|
226896
|
+
this.notificationService = deps.notificationService;
|
|
226897
|
+
this.reverseConnectManager = deps.reverseConnectManager;
|
|
226898
|
+
this.proxy = deps.proxy ?? proxyToRemoteAuto;
|
|
226899
|
+
}
|
|
226900
|
+
start() {
|
|
226901
|
+
if (this.timer || this.stopped) return;
|
|
226902
|
+
this.timer = setInterval(() => {
|
|
226903
|
+
this.enqueue(() => this.syncAll({ includeExpired: false }));
|
|
226904
|
+
}, SYNC_INTERVAL_MS);
|
|
226905
|
+
this.timer.unref?.();
|
|
226906
|
+
}
|
|
226907
|
+
shutdown() {
|
|
226908
|
+
this.stopped = true;
|
|
226909
|
+
if (this.timer) clearInterval(this.timer);
|
|
226910
|
+
this.timer = null;
|
|
226911
|
+
this.activeRemoteSessions.clear();
|
|
226912
|
+
this.lastWatchExtend.clear();
|
|
226913
|
+
}
|
|
226914
|
+
/**
|
|
226915
|
+
* Watch live remote activity (design §Mapping initialization and polling).
|
|
226916
|
+
*
|
|
226917
|
+
* The bus carries the remote stream's bridged `session:status` /
|
|
226918
|
+
* `session:taskCompleted` frames plus this front's own emits for remote
|
|
226919
|
+
* sessions, so it is the one place that sees a remote session start and stop
|
|
226920
|
+
* without threading a callback through the WebSocket plumbing.
|
|
226921
|
+
*/
|
|
226922
|
+
setEventBus(bus) {
|
|
226923
|
+
bus.subscribe((event) => this.observeActivity(event));
|
|
226924
|
+
}
|
|
226925
|
+
observeActivity(event) {
|
|
226926
|
+
const sessionId = event.sessionId;
|
|
226927
|
+
if (!sessionId || !sessionId.startsWith("remote-")) return;
|
|
226928
|
+
if (event.type === "session:status") {
|
|
226929
|
+
if (event.status === "running") this.activeRemoteSessions.add(sessionId);
|
|
226930
|
+
else this.activeRemoteSessions.delete(sessionId);
|
|
226931
|
+
} else if (event.type === "session:taskCompleted" || event.type === "session:finished") {
|
|
226932
|
+
this.activeRemoteSessions.delete(sessionId);
|
|
226933
|
+
}
|
|
226934
|
+
const now2 = Date.now();
|
|
226935
|
+
const last = this.lastWatchExtend.get(sessionId) ?? 0;
|
|
226936
|
+
if (now2 - last < WATCH_EXTEND_THROTTLE_MS) return;
|
|
226937
|
+
if (this.lastWatchExtend.size >= MAX_TRACKED_SESSIONS) this.pruneWatchBookkeeping(now2);
|
|
226938
|
+
this.lastWatchExtend.set(sessionId, now2);
|
|
226939
|
+
void this.extendWatch(sessionId);
|
|
226940
|
+
}
|
|
226941
|
+
/** Drop throttle entries older than a full window — they can only re-extend. */
|
|
226942
|
+
pruneWatchBookkeeping(now2) {
|
|
226943
|
+
for (const [sessionId, at] of this.lastWatchExtend) {
|
|
226944
|
+
if (now2 - at > WATCH_WINDOW_MS) this.lastWatchExtend.delete(sessionId);
|
|
226945
|
+
}
|
|
226946
|
+
}
|
|
226947
|
+
/** Fire-and-forget sweep, queued behind any in-flight one. */
|
|
226948
|
+
enqueue(work) {
|
|
226949
|
+
if (this.stopped) return;
|
|
226950
|
+
this.chain = this.chain.then(work).catch((err) => console.warn("[RemoteNotifications] sync failed:", err));
|
|
226951
|
+
}
|
|
226952
|
+
/** Extend a mapping's periodic-poll window (create / send / workflow start / live activity). */
|
|
226953
|
+
async extendWatch(localSessionId) {
|
|
226954
|
+
await this.storage.remoteSessionMappings.extendNotificationWatch(localSessionId, Date.now() + WATCH_WINDOW_MS).catch((err) => console.warn(`[RemoteNotifications] extendWatch(${localSessionId}) failed:`, err));
|
|
226955
|
+
}
|
|
226956
|
+
/**
|
|
226957
|
+
* Called before this front starts a new turn on a remote session.
|
|
226958
|
+
*
|
|
226959
|
+
* For a `from_now` mapping that has never been initialized, this records the
|
|
226960
|
+
* worker's current head FIRST — otherwise the turn we are about to start could
|
|
226961
|
+
* finish, be seen as "at or below the head we later record", and be suppressed
|
|
226962
|
+
* as history. Returns false when the baseline could not be recorded, in which
|
|
226963
|
+
* case the caller must NOT start the turn.
|
|
226964
|
+
*/
|
|
226965
|
+
async prepareForNewTurn(localSessionId) {
|
|
226966
|
+
const mapping = await this.storage.remoteSessionMappings.getByLocal(localSessionId);
|
|
226967
|
+
if (!mapping) return true;
|
|
226968
|
+
await this.extendWatch(localSessionId);
|
|
226969
|
+
if (mapping.notification_sync_start !== "from_now") return true;
|
|
226970
|
+
const existing = await this.storage.notificationSyncCursors.get(
|
|
226971
|
+
mapping.remote_server_id,
|
|
226972
|
+
mapping.remote_session_id
|
|
226973
|
+
);
|
|
226974
|
+
if (existing !== void 0) return true;
|
|
226975
|
+
const target = await this.resolveTarget(mapping);
|
|
226976
|
+
if (!target) {
|
|
226977
|
+
console.warn(`[RemoteNotifications] no remote link for ${localSessionId}; skipping baseline`);
|
|
226978
|
+
return true;
|
|
226979
|
+
}
|
|
226980
|
+
const result = await this.query(target, [{ sessionId: mapping.remote_session_id, after: 0, headOnly: true }]);
|
|
226981
|
+
if (!result.ok) {
|
|
226982
|
+
console.warn(
|
|
226983
|
+
`[RemoteNotifications] baseline for ${localSessionId} failed (status ${result.status}); refusing to start the turn`
|
|
226984
|
+
);
|
|
226985
|
+
return false;
|
|
226986
|
+
}
|
|
226987
|
+
const sessions = this.parseSessions(result);
|
|
226988
|
+
const head = sessions.find((s3) => s3.sessionId === mapping.remote_session_id)?.headCursor;
|
|
226989
|
+
if (head === void 0) {
|
|
226990
|
+
console.warn(`[RemoteNotifications] baseline response for ${localSessionId} omitted the session`);
|
|
226991
|
+
return false;
|
|
226992
|
+
}
|
|
226993
|
+
await this.storage.notificationSyncCursors.initializeIfAbsent(
|
|
226994
|
+
mapping.remote_server_id,
|
|
226995
|
+
mapping.remote_session_id,
|
|
226996
|
+
head
|
|
226997
|
+
);
|
|
226998
|
+
return true;
|
|
226999
|
+
}
|
|
227000
|
+
/** Sweep every mapped remote server. */
|
|
227001
|
+
async syncAll(opts) {
|
|
227002
|
+
const mappings = await this.candidates(opts);
|
|
227003
|
+
for (const [serverId, group2] of groupByServer(mappings)) {
|
|
227004
|
+
await this.syncMappings(serverId, group2);
|
|
227005
|
+
}
|
|
227006
|
+
}
|
|
227007
|
+
/** Sweep one server — used when a reverse connection comes online. */
|
|
227008
|
+
async syncServer(remoteServerId, opts) {
|
|
227009
|
+
const mappings = (await this.candidates(opts)).filter((m2) => m2.remote_server_id === remoteServerId);
|
|
227010
|
+
if (mappings.length > 0) await this.syncMappings(remoteServerId, mappings);
|
|
227011
|
+
}
|
|
227012
|
+
/**
|
|
227013
|
+
* Mappings this sweep should poll: the persisted watch set, plus any session
|
|
227014
|
+
* currently believed to be running.
|
|
227015
|
+
*
|
|
227016
|
+
* The union is what keeps a turn longer than WATCH_WINDOW_MS covered. Its
|
|
227017
|
+
* window is extended when the turn starts, but a long turn can go silent for
|
|
227018
|
+
* hours; without this the mapping would lapse out of the periodic set and its
|
|
227019
|
+
* eventual milestone would wait for a server restart or a remote reconnect.
|
|
227020
|
+
*/
|
|
227021
|
+
async candidates(opts) {
|
|
227022
|
+
const mappings = await this.storage.remoteSessionMappings.getNotificationSyncCandidates({
|
|
227023
|
+
now: Date.now(),
|
|
227024
|
+
includeExpired: opts.includeExpired
|
|
227025
|
+
});
|
|
227026
|
+
if (opts.includeExpired || this.activeRemoteSessions.size === 0) return mappings;
|
|
227027
|
+
const present = new Set(mappings.map((m2) => m2.local_session_id));
|
|
227028
|
+
for (const localSessionId of this.activeRemoteSessions) {
|
|
227029
|
+
if (present.has(localSessionId)) continue;
|
|
227030
|
+
const mapping = await this.storage.remoteSessionMappings.getByLocal(localSessionId);
|
|
227031
|
+
if (mapping) mappings.push(mapping);
|
|
227032
|
+
}
|
|
227033
|
+
return mappings;
|
|
227034
|
+
}
|
|
227035
|
+
async syncMappings(remoteServerId, mappings) {
|
|
227036
|
+
for (let i = 0; i < mappings.length; i += MAX_SESSIONS_PER_REQUEST) {
|
|
227037
|
+
const chunk = mappings.slice(i, i + MAX_SESSIONS_PER_REQUEST);
|
|
227038
|
+
await this.syncChunk(remoteServerId, chunk).catch(
|
|
227039
|
+
(err) => console.warn(`[RemoteNotifications] chunk sync for ${remoteServerId} failed:`, err)
|
|
227040
|
+
);
|
|
227041
|
+
}
|
|
227042
|
+
}
|
|
227043
|
+
async syncChunk(remoteServerId, chunk) {
|
|
227044
|
+
const target = await this.resolveTarget(chunk[0]);
|
|
227045
|
+
if (!target) {
|
|
227046
|
+
console.warn(`[RemoteNotifications] no remote link for server ${remoteServerId}; skipping ${chunk.length} mapping(s)`);
|
|
227047
|
+
return;
|
|
227048
|
+
}
|
|
227049
|
+
let pending = new Map(chunk.map((m2) => [m2.remote_session_id, m2]));
|
|
227050
|
+
for (let page = 0; page < MAX_PAGES_PER_SYNC && pending.size > 0; page++) {
|
|
227051
|
+
const cursors = await this.storage.notificationSyncCursors.getMany(
|
|
227052
|
+
remoteServerId,
|
|
227053
|
+
[...pending.keys()]
|
|
227054
|
+
);
|
|
227055
|
+
const requests = [...pending.values()].map((mapping) => {
|
|
227056
|
+
const cursor = cursors.get(mapping.remote_session_id);
|
|
227057
|
+
if (mapping.notification_sync_start === "from_now" && cursor === void 0) {
|
|
227058
|
+
return { sessionId: mapping.remote_session_id, after: 0, headOnly: true };
|
|
227059
|
+
}
|
|
227060
|
+
return { sessionId: mapping.remote_session_id, after: cursor ?? 0 };
|
|
227061
|
+
});
|
|
227062
|
+
const result = await this.query(target, requests);
|
|
227063
|
+
if (!result.ok) {
|
|
227064
|
+
console.warn(`[RemoteNotifications] outbox query to ${remoteServerId} failed (status ${result.status})`);
|
|
227065
|
+
return;
|
|
227066
|
+
}
|
|
227067
|
+
const byId = new Map(this.parseSessions(result).map((s3) => [s3.sessionId, s3]));
|
|
227068
|
+
const next = /* @__PURE__ */ new Map();
|
|
227069
|
+
for (const request of requests) {
|
|
227070
|
+
const mapping = pending.get(request.sessionId);
|
|
227071
|
+
const session = byId.get(request.sessionId);
|
|
227072
|
+
if (!session) continue;
|
|
227073
|
+
if ("headOnly" in request && request.headOnly) {
|
|
227074
|
+
await this.storage.notificationSyncCursors.initializeIfAbsent(
|
|
227075
|
+
remoteServerId,
|
|
227076
|
+
mapping.remote_session_id,
|
|
227077
|
+
session.headCursor
|
|
227078
|
+
);
|
|
227079
|
+
continue;
|
|
227080
|
+
}
|
|
227081
|
+
const imported = await this.importSession(remoteServerId, mapping, session);
|
|
227082
|
+
if (imported && session.hasMore) next.set(request.sessionId, mapping);
|
|
227083
|
+
}
|
|
227084
|
+
pending = next;
|
|
227085
|
+
}
|
|
227086
|
+
}
|
|
227087
|
+
/**
|
|
227088
|
+
* Import one session's page. Returns false when the batch was rejected, in
|
|
227089
|
+
* which case its cursor is untouched.
|
|
227090
|
+
*/
|
|
227091
|
+
async importSession(remoteServerId, mapping, session) {
|
|
227092
|
+
for (const event of session.events) {
|
|
227093
|
+
if (event.session_id !== mapping.remote_session_id) {
|
|
227094
|
+
console.warn(
|
|
227095
|
+
`[RemoteNotifications] ${remoteServerId}: event ${event.id} claims session ${event.session_id}, expected ${mapping.remote_session_id}; rejecting batch`
|
|
227096
|
+
);
|
|
227097
|
+
return false;
|
|
227098
|
+
}
|
|
227099
|
+
}
|
|
227100
|
+
const ownerId = await this.storage.projects.getOwnerId(mapping.project_id);
|
|
227101
|
+
if (ownerId === void 0) {
|
|
227102
|
+
console.warn(`[RemoteNotifications] project ${mapping.project_id} not found; skipping ${mapping.local_session_id}`);
|
|
227103
|
+
return false;
|
|
227104
|
+
}
|
|
227105
|
+
for (const event of session.events) {
|
|
227106
|
+
const notification = await this.notificationService.buildNotification(event, {
|
|
227107
|
+
id: localNotificationId(remoteServerId, event.id),
|
|
227108
|
+
userId: ownerId || "local",
|
|
227109
|
+
projectId: mapping.project_id,
|
|
227110
|
+
sessionId: mapping.local_session_id,
|
|
227111
|
+
workflowRunId: event.workflow_run_id ? localWorkflowRunId(remoteServerId, mapping.project_id, event.workflow_run_id) : null
|
|
227112
|
+
});
|
|
227113
|
+
const { inserted } = await this.storage.notifications.importRemote({
|
|
227114
|
+
notification,
|
|
227115
|
+
remoteServerId,
|
|
227116
|
+
remoteSessionId: mapping.remote_session_id,
|
|
227117
|
+
seq: event.seq
|
|
227118
|
+
});
|
|
227119
|
+
if (inserted) this.notificationService.emitCreated(notification);
|
|
227120
|
+
}
|
|
227121
|
+
if (session.events.length > 0) await this.extendWatch(mapping.local_session_id);
|
|
227122
|
+
return true;
|
|
227123
|
+
}
|
|
227124
|
+
async query(target, sessions) {
|
|
227125
|
+
return this.proxy(
|
|
227126
|
+
target.serverId,
|
|
227127
|
+
target.url,
|
|
227128
|
+
target.apiKey,
|
|
227129
|
+
"POST",
|
|
227130
|
+
OUTBOX_QUERY_PATH,
|
|
227131
|
+
{ sessions, limitPerSession: MAX_EVENTS_PER_SESSION },
|
|
227132
|
+
{ timeoutMs: QUERY_TIMEOUT_MS, reverseConnectManager: this.reverseConnectManager }
|
|
227133
|
+
);
|
|
227134
|
+
}
|
|
227135
|
+
parseSessions(result) {
|
|
227136
|
+
const sessions = result.data?.sessions;
|
|
227137
|
+
return Array.isArray(sessions) ? sessions : [];
|
|
227138
|
+
}
|
|
227139
|
+
/**
|
|
227140
|
+
* URL/apiKey come from project_remotes — the authoritative source, same as the
|
|
227141
|
+
* remoteSessionMap hydration in shared-services. Reverse-connect rows have an
|
|
227142
|
+
* empty URL and are routed by server id instead.
|
|
227143
|
+
*/
|
|
227144
|
+
async resolveTarget(mapping) {
|
|
227145
|
+
const remote = await this.storage.projectRemotes.getByProjectAndServer(
|
|
227146
|
+
mapping.project_id,
|
|
227147
|
+
mapping.remote_server_id
|
|
227148
|
+
);
|
|
227149
|
+
if (!remote) return null;
|
|
227150
|
+
return {
|
|
227151
|
+
serverId: mapping.remote_server_id,
|
|
227152
|
+
url: remote.server_url ?? "",
|
|
227153
|
+
apiKey: remote.server_api_key || ""
|
|
227154
|
+
};
|
|
227155
|
+
}
|
|
227156
|
+
};
|
|
227157
|
+
function groupByServer(mappings) {
|
|
227158
|
+
const groups = /* @__PURE__ */ new Map();
|
|
227159
|
+
for (const mapping of mappings) {
|
|
227160
|
+
const existing = groups.get(mapping.remote_server_id);
|
|
227161
|
+
if (existing) existing.push(mapping);
|
|
227162
|
+
else groups.set(mapping.remote_server_id, [mapping]);
|
|
227163
|
+
}
|
|
227164
|
+
return groups;
|
|
227165
|
+
}
|
|
227166
|
+
|
|
226299
227167
|
// src/remote-agent-sessions.ts
|
|
226300
227168
|
async function createRemoteAgentSession(deps, params) {
|
|
226301
227169
|
const { projectId, agentMode, remoteConfig, branch, permissionMode, agentType, force, userId } = params;
|
|
@@ -226332,7 +227200,8 @@ async function createRemoteAgentSession(deps, params) {
|
|
|
226332
227200
|
deps.remoteSessionMap.delete(localSessionId);
|
|
226333
227201
|
return { ok: false, status: 409, data: { error: "Remote returned an unexpected session id; upgrade the remote" } };
|
|
226334
227202
|
}
|
|
226335
|
-
await deps.remoteSessionMappings.upsert(localSessionId, projectId, agentMode, remoteSessionId, branch ?? null);
|
|
227203
|
+
await deps.remoteSessionMappings.upsert(localSessionId, projectId, agentMode, remoteSessionId, branch ?? null, "from_start");
|
|
227204
|
+
await deps.remoteSessionMappings.extendNotificationWatch(localSessionId, Date.now() + WATCH_WINDOW_MS).catch((err) => console.warn("[RemoteSession] notification watch extend failed:", err));
|
|
226336
227205
|
await deps.storage.searchCache.noteSessionCreated({
|
|
226337
227206
|
localSessionId,
|
|
226338
227207
|
projectId,
|
|
@@ -228975,6 +229844,15 @@ var WorkflowError = class extends Error {
|
|
|
228975
229844
|
}
|
|
228976
229845
|
code;
|
|
228977
229846
|
};
|
|
229847
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "cancelled", "failed"]);
|
|
229848
|
+
var REVIEWER_TURN = {
|
|
229849
|
+
origin: "workflow",
|
|
229850
|
+
notificationDisposition: "milestone-managed"
|
|
229851
|
+
};
|
|
229852
|
+
var FEEDBACK_TURN = {
|
|
229853
|
+
origin: "workflow",
|
|
229854
|
+
notificationDisposition: "result"
|
|
229855
|
+
};
|
|
228978
229856
|
var MAX_CONTEXT_CHARS = 2e3;
|
|
228979
229857
|
var MAX_SELF_REPORT_CHARS = 4e3;
|
|
228980
229858
|
var SELF_REPORT_MIN_CHARS = 80;
|
|
@@ -229170,10 +230048,57 @@ var WorkflowEngine = class {
|
|
|
229170
230048
|
if (participant.runId === runId) this.participants.delete(sid);
|
|
229171
230049
|
}
|
|
229172
230050
|
}
|
|
230051
|
+
/**
|
|
230052
|
+
* The single place a run becomes `failed`. Goes through a guarded transition
|
|
230053
|
+
* out of the run's CURRENT status rather than an unconditional status write,
|
|
230054
|
+
* so the failure milestone can only be created by the caller that actually
|
|
230055
|
+
* performed the transition — a run that concurrently completed or was
|
|
230056
|
+
* cancelled fails the CAS and writes nothing.
|
|
230057
|
+
*
|
|
230058
|
+
* The milestone targets the participant the user should inspect: the reviewer
|
|
230059
|
+
* when one exists, otherwise the source session (a run can fail before its
|
|
230060
|
+
* reviewer is ever created).
|
|
230061
|
+
*/
|
|
229173
230062
|
async failRun(run2, error48) {
|
|
229174
|
-
|
|
230063
|
+
if (TERMINAL_STATUSES.has(run2.status)) return;
|
|
230064
|
+
const from = run2.status;
|
|
230065
|
+
const ok = await this.storage.workflowRuns.transitionWithOutbox(
|
|
230066
|
+
run2.id,
|
|
230067
|
+
from,
|
|
230068
|
+
"failed",
|
|
230069
|
+
{ error: error48 },
|
|
230070
|
+
{
|
|
230071
|
+
// The state the run failed OUT OF: two distinct failures of one run are
|
|
230072
|
+
// distinct attention milestones, while a retried transition out of the
|
|
230073
|
+
// same state is not.
|
|
230074
|
+
id: workflowFailedId(run2.id, from),
|
|
230075
|
+
kind: "workflow_failed",
|
|
230076
|
+
project_id: run2.project_id,
|
|
230077
|
+
branch: run2.branch,
|
|
230078
|
+
session_id: run2.reviewer_session_id ?? run2.source_session_id,
|
|
230079
|
+
workflow_run_id: run2.id,
|
|
230080
|
+
created_at: Date.now()
|
|
230081
|
+
}
|
|
230082
|
+
);
|
|
230083
|
+
if (!ok) return;
|
|
230084
|
+
const failed = await this.storage.workflowRuns.getById(run2.id);
|
|
229175
230085
|
if (failed) this.untrackRun(failed);
|
|
230086
|
+
this.onMilestoneCreated?.();
|
|
230087
|
+
}
|
|
230088
|
+
/** Test seam for the failure path (see failRun). */
|
|
230089
|
+
async failRunForTest(runId, error48) {
|
|
230090
|
+
const run2 = await this.storage.workflowRuns.getById(runId);
|
|
230091
|
+
if (run2) await this.failRun(run2, error48);
|
|
229176
230092
|
}
|
|
230093
|
+
/**
|
|
230094
|
+
* Injected by shared-services: nudges NotificationService to drain the local
|
|
230095
|
+
* outbox promptly. Latency only — correctness rests on the periodic/startup
|
|
230096
|
+
* drains, since this can't fire if the process dies right after the commit.
|
|
230097
|
+
*/
|
|
230098
|
+
setMilestoneListener(listener) {
|
|
230099
|
+
this.onMilestoneCreated = listener;
|
|
230100
|
+
}
|
|
230101
|
+
onMilestoneCreated = null;
|
|
229177
230102
|
/** Sync check used by ChatSessionManager before waking the commander model. */
|
|
229178
230103
|
shouldSuppressAgentEvent(sessionId) {
|
|
229179
230104
|
return this.participants.get(sessionId)?.role === "reviewer";
|
|
@@ -229299,7 +230224,7 @@ var WorkflowEngine = class {
|
|
|
229299
230224
|
reviewFocus: opts.reviewFocus ?? null,
|
|
229300
230225
|
target
|
|
229301
230226
|
});
|
|
229302
|
-
const sent = await this.agentOps.sendUserMessage(opts.reviewerSessionId, prompt, opts.project.path, void 0,
|
|
230227
|
+
const sent = await this.agentOps.sendUserMessage(opts.reviewerSessionId, prompt, opts.project.path, void 0, REVIEWER_TURN).catch(() => false);
|
|
229303
230228
|
if (!sent) {
|
|
229304
230229
|
await this.failRun(run2, "\u5411\u4E0A\u6B21 reviewer \u6295\u9012\u590D\u5BA1\u4EFB\u52A1\u5931\u8D25");
|
|
229305
230230
|
throw new WorkflowError("send-failed", "\u5411\u4E0A\u6B21 reviewer \u6295\u9012\u590D\u5BA1\u4EFB\u52A1\u5931\u8D25");
|
|
@@ -229344,13 +230269,9 @@ var WorkflowEngine = class {
|
|
|
229344
230269
|
target,
|
|
229345
230270
|
scope
|
|
229346
230271
|
});
|
|
229347
|
-
const sent = await this.agentOps.sendUserMessage(reviewerId, prompt, opts.project.path, void 0,
|
|
230272
|
+
const sent = await this.agentOps.sendUserMessage(reviewerId, prompt, opts.project.path, void 0, REVIEWER_TURN);
|
|
229348
230273
|
if (!sent) {
|
|
229349
|
-
|
|
229350
|
-
status: "failed",
|
|
229351
|
-
error: "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25"
|
|
229352
|
-
});
|
|
229353
|
-
if (failed) this.untrackRun(failed);
|
|
230274
|
+
await this.failRun({ ...run2, reviewer_session_id: reviewerId }, "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
|
|
229354
230275
|
throw new WorkflowError("spawn-failed", "\u5411 reviewer \u6295\u9012\u4EFB\u52A1\u5931\u8D25");
|
|
229355
230276
|
}
|
|
229356
230277
|
const updated = await this.storage.workflowRuns.update(run2.id, { reviewer_session_id: reviewerId });
|
|
@@ -229359,11 +230280,7 @@ var WorkflowEngine = class {
|
|
|
229359
230280
|
return updated;
|
|
229360
230281
|
} catch (err) {
|
|
229361
230282
|
if (err instanceof WorkflowError && err.code === "spawn-failed") throw err;
|
|
229362
|
-
|
|
229363
|
-
status: "failed",
|
|
229364
|
-
error: `\u521B\u5EFA reviewer \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
|
|
229365
|
-
});
|
|
229366
|
-
if (failed) this.untrackRun(failed);
|
|
230283
|
+
await this.failRun(run2, `\u521B\u5EFA reviewer \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
|
|
229367
230284
|
throw new WorkflowError("spawn-failed", "\u521B\u5EFA reviewer session \u5931\u8D25");
|
|
229368
230285
|
}
|
|
229369
230286
|
} catch (err) {
|
|
@@ -229388,11 +230305,28 @@ var WorkflowEngine = class {
|
|
|
229388
230305
|
}
|
|
229389
230306
|
} catch {
|
|
229390
230307
|
}
|
|
229391
|
-
const ok = await this.storage.workflowRuns.
|
|
229392
|
-
|
|
229393
|
-
|
|
229394
|
-
|
|
230308
|
+
const ok = await this.storage.workflowRuns.transitionWithOutbox(
|
|
230309
|
+
run2.id,
|
|
230310
|
+
"waiting_reviewer",
|
|
230311
|
+
"waiting_feedback",
|
|
230312
|
+
{
|
|
230313
|
+
feedback_snapshot: feedback,
|
|
230314
|
+
...driftNote ? { error: driftNote } : {}
|
|
230315
|
+
},
|
|
230316
|
+
{
|
|
230317
|
+
id: reviewReadyId(run2.id),
|
|
230318
|
+
kind: "review_ready",
|
|
230319
|
+
project_id: run2.project_id,
|
|
230320
|
+
branch: run2.branch,
|
|
230321
|
+
// Target the reviewer: that's where the feedback and the
|
|
230322
|
+
// approve/discard controls are.
|
|
230323
|
+
session_id: event.sessionId,
|
|
230324
|
+
workflow_run_id: run2.id,
|
|
230325
|
+
created_at: Date.now()
|
|
230326
|
+
}
|
|
230327
|
+
);
|
|
229395
230328
|
if (!ok) return;
|
|
230329
|
+
this.onMilestoneCreated?.();
|
|
229396
230330
|
const updated = await this.storage.workflowRuns.getById(run2.id);
|
|
229397
230331
|
if (updated) this.emitRunUpdated(updated);
|
|
229398
230332
|
}
|
|
@@ -229409,7 +230343,7 @@ var WorkflowEngine = class {
|
|
|
229409
230343
|
if (!claimed) throw new WorkflowError("bad-state", "run \u72B6\u6001\u5DF2\u53D8\u5316\uFF08\u53EF\u80FD\u5DF2\u88AB\u5904\u7406\uFF09");
|
|
229410
230344
|
const feedback = editedPayload ?? run2.feedback_snapshot ?? "";
|
|
229411
230345
|
const project = await this.storage.projects.getById(run2.project_id);
|
|
229412
|
-
const ok = await this.agentOps.sendUserMessage(run2.source_session_id, buildFeedbackMessage(feedback), project?.path ?? void 0, void 0,
|
|
230346
|
+
const ok = await this.agentOps.sendUserMessage(run2.source_session_id, buildFeedbackMessage(feedback), project?.path ?? void 0, void 0, FEEDBACK_TURN).catch(() => false);
|
|
229413
230347
|
if (!ok) {
|
|
229414
230348
|
await this.storage.workflowRuns.transition(runId, "sending_feedback", "waiting_feedback", {
|
|
229415
230349
|
error: "\u53D1\u9001\u5931\u8D25\uFF1A\u76EE\u6807 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"
|
|
@@ -229430,7 +230364,7 @@ var WorkflowEngine = class {
|
|
|
229430
230364
|
async cancelRun(runId, reason) {
|
|
229431
230365
|
const run2 = await this.storage.workflowRuns.getById(runId);
|
|
229432
230366
|
if (!run2) return void 0;
|
|
229433
|
-
if (
|
|
230367
|
+
if (TERMINAL_STATUSES.has(run2.status)) return run2;
|
|
229434
230368
|
const patch = reason ? { error: reason } : void 0;
|
|
229435
230369
|
const cancelled = await this.storage.workflowRuns.transition(runId, "waiting_reviewer", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_feedback", "cancelled", patch);
|
|
229436
230370
|
if (!cancelled) {
|
|
@@ -231244,6 +232178,154 @@ var SchedulerService = class {
|
|
|
231244
232178
|
}
|
|
231245
232179
|
};
|
|
231246
232180
|
|
|
232181
|
+
// src/notification-service.ts
|
|
232182
|
+
var TITLE_BY_KIND = {
|
|
232183
|
+
review_ready: "Review feedback is ready",
|
|
232184
|
+
session_result_ready: "Session result is ready",
|
|
232185
|
+
session_failed: "Session failed",
|
|
232186
|
+
workflow_failed: "Workflow needs attention"
|
|
232187
|
+
};
|
|
232188
|
+
var PLACEHOLDER_TITLES = /* @__PURE__ */ new Set(["New Session", "Generating title\u2026", "Generating title..."]);
|
|
232189
|
+
var LOCAL_CURSOR_KEY = "notification_local_cursor";
|
|
232190
|
+
var OUTBOX_PAGE_SIZE = 100;
|
|
232191
|
+
var READ_HISTORY_PER_USER = 500;
|
|
232192
|
+
var OUTBOX_RETENTION_DAYS = 90;
|
|
232193
|
+
var DRAIN_INTERVAL_MS = 3e4;
|
|
232194
|
+
var CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
232195
|
+
function notificationBody(opts) {
|
|
232196
|
+
const title = opts.sessionTitle?.trim();
|
|
232197
|
+
if (title && !PLACEHOLDER_TITLES.has(title)) return title;
|
|
232198
|
+
if (opts.branch) return opts.branch;
|
|
232199
|
+
return opts.projectName?.trim() || null;
|
|
232200
|
+
}
|
|
232201
|
+
function notificationTitle(kind) {
|
|
232202
|
+
return TITLE_BY_KIND[kind] ?? "Update";
|
|
232203
|
+
}
|
|
232204
|
+
var NotificationService = class {
|
|
232205
|
+
constructor(storage, eventBus) {
|
|
232206
|
+
this.storage = storage;
|
|
232207
|
+
this.eventBus = eventBus;
|
|
232208
|
+
}
|
|
232209
|
+
storage;
|
|
232210
|
+
eventBus;
|
|
232211
|
+
drainTimer = null;
|
|
232212
|
+
cleanupTimer = null;
|
|
232213
|
+
/** Serializes drains so a nudge can't interleave with the periodic tick. */
|
|
232214
|
+
drainChain = Promise.resolve();
|
|
232215
|
+
stopped = false;
|
|
232216
|
+
/**
|
|
232217
|
+
* Start the background timers. Unref'd: a pending drain must never be the
|
|
232218
|
+
* reason a CLI process refuses to exit.
|
|
232219
|
+
*/
|
|
232220
|
+
start() {
|
|
232221
|
+
if (this.drainTimer || this.stopped) return;
|
|
232222
|
+
this.drainTimer = setInterval(() => this.requestDrain(), DRAIN_INTERVAL_MS);
|
|
232223
|
+
this.drainTimer.unref?.();
|
|
232224
|
+
this.cleanupTimer = setInterval(() => {
|
|
232225
|
+
void this.cleanup().catch((err) => console.warn("[Notifications] cleanup failed:", err));
|
|
232226
|
+
}, CLEANUP_INTERVAL_MS);
|
|
232227
|
+
this.cleanupTimer.unref?.();
|
|
232228
|
+
}
|
|
232229
|
+
shutdown() {
|
|
232230
|
+
this.stopped = true;
|
|
232231
|
+
if (this.drainTimer) clearInterval(this.drainTimer);
|
|
232232
|
+
if (this.cleanupTimer) clearInterval(this.cleanupTimer);
|
|
232233
|
+
this.drainTimer = null;
|
|
232234
|
+
this.cleanupTimer = null;
|
|
232235
|
+
}
|
|
232236
|
+
/**
|
|
232237
|
+
* Fire-and-forget nudge used right after a milestone lands, so the bell is
|
|
232238
|
+
* fast in the common case. Queued behind any in-flight drain.
|
|
232239
|
+
*/
|
|
232240
|
+
requestDrain() {
|
|
232241
|
+
if (this.stopped) return;
|
|
232242
|
+
this.drainChain = this.drainChain.then(() => this.drainLocal()).catch((err) => console.warn("[Notifications] local drain failed:", err));
|
|
232243
|
+
}
|
|
232244
|
+
/**
|
|
232245
|
+
* Import every local outbox event after the durable cursor.
|
|
232246
|
+
*
|
|
232247
|
+
* The cursor advances per page AFTER its rows are inserted, so a crash
|
|
232248
|
+
* mid-drain replays the page — harmless, because insertion is idempotent on
|
|
232249
|
+
* the milestone id and `notification:created` is only emitted for rows this
|
|
232250
|
+
* call actually inserted.
|
|
232251
|
+
*/
|
|
232252
|
+
async drainLocal() {
|
|
232253
|
+
for (; ; ) {
|
|
232254
|
+
const cursor = await this.readLocalCursor();
|
|
232255
|
+
const events = await this.storage.notificationOutbox.listAfter(cursor, OUTBOX_PAGE_SIZE);
|
|
232256
|
+
if (events.length === 0) return;
|
|
232257
|
+
for (const event of events) {
|
|
232258
|
+
await this.importLocalEvent(event);
|
|
232259
|
+
}
|
|
232260
|
+
await this.storage.settings.set(LOCAL_CURSOR_KEY, String(events[events.length - 1].seq));
|
|
232261
|
+
if (events.length < OUTBOX_PAGE_SIZE) return;
|
|
232262
|
+
}
|
|
232263
|
+
}
|
|
232264
|
+
async importLocalEvent(event) {
|
|
232265
|
+
const ownerId = await this.storage.projects.getOwnerId(event.project_id);
|
|
232266
|
+
if (ownerId === void 0) {
|
|
232267
|
+
console.warn(`[Notifications] dropping outbox event ${event.id}: project ${event.project_id} not found`);
|
|
232268
|
+
return;
|
|
232269
|
+
}
|
|
232270
|
+
const notification = await this.buildNotification(event, {
|
|
232271
|
+
id: event.id,
|
|
232272
|
+
// "" is the solo-mode column default; the browser routes read the
|
|
232273
|
+
// "local" sentinel, so normalize here rather than in every query.
|
|
232274
|
+
userId: ownerId || "local",
|
|
232275
|
+
sessionId: event.session_id,
|
|
232276
|
+
projectId: event.project_id,
|
|
232277
|
+
workflowRunId: event.workflow_run_id
|
|
232278
|
+
});
|
|
232279
|
+
if (await this.storage.notifications.insert(notification)) {
|
|
232280
|
+
this.eventBus.emit({ type: "notification:created", projectId: notification.project_id, notification });
|
|
232281
|
+
}
|
|
232282
|
+
}
|
|
232283
|
+
/**
|
|
232284
|
+
* Assemble the persisted row. `ids` is separate from the event because remote
|
|
232285
|
+
* imports substitute front-local identities (see RemoteNotificationSync) while
|
|
232286
|
+
* reusing this exact copy generation.
|
|
232287
|
+
*/
|
|
232288
|
+
async buildNotification(event, ids) {
|
|
232289
|
+
const session = ids.sessionId ? await this.storage.agentSessions.getById(ids.sessionId) : void 0;
|
|
232290
|
+
const project = await this.storage.projects.getById(ids.projectId);
|
|
232291
|
+
return {
|
|
232292
|
+
id: ids.id,
|
|
232293
|
+
user_id: ids.userId,
|
|
232294
|
+
kind: event.kind,
|
|
232295
|
+
project_id: ids.projectId,
|
|
232296
|
+
branch: event.branch,
|
|
232297
|
+
session_id: ids.sessionId,
|
|
232298
|
+
workflow_run_id: ids.workflowRunId,
|
|
232299
|
+
title: notificationTitle(event.kind),
|
|
232300
|
+
body: notificationBody({
|
|
232301
|
+
sessionTitle: session?.title,
|
|
232302
|
+
branch: event.branch,
|
|
232303
|
+
projectName: project?.name
|
|
232304
|
+
}),
|
|
232305
|
+
created_at: event.created_at,
|
|
232306
|
+
read_at: null
|
|
232307
|
+
};
|
|
232308
|
+
}
|
|
232309
|
+
/** Emit for a row a remote import just inserted. */
|
|
232310
|
+
emitCreated(notification) {
|
|
232311
|
+
this.eventBus.emit({ type: "notification:created", projectId: notification.project_id, notification });
|
|
232312
|
+
}
|
|
232313
|
+
/** Retention: cap read history per user, prune aged worker outbox rows. */
|
|
232314
|
+
async cleanup() {
|
|
232315
|
+
await this.storage.notifications.cleanup(READ_HISTORY_PER_USER);
|
|
232316
|
+
await this.storage.notificationOutbox.pruneOlderThan(Date.now() - OUTBOX_RETENTION_DAYS * 864e5);
|
|
232317
|
+
}
|
|
232318
|
+
async readLocalCursor() {
|
|
232319
|
+
const raw = await this.storage.settings.get(LOCAL_CURSOR_KEY);
|
|
232320
|
+
const parsed = raw === void 0 ? 0 : Number(raw);
|
|
232321
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
232322
|
+
}
|
|
232323
|
+
/** Simulates a crash between inbox insertion and cursor commit. */
|
|
232324
|
+
async resetLocalCursorForTest() {
|
|
232325
|
+
await this.storage.settings.set(LOCAL_CURSOR_KEY, "0");
|
|
232326
|
+
}
|
|
232327
|
+
};
|
|
232328
|
+
|
|
231247
232329
|
// src/plugins/shared-services.ts
|
|
231248
232330
|
var sharedServices = async (fastify2, opts) => {
|
|
231249
232331
|
const processManager = new ProcessManager(opts.storage);
|
|
@@ -231382,8 +232464,25 @@ var sharedServices = async (fastify2, opts) => {
|
|
|
231382
232464
|
fastify2.decorate("browserManager", browserManager);
|
|
231383
232465
|
fastify2.decorate("scheduler", scheduler);
|
|
231384
232466
|
agentSessionManager.setEventBus(eventBus);
|
|
232467
|
+
const notificationService = new NotificationService(opts.storage, eventBus);
|
|
232468
|
+
fastify2.decorate("notificationService", notificationService);
|
|
232469
|
+
agentSessionManager.setMilestoneListener(() => notificationService.requestDrain());
|
|
232470
|
+
const remoteNotificationSync = new RemoteNotificationSync({
|
|
232471
|
+
storage: opts.storage,
|
|
232472
|
+
notificationService,
|
|
232473
|
+
reverseConnectManager
|
|
232474
|
+
});
|
|
232475
|
+
fastify2.decorate("remoteNotificationSync", remoteNotificationSync);
|
|
232476
|
+
remoteNotificationSync.setEventBus(eventBus);
|
|
232477
|
+
reverseConnectManager.setStatusChangeHandler((remoteServerId, status) => {
|
|
232478
|
+
if (status !== "online") return;
|
|
232479
|
+
remoteNotificationSync.enqueue(
|
|
232480
|
+
() => remoteNotificationSync.syncServer(remoteServerId, { includeExpired: true })
|
|
232481
|
+
);
|
|
232482
|
+
});
|
|
231385
232483
|
const workflowEngine = new WorkflowEngine(opts.storage, agentSessionManager);
|
|
231386
232484
|
workflowEngine.setEventBus(eventBus);
|
|
232485
|
+
workflowEngine.setMilestoneListener(() => notificationService.requestDrain());
|
|
231387
232486
|
await workflowEngine.init();
|
|
231388
232487
|
fastify2.decorate("workflowEngine", workflowEngine);
|
|
231389
232488
|
chatSessionManager.setWorkflowEngine(workflowEngine);
|
|
@@ -231393,6 +232492,10 @@ var sharedServices = async (fastify2, opts) => {
|
|
|
231393
232492
|
processManager.setEventBus(eventBus);
|
|
231394
232493
|
scheduler.setEventBus(eventBus);
|
|
231395
232494
|
await scheduler.start();
|
|
232495
|
+
notificationService.start();
|
|
232496
|
+
notificationService.requestDrain();
|
|
232497
|
+
remoteNotificationSync.start();
|
|
232498
|
+
remoteNotificationSync.enqueue(() => remoteNotificationSync.syncAll({ includeExpired: true }));
|
|
231396
232499
|
void (async () => {
|
|
231397
232500
|
const savedRows = await opts.storage.remoteExecutorProcesses.getRunning();
|
|
231398
232501
|
const directUrlRows = savedRows.filter((r) => r.remote_url);
|
|
@@ -231408,6 +232511,8 @@ var sharedServices = async (fastify2, opts) => {
|
|
|
231408
232511
|
});
|
|
231409
232512
|
fastify2.addHook("onClose", async () => {
|
|
231410
232513
|
scheduler.shutdown();
|
|
232514
|
+
notificationService.shutdown();
|
|
232515
|
+
remoteNotificationSync.shutdown();
|
|
231411
232516
|
agentSessionManager.shutdown();
|
|
231412
232517
|
processManager.shutdown();
|
|
231413
232518
|
remotePatchCache.shutdown();
|
|
@@ -231416,10 +232521,10 @@ var sharedServices = async (fastify2, opts) => {
|
|
|
231416
232521
|
await browserManager.shutdown();
|
|
231417
232522
|
});
|
|
231418
232523
|
};
|
|
231419
|
-
var shared_services_default = (0,
|
|
232524
|
+
var shared_services_default = (0, import_fastify_plugin2.default)(sharedServices, { name: "shared-services" });
|
|
231420
232525
|
|
|
231421
232526
|
// src/routes/project-routes.ts
|
|
231422
|
-
var
|
|
232527
|
+
var import_fastify_plugin3 = __toESM(require_plugin2(), 1);
|
|
231423
232528
|
import path8 from "path";
|
|
231424
232529
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
231425
232530
|
import { exec as exec2 } from "child_process";
|
|
@@ -231472,7 +232577,7 @@ function sanitizeProject(project) {
|
|
|
231472
232577
|
const { remote_api_key, ...safe } = project;
|
|
231473
232578
|
return { ...safe, has_remote_api_key: !!remote_api_key };
|
|
231474
232579
|
}
|
|
231475
|
-
var
|
|
232580
|
+
var routes2 = async (fastify2) => {
|
|
231476
232581
|
fastify2.get("/api/projects", async (req, reply) => {
|
|
231477
232582
|
const userId = requireAuth(req, reply);
|
|
231478
232583
|
if (userId === null) return;
|
|
@@ -231734,12 +232839,12 @@ var routes = async (fastify2) => {
|
|
|
231734
232839
|
}
|
|
231735
232840
|
});
|
|
231736
232841
|
};
|
|
231737
|
-
var project_routes_default = (0,
|
|
232842
|
+
var project_routes_default = (0, import_fastify_plugin3.default)(routes2, { name: "project-routes" });
|
|
231738
232843
|
|
|
231739
232844
|
// src/routes/remote-routes.ts
|
|
231740
|
-
var
|
|
232845
|
+
var import_fastify_plugin4 = __toESM(require_plugin2(), 1);
|
|
231741
232846
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
231742
|
-
var
|
|
232847
|
+
var routes3 = async (fastify2) => {
|
|
231743
232848
|
fastify2.post("/api/remote/test-connection", async (req, reply) => {
|
|
231744
232849
|
const { url: url2, apiKey } = req.body;
|
|
231745
232850
|
if (!url2 || !apiKey) {
|
|
@@ -231791,17 +232896,17 @@ var routes2 = async (fastify2) => {
|
|
|
231791
232896
|
return reply.code(201).send({ project: safeProject });
|
|
231792
232897
|
});
|
|
231793
232898
|
};
|
|
231794
|
-
var remote_routes_default = (0,
|
|
232899
|
+
var remote_routes_default = (0, import_fastify_plugin4.default)(routes3, { name: "remote-routes" });
|
|
231795
232900
|
|
|
231796
232901
|
// src/routes/remote-server-routes.ts
|
|
231797
|
-
var
|
|
232902
|
+
var import_fastify_plugin5 = __toESM(require_plugin2(), 1);
|
|
231798
232903
|
function sanitizeServer(server) {
|
|
231799
232904
|
const { api_key: _3, connect_token: _t, ...safe } = server;
|
|
231800
232905
|
return safe;
|
|
231801
232906
|
}
|
|
231802
232907
|
var CROSS_REMOTE_ACCESS_VALUES = ["off", "read", "exec"];
|
|
231803
232908
|
var isCrossRemoteAccess = (value) => typeof value === "string" && CROSS_REMOTE_ACCESS_VALUES.includes(value);
|
|
231804
|
-
var
|
|
232909
|
+
var routes4 = async (fastify2) => {
|
|
231805
232910
|
fastify2.get("/api/remote-servers", async (request, reply) => {
|
|
231806
232911
|
const userId = requireAuth(request, reply);
|
|
231807
232912
|
if (userId === null) return;
|
|
@@ -231986,15 +233091,15 @@ var routes3 = async (fastify2) => {
|
|
|
231986
233091
|
}
|
|
231987
233092
|
);
|
|
231988
233093
|
};
|
|
231989
|
-
var remote_server_routes_default = (0,
|
|
233094
|
+
var remote_server_routes_default = (0, import_fastify_plugin5.default)(routes4, { name: "remote-server-routes" });
|
|
231990
233095
|
|
|
231991
233096
|
// src/routes/project-remote-routes.ts
|
|
231992
|
-
var
|
|
233097
|
+
var import_fastify_plugin6 = __toESM(require_plugin2(), 1);
|
|
231993
233098
|
function sanitizeProjectRemote(pr) {
|
|
231994
233099
|
const { server_api_key: _3, ...safe } = pr;
|
|
231995
233100
|
return safe;
|
|
231996
233101
|
}
|
|
231997
|
-
var
|
|
233102
|
+
var routes5 = async (fastify2) => {
|
|
231998
233103
|
fastify2.get(
|
|
231999
233104
|
"/api/projects/:id/remotes",
|
|
232000
233105
|
async (request, reply) => {
|
|
@@ -232088,12 +233193,12 @@ var routes4 = async (fastify2) => {
|
|
|
232088
233193
|
}
|
|
232089
233194
|
);
|
|
232090
233195
|
};
|
|
232091
|
-
var project_remote_routes_default = (0,
|
|
233196
|
+
var project_remote_routes_default = (0, import_fastify_plugin6.default)(routes5, { name: "project-remote-routes" });
|
|
232092
233197
|
|
|
232093
233198
|
// src/routes/executor-group-routes.ts
|
|
232094
|
-
var
|
|
233199
|
+
var import_fastify_plugin7 = __toESM(require_plugin2(), 1);
|
|
232095
233200
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
232096
|
-
var
|
|
233201
|
+
var routes6 = async (fastify2) => {
|
|
232097
233202
|
fastify2.get(
|
|
232098
233203
|
"/api/projects/:projectId/executor-groups",
|
|
232099
233204
|
async (req, reply) => {
|
|
@@ -232164,17 +233269,17 @@ var routes5 = async (fastify2) => {
|
|
|
232164
233269
|
}
|
|
232165
233270
|
);
|
|
232166
233271
|
};
|
|
232167
|
-
var executor_group_routes_default = (0,
|
|
233272
|
+
var executor_group_routes_default = (0, import_fastify_plugin7.default)(routes6, { name: "executor-group-routes" });
|
|
232168
233273
|
|
|
232169
233274
|
// src/routes/executor-routes.ts
|
|
232170
|
-
var
|
|
233275
|
+
var import_fastify_plugin8 = __toESM(require_plugin2(), 1);
|
|
232171
233276
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
232172
233277
|
function normalizeSqlTimestamp(value) {
|
|
232173
233278
|
if (!value) return null;
|
|
232174
233279
|
if (value.includes("T") && /(Z|[+-]\d{2}:?\d{2})$/.test(value)) return value;
|
|
232175
233280
|
return value.replace(" ", "T") + "Z";
|
|
232176
233281
|
}
|
|
232177
|
-
var
|
|
233282
|
+
var routes7 = async (fastify2) => {
|
|
232178
233283
|
fastify2.get(
|
|
232179
233284
|
"/api/projects/:projectId/executors",
|
|
232180
233285
|
async (req, reply) => {
|
|
@@ -232313,13 +233418,13 @@ var routes6 = async (fastify2) => {
|
|
|
232313
233418
|
return reply.code(200).send({ success: true });
|
|
232314
233419
|
});
|
|
232315
233420
|
};
|
|
232316
|
-
var executor_routes_default = (0,
|
|
233421
|
+
var executor_routes_default = (0, import_fastify_plugin8.default)(routes7, { name: "executor-routes" });
|
|
232317
233422
|
|
|
232318
233423
|
// src/routes/process-routes.ts
|
|
232319
|
-
var
|
|
233424
|
+
var import_fastify_plugin9 = __toESM(require_plugin2(), 1);
|
|
232320
233425
|
import path9 from "path";
|
|
232321
233426
|
import { randomUUID as randomUUID13 } from "crypto";
|
|
232322
|
-
var
|
|
233427
|
+
var routes8 = async (fastify2) => {
|
|
232323
233428
|
fastify2.post("/api/path/execute", async (req, reply) => {
|
|
232324
233429
|
const { path: projectPath, command, executor_type, prompt_provider, cwd, branch, pty: pty2 } = req.body;
|
|
232325
233430
|
if (!projectPath || !command) {
|
|
@@ -232529,10 +233634,10 @@ var routes7 = async (fastify2) => {
|
|
|
232529
233634
|
return reply.code(200).send({ processes });
|
|
232530
233635
|
});
|
|
232531
233636
|
};
|
|
232532
|
-
var process_routes_default = (0,
|
|
233637
|
+
var process_routes_default = (0, import_fastify_plugin9.default)(routes8, { name: "process-routes" });
|
|
232533
233638
|
|
|
232534
233639
|
// src/routes/worktree-routes.ts
|
|
232535
|
-
var
|
|
233640
|
+
var import_fastify_plugin10 = __toESM(require_plugin2(), 1);
|
|
232536
233641
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
232537
233642
|
async function getAllRemoteConfigs(fastify2, project) {
|
|
232538
233643
|
const remotes = await fastify2.storage.projectRemotes.getByProject(project.id);
|
|
@@ -232558,7 +233663,7 @@ async function getRemoteConfig(fastify2, project) {
|
|
|
232558
233663
|
const all = await getAllRemoteConfigs(fastify2, project);
|
|
232559
233664
|
return all.length > 0 ? all[0] : null;
|
|
232560
233665
|
}
|
|
232561
|
-
var
|
|
233666
|
+
var routes9 = async (fastify2) => {
|
|
232562
233667
|
fastify2.get("/api/path/branches", async (req, reply) => {
|
|
232563
233668
|
const projectPath = req.query.path;
|
|
232564
233669
|
if (!projectPath) {
|
|
@@ -233096,10 +234201,10 @@ var routes8 = async (fastify2) => {
|
|
|
233096
234201
|
});
|
|
233097
234202
|
});
|
|
233098
234203
|
};
|
|
233099
|
-
var worktree_routes_default = (0,
|
|
234204
|
+
var worktree_routes_default = (0, import_fastify_plugin10.default)(routes9, { name: "worktree-routes" });
|
|
233100
234205
|
|
|
233101
234206
|
// src/routes/diff-routes.ts
|
|
233102
|
-
var
|
|
234207
|
+
var import_fastify_plugin11 = __toESM(require_plugin2(), 1);
|
|
233103
234208
|
import path10 from "path";
|
|
233104
234209
|
import { readFileSync as readFileSync4 } from "fs";
|
|
233105
234210
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
@@ -233419,7 +234524,7 @@ async function getUntrackedFiles(cwd) {
|
|
|
233419
234524
|
}
|
|
233420
234525
|
return files;
|
|
233421
234526
|
}
|
|
233422
|
-
var
|
|
234527
|
+
var routes10 = async (fastify2) => {
|
|
233423
234528
|
fastify2.get("/api/path/diff", async (req, reply) => {
|
|
233424
234529
|
const projectPath = req.query.path;
|
|
233425
234530
|
if (!projectPath) {
|
|
@@ -233614,10 +234719,10 @@ var routes9 = async (fastify2) => {
|
|
|
233614
234719
|
}
|
|
233615
234720
|
});
|
|
233616
234721
|
};
|
|
233617
|
-
var diff_routes_default = (0,
|
|
234722
|
+
var diff_routes_default = (0, import_fastify_plugin11.default)(routes10, { name: "diff-routes" });
|
|
233618
234723
|
|
|
233619
234724
|
// src/routes/file-routes.ts
|
|
233620
|
-
var
|
|
234725
|
+
var import_fastify_plugin12 = __toESM(require_plugin2(), 1);
|
|
233621
234726
|
import path11 from "path";
|
|
233622
234727
|
import fs2 from "fs/promises";
|
|
233623
234728
|
import { createReadStream, constants as fsConstants } from "fs";
|
|
@@ -233853,7 +234958,7 @@ async function searchSymbol(basePath33, symbol26) {
|
|
|
233853
234958
|
}
|
|
233854
234959
|
return { hits, truncated };
|
|
233855
234960
|
}
|
|
233856
|
-
var
|
|
234961
|
+
var routes11 = async (fastify2) => {
|
|
233857
234962
|
fastify2.get("/api/path/browse", async (req, reply) => {
|
|
233858
234963
|
const projectPath = req.query.path;
|
|
233859
234964
|
if (!projectPath) {
|
|
@@ -234447,10 +235552,10 @@ var routes10 = async (fastify2) => {
|
|
|
234447
235552
|
}
|
|
234448
235553
|
});
|
|
234449
235554
|
};
|
|
234450
|
-
var file_routes_default = (0,
|
|
235555
|
+
var file_routes_default = (0, import_fastify_plugin12.default)(routes11, { name: "file-routes" });
|
|
234451
235556
|
|
|
234452
235557
|
// src/routes/agent-session-routes.ts
|
|
234453
|
-
var
|
|
235558
|
+
var import_fastify_plugin13 = __toESM(require_plugin2(), 1);
|
|
234454
235559
|
|
|
234455
235560
|
// src/utils/paste-file.ts
|
|
234456
235561
|
import { chmod, mkdir as mkdir4, open } from "node:fs/promises";
|
|
@@ -234498,7 +235603,7 @@ function messageTextLength(content) {
|
|
|
234498
235603
|
}
|
|
234499
235604
|
return n;
|
|
234500
235605
|
}
|
|
234501
|
-
var
|
|
235606
|
+
var routes12 = async (fastify2) => {
|
|
234502
235607
|
function proxyAuto(remoteServerId, remoteUrl, remoteApiKey, method, apiPath, body) {
|
|
234503
235608
|
return proxyToRemoteAuto(remoteServerId, remoteUrl, remoteApiKey, method, apiPath, body, {
|
|
234504
235609
|
reverseConnectManager: fastify2.reverseConnectManager
|
|
@@ -234756,7 +235861,8 @@ var routes11 = async (fastify2) => {
|
|
|
234756
235861
|
project.id,
|
|
234757
235862
|
project.agent_mode,
|
|
234758
235863
|
s3.id,
|
|
234759
|
-
s3.branch ?? null
|
|
235864
|
+
s3.branch ?? null,
|
|
235865
|
+
"from_now"
|
|
234760
235866
|
);
|
|
234761
235867
|
return { ...s3, id: localSessionId, processAlive: s3.processAlive ?? false, entry_count: s3.entry_count ?? 0 };
|
|
234762
235868
|
}));
|
|
@@ -234851,7 +235957,8 @@ var routes11 = async (fastify2) => {
|
|
|
234851
235957
|
project.id,
|
|
234852
235958
|
agentMode,
|
|
234853
235959
|
remoteData.session.id,
|
|
234854
|
-
branch ?? null
|
|
235960
|
+
branch ?? null,
|
|
235961
|
+
"from_now"
|
|
234855
235962
|
);
|
|
234856
235963
|
if (remoteData.messages && remoteData.messages.length > 0) {
|
|
234857
235964
|
const cacheEntry = fastify2.remotePatchCache.getOrCreate(localSessionId);
|
|
@@ -235078,6 +236185,12 @@ var routes11 = async (fastify2) => {
|
|
|
235078
236185
|
console.log(`[API] /message 404: remote session not found. Known keys: [${[...fastify2.remoteSessionMap.keys()].join(", ")}]`);
|
|
235079
236186
|
return reply.code(404).send({ error: "Remote session not found" });
|
|
235080
236187
|
}
|
|
236188
|
+
if (!await fastify2.remoteNotificationSync.prepareForNewTurn(req.params.sessionId)) {
|
|
236189
|
+
return reply.code(502).send({
|
|
236190
|
+
error: "Could not reach the remote server to prepare notification delivery",
|
|
236191
|
+
errorCode: "notification_baseline_failed"
|
|
236192
|
+
});
|
|
236193
|
+
}
|
|
235081
236194
|
const result = await proxyAuto(
|
|
235082
236195
|
remoteInfo.remoteServerId,
|
|
235083
236196
|
remoteInfo.remoteUrl,
|
|
@@ -235332,7 +236445,8 @@ var routes11 = async (fastify2) => {
|
|
|
235332
236445
|
projectId,
|
|
235333
236446
|
remoteInfo.remoteServerId,
|
|
235334
236447
|
newRemoteSessionId,
|
|
235335
|
-
remoteInfo.branch ?? null
|
|
236448
|
+
remoteInfo.branch ?? null,
|
|
236449
|
+
"from_start"
|
|
235336
236450
|
);
|
|
235337
236451
|
await fastify2.storage.remoteSessionMappings.markTitleResolved(localSessionId);
|
|
235338
236452
|
fastify2.agentSessionManager.markTitleResolved(localSessionId);
|
|
@@ -235627,10 +236741,10 @@ var routes11 = async (fastify2) => {
|
|
|
235627
236741
|
return reply.code(200).send({ success: true, favorited });
|
|
235628
236742
|
});
|
|
235629
236743
|
};
|
|
235630
|
-
var agent_session_routes_default = (0,
|
|
236744
|
+
var agent_session_routes_default = (0, import_fastify_plugin13.default)(routes12, { name: "agent-session-routes" });
|
|
235631
236745
|
|
|
235632
236746
|
// src/routes/branch-activity-routes.ts
|
|
235633
|
-
var
|
|
236747
|
+
var import_fastify_plugin14 = __toESM(require_plugin2(), 1);
|
|
235634
236748
|
function toResponse(map2) {
|
|
235635
236749
|
return {
|
|
235636
236750
|
branches: Array.from(map2.entries()).map(([branch, state]) => ({
|
|
@@ -235645,7 +236759,7 @@ function fromResponse(resp) {
|
|
|
235645
236759
|
resp.branches.map((b2) => [b2.branch ?? "", { activity: b2.activity, since: b2.since }])
|
|
235646
236760
|
);
|
|
235647
236761
|
}
|
|
235648
|
-
var
|
|
236762
|
+
var routes13 = async (fastify2) => {
|
|
235649
236763
|
function proxyAuto(remoteServerId, remoteUrl, remoteApiKey, method, apiPath, body) {
|
|
235650
236764
|
return proxyToRemoteAuto(remoteServerId, remoteUrl, remoteApiKey, method, apiPath, body, {
|
|
235651
236765
|
reverseConnectManager: fastify2.reverseConnectManager
|
|
@@ -235712,10 +236826,10 @@ var routes12 = async (fastify2) => {
|
|
|
235712
236826
|
}
|
|
235713
236827
|
);
|
|
235714
236828
|
};
|
|
235715
|
-
var branch_activity_routes_default = (0,
|
|
236829
|
+
var branch_activity_routes_default = (0, import_fastify_plugin14.default)(routes13, { name: "branch-activity-routes" });
|
|
235716
236830
|
|
|
235717
236831
|
// src/routes/merge-status-routes.ts
|
|
235718
|
-
var
|
|
236832
|
+
var import_fastify_plugin15 = __toESM(require_plugin2(), 1);
|
|
235719
236833
|
function resolveTargets(comparisons, storedTargets) {
|
|
235720
236834
|
const sources = [];
|
|
235721
236835
|
const effective = comparisons.map((comparison) => {
|
|
@@ -235849,7 +236963,7 @@ function sendComputed(reply, repoPath, comparisons, repository) {
|
|
|
235849
236963
|
return reply.code(statusCode).send({ error: message });
|
|
235850
236964
|
}
|
|
235851
236965
|
}
|
|
235852
|
-
var
|
|
236966
|
+
var routes14 = async (fastify2) => {
|
|
235853
236967
|
fastify2.post(
|
|
235854
236968
|
"/api/path/branches/merge-status",
|
|
235855
236969
|
async (req, reply) => {
|
|
@@ -235973,11 +237087,11 @@ var routes13 = async (fastify2) => {
|
|
|
235973
237087
|
}
|
|
235974
237088
|
);
|
|
235975
237089
|
};
|
|
235976
|
-
var merge_status_routes_default = (0,
|
|
237090
|
+
var merge_status_routes_default = (0, import_fastify_plugin15.default)(routes14, { name: "merge-status-routes" });
|
|
235977
237091
|
|
|
235978
237092
|
// src/routes/chat-session-routes.ts
|
|
235979
|
-
var
|
|
235980
|
-
var
|
|
237093
|
+
var import_fastify_plugin16 = __toESM(require_plugin2(), 1);
|
|
237094
|
+
var routes15 = async (fastify2) => {
|
|
235981
237095
|
const getAuthorizedSession = (req, reply, sessionId) => {
|
|
235982
237096
|
const authResult = requireAuth(req, reply);
|
|
235983
237097
|
if (authResult === null) return null;
|
|
@@ -236082,12 +237196,12 @@ var routes14 = async (fastify2) => {
|
|
|
236082
237196
|
return reply.send({ ok: true });
|
|
236083
237197
|
});
|
|
236084
237198
|
};
|
|
236085
|
-
var chat_session_routes_default = (0,
|
|
237199
|
+
var chat_session_routes_default = (0, import_fastify_plugin16.default)(routes15, { name: "chat-session-routes" });
|
|
236086
237200
|
|
|
236087
237201
|
// src/routes/task-routes.ts
|
|
236088
|
-
var
|
|
237202
|
+
var import_fastify_plugin17 = __toESM(require_plugin2(), 1);
|
|
236089
237203
|
import { randomUUID as randomUUID16 } from "crypto";
|
|
236090
|
-
var
|
|
237204
|
+
var routes16 = async (fastify2) => {
|
|
236091
237205
|
fastify2.get(
|
|
236092
237206
|
"/api/projects/:projectId/tasks",
|
|
236093
237207
|
async (req, reply) => {
|
|
@@ -236233,12 +237347,12 @@ Description: ${description}`,
|
|
|
236233
237347
|
return reply.code(200).send({ success: true });
|
|
236234
237348
|
});
|
|
236235
237349
|
};
|
|
236236
|
-
var task_routes_default = (0,
|
|
237350
|
+
var task_routes_default = (0, import_fastify_plugin17.default)(routes16, { name: "task-routes" });
|
|
236237
237351
|
|
|
236238
237352
|
// src/routes/rule-routes.ts
|
|
236239
|
-
var
|
|
237353
|
+
var import_fastify_plugin18 = __toESM(require_plugin2(), 1);
|
|
236240
237354
|
import { randomUUID as randomUUID17 } from "crypto";
|
|
236241
|
-
var
|
|
237355
|
+
var routes17 = async (fastify2) => {
|
|
236242
237356
|
fastify2.get(
|
|
236243
237357
|
"/api/projects/:projectId/rules",
|
|
236244
237358
|
async (req, reply) => {
|
|
@@ -236324,12 +237438,12 @@ var routes16 = async (fastify2) => {
|
|
|
236324
237438
|
return reply.code(200).send({ success: true });
|
|
236325
237439
|
});
|
|
236326
237440
|
};
|
|
236327
|
-
var rule_routes_default = (0,
|
|
237441
|
+
var rule_routes_default = (0, import_fastify_plugin18.default)(routes17, { name: "rule-routes" });
|
|
236328
237442
|
|
|
236329
237443
|
// src/routes/command-routes.ts
|
|
236330
|
-
var
|
|
237444
|
+
var import_fastify_plugin19 = __toESM(require_plugin2(), 1);
|
|
236331
237445
|
import { randomUUID as randomUUID18 } from "crypto";
|
|
236332
|
-
var
|
|
237446
|
+
var routes18 = async (fastify2) => {
|
|
236333
237447
|
fastify2.get(
|
|
236334
237448
|
"/api/projects/:projectId/commands",
|
|
236335
237449
|
async (req, reply) => {
|
|
@@ -236398,10 +237512,10 @@ var routes17 = async (fastify2) => {
|
|
|
236398
237512
|
return reply.code(200).send({ success: true });
|
|
236399
237513
|
});
|
|
236400
237514
|
};
|
|
236401
|
-
var command_routes_default = (0,
|
|
237515
|
+
var command_routes_default = (0, import_fastify_plugin19.default)(routes18, { name: "command-routes" });
|
|
236402
237516
|
|
|
236403
237517
|
// src/routes/workflow-run-routes.ts
|
|
236404
|
-
var
|
|
237518
|
+
var import_fastify_plugin20 = __toESM(require_plugin2(), 1);
|
|
236405
237519
|
|
|
236406
237520
|
// src/utils/review-brief.ts
|
|
236407
237521
|
var AI_TIMEOUT_MS2 = 15e3;
|
|
@@ -236515,7 +237629,7 @@ function errStatus(err) {
|
|
|
236515
237629
|
return 500;
|
|
236516
237630
|
}
|
|
236517
237631
|
}
|
|
236518
|
-
async function
|
|
237632
|
+
async function routes19(fastify2) {
|
|
236519
237633
|
const remoteRunMap = /* @__PURE__ */ new Map();
|
|
236520
237634
|
const TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "cancelled", "failed"]);
|
|
236521
237635
|
const trackRemoteRun = (localRun, info) => {
|
|
@@ -236599,6 +237713,12 @@ async function routes18(fastify2) {
|
|
|
236599
237713
|
if (!clientProvidedBrief && !bareReviewerSessionId) {
|
|
236600
237714
|
intentBrief2 = await distillIntentBrief(userId, sourceSessionId);
|
|
236601
237715
|
}
|
|
237716
|
+
if (reviewerSessionId && !await fastify2.remoteNotificationSync.prepareForNewTurn(reviewerSessionId)) {
|
|
237717
|
+
return reply.code(502).send({
|
|
237718
|
+
error: "Could not reach the remote server to prepare notification delivery",
|
|
237719
|
+
errorCode: "notification_baseline_failed"
|
|
237720
|
+
});
|
|
237721
|
+
}
|
|
236602
237722
|
const result = await proxyAuto(remoteInfo, "POST", "/api/path/workflow-runs", {
|
|
236603
237723
|
sourceSessionId: remoteInfo.remoteSessionId,
|
|
236604
237724
|
reviewFocus,
|
|
@@ -236632,7 +237752,12 @@ async function routes18(fastify2) {
|
|
|
236632
237752
|
projectId,
|
|
236633
237753
|
remoteInfo.remoteServerId,
|
|
236634
237754
|
bareRun.reviewer_session_id,
|
|
236635
|
-
bareRun.branch
|
|
237755
|
+
bareRun.branch,
|
|
237756
|
+
"from_start"
|
|
237757
|
+
);
|
|
237758
|
+
await fastify2.remoteNotificationSync.extendWatch(localRun.reviewer_session_id);
|
|
237759
|
+
fastify2.remoteNotificationSync.enqueue(
|
|
237760
|
+
() => fastify2.remoteNotificationSync.syncServer(remoteInfo.remoteServerId, { includeExpired: true })
|
|
236636
237761
|
);
|
|
236637
237762
|
ensureRemoteAgentStream(localRun.reviewer_session_id, {
|
|
236638
237763
|
remoteSessionMap: fastify2.remoteSessionMap,
|
|
@@ -236641,6 +237766,11 @@ async function routes18(fastify2) {
|
|
|
236641
237766
|
eventBus: fastify2.eventBus,
|
|
236642
237767
|
agentSessionManager: fastify2.agentSessionManager
|
|
236643
237768
|
});
|
|
237769
|
+
fastify2.agentSessionManager.emitBranchActivityIfChanged(projectId, bareRun.branch, {
|
|
237770
|
+
activity: "working",
|
|
237771
|
+
since: Date.now(),
|
|
237772
|
+
sessionId: localRun.reviewer_session_id
|
|
237773
|
+
});
|
|
236644
237774
|
fastify2.eventBus.emit({
|
|
236645
237775
|
type: "session:process",
|
|
236646
237776
|
projectId,
|
|
@@ -236755,7 +237885,8 @@ async function routes18(fastify2) {
|
|
|
236755
237885
|
projectId,
|
|
236756
237886
|
remoteInfo.remoteServerId,
|
|
236757
237887
|
bareCandidate.sessionId,
|
|
236758
|
-
remoteInfo.branch ?? null
|
|
237888
|
+
remoteInfo.branch ?? null,
|
|
237889
|
+
"from_now"
|
|
236759
237890
|
);
|
|
236760
237891
|
}
|
|
236761
237892
|
return reply.send({ candidate: candidate2 });
|
|
@@ -236949,10 +238080,10 @@ async function routes18(fastify2) {
|
|
|
236949
238080
|
return reply.send({ runs });
|
|
236950
238081
|
});
|
|
236951
238082
|
}
|
|
236952
|
-
var workflow_run_routes_default = (0,
|
|
238083
|
+
var workflow_run_routes_default = (0, import_fastify_plugin20.default)(routes19, { name: "workflow-run-routes" });
|
|
236953
238084
|
|
|
236954
238085
|
// src/routes/settings-routes.ts
|
|
236955
|
-
var
|
|
238086
|
+
var import_fastify_plugin21 = __toESM(require_plugin2(), 1);
|
|
236956
238087
|
var SettingsValidationError = class extends Error {
|
|
236957
238088
|
};
|
|
236958
238089
|
var DEFAULT_PROXY_CONFIG = { type: "none", host: "", port: 0 };
|
|
@@ -237016,7 +238147,7 @@ function requireSettingsUser(req, reply) {
|
|
|
237016
238147
|
if (auth === null) return null;
|
|
237017
238148
|
return resolveUserId(auth);
|
|
237018
238149
|
}
|
|
237019
|
-
var
|
|
238150
|
+
var routes20 = async (fastify2) => {
|
|
237020
238151
|
fastify2.get("/api/settings/agent-processes", async (req, reply) => {
|
|
237021
238152
|
if (requireAuth(req, reply) === null) return;
|
|
237022
238153
|
const saved = await fastify2.storage.settings.get("agentProcesses");
|
|
@@ -237290,11 +238421,11 @@ var routes19 = async (fastify2) => {
|
|
|
237290
238421
|
return reply.code(200).send(updated);
|
|
237291
238422
|
});
|
|
237292
238423
|
};
|
|
237293
|
-
var settings_routes_default = (0,
|
|
238424
|
+
var settings_routes_default = (0, import_fastify_plugin21.default)(routes20, { name: "settings-routes" });
|
|
237294
238425
|
|
|
237295
238426
|
// src/routes/translate-routes.ts
|
|
237296
|
-
var
|
|
237297
|
-
var
|
|
238427
|
+
var import_fastify_plugin22 = __toESM(require_plugin2(), 1);
|
|
238428
|
+
var routes21 = async (fastify2) => {
|
|
237298
238429
|
fastify2.post(
|
|
237299
238430
|
"/api/translate",
|
|
237300
238431
|
async (req, reply) => {
|
|
@@ -237335,10 +238466,10 @@ ${text2}`,
|
|
|
237335
238466
|
}
|
|
237336
238467
|
);
|
|
237337
238468
|
};
|
|
237338
|
-
var translate_routes_default = (0,
|
|
238469
|
+
var translate_routes_default = (0, import_fastify_plugin22.default)(routes21, { name: "translate-routes" });
|
|
237339
238470
|
|
|
237340
238471
|
// src/routes/websocket-routes.ts
|
|
237341
|
-
var
|
|
238472
|
+
var import_fastify_plugin23 = __toESM(require_plugin2(), 1);
|
|
237342
238473
|
|
|
237343
238474
|
// src/routes/executor-stream-handlers.ts
|
|
237344
238475
|
import { randomUUID as randomUUID19 } from "crypto";
|
|
@@ -237616,7 +238747,7 @@ async function userOwnsSession(fastify2, sessionId, userId) {
|
|
|
237616
238747
|
}
|
|
237617
238748
|
|
|
237618
238749
|
// src/routes/websocket-routes.ts
|
|
237619
|
-
var
|
|
238750
|
+
var routes22 = async (fastify2) => {
|
|
237620
238751
|
fastify2.reverseConnectManager.setStatusChangeHandler((remoteServerId, status) => {
|
|
237621
238752
|
if (status !== "online") return;
|
|
237622
238753
|
const cache2 = fastify2.remotePatchCache;
|
|
@@ -238003,13 +239134,13 @@ var routes21 = async (fastify2) => {
|
|
|
238003
239134
|
);
|
|
238004
239135
|
});
|
|
238005
239136
|
};
|
|
238006
|
-
var websocket_routes_default = (0,
|
|
239137
|
+
var websocket_routes_default = (0, import_fastify_plugin23.default)(routes22, { name: "websocket-routes" });
|
|
238007
239138
|
|
|
238008
239139
|
// src/routes/reverse-connect-routes.ts
|
|
238009
|
-
var
|
|
239140
|
+
var import_fastify_plugin24 = __toESM(require_plugin2(), 1);
|
|
238010
239141
|
import { randomBytes as randomBytes2, createHash as createHash3, verify as cryptoVerify } from "crypto";
|
|
238011
239142
|
var MACHINE_HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
238012
|
-
var
|
|
239143
|
+
var routes23 = async (fastify2) => {
|
|
238013
239144
|
fastify2.get("/api/reverse-connect/identity", async (req, reply) => {
|
|
238014
239145
|
const token = req.headers["x-vibedeckx-connect-token"];
|
|
238015
239146
|
if (typeof token !== "string" || token.length === 0) {
|
|
@@ -238119,10 +239250,10 @@ var routes22 = async (fastify2) => {
|
|
|
238119
239250
|
);
|
|
238120
239251
|
});
|
|
238121
239252
|
};
|
|
238122
|
-
var reverse_connect_routes_default = (0,
|
|
239253
|
+
var reverse_connect_routes_default = (0, import_fastify_plugin24.default)(routes23, { name: "reverse-connect-routes" });
|
|
238123
239254
|
|
|
238124
239255
|
// src/routes/event-routes.ts
|
|
238125
|
-
var
|
|
239256
|
+
var import_fastify_plugin25 = __toESM(require_plugin2(), 1);
|
|
238126
239257
|
function toWireEvent(event) {
|
|
238127
239258
|
if (event.type === "executor:stopped" && (event.tailOutput !== void 0 || event.finalResult !== void 0)) {
|
|
238128
239259
|
const { tailOutput: _tailOutput, finalResult: _finalResult, ...rest } = event;
|
|
@@ -238130,7 +239261,7 @@ function toWireEvent(event) {
|
|
|
238130
239261
|
}
|
|
238131
239262
|
return event;
|
|
238132
239263
|
}
|
|
238133
|
-
var
|
|
239264
|
+
var routes24 = async (fastify2) => {
|
|
238134
239265
|
fastify2.get("/api/events", async (req, reply) => {
|
|
238135
239266
|
let userId = null;
|
|
238136
239267
|
if (fastify2.authEnabled) {
|
|
@@ -238188,10 +239319,10 @@ var routes23 = async (fastify2) => {
|
|
|
238188
239319
|
await reply;
|
|
238189
239320
|
});
|
|
238190
239321
|
};
|
|
238191
|
-
var event_routes_default = (0,
|
|
239322
|
+
var event_routes_default = (0, import_fastify_plugin25.default)(routes24, { name: "event-routes" });
|
|
238192
239323
|
|
|
238193
239324
|
// src/routes/terminal-routes.ts
|
|
238194
|
-
var
|
|
239325
|
+
var import_fastify_plugin26 = __toESM(require_plugin2(), 1);
|
|
238195
239326
|
import path13 from "path";
|
|
238196
239327
|
async function getRemoteConfig5(fastify2, project, remoteServerId) {
|
|
238197
239328
|
const remotes = await fastify2.storage.projectRemotes.getByProject(project.id);
|
|
@@ -238216,7 +239347,7 @@ async function getRemoteConfig5(fastify2, project, remoteServerId) {
|
|
|
238216
239347
|
}
|
|
238217
239348
|
return null;
|
|
238218
239349
|
}
|
|
238219
|
-
var
|
|
239350
|
+
var routes25 = async (fastify2) => {
|
|
238220
239351
|
fastify2.post("/api/path/terminals", async (req, reply) => {
|
|
238221
239352
|
const { path: projectPath, branch } = req.body;
|
|
238222
239353
|
if (!projectPath) {
|
|
@@ -238399,11 +239530,11 @@ var routes24 = async (fastify2) => {
|
|
|
238399
239530
|
}
|
|
238400
239531
|
);
|
|
238401
239532
|
};
|
|
238402
|
-
var terminal_routes_default = (0,
|
|
239533
|
+
var terminal_routes_default = (0, import_fastify_plugin26.default)(routes25, { name: "terminal-routes" });
|
|
238403
239534
|
|
|
238404
239535
|
// src/routes/browser-routes.ts
|
|
238405
|
-
var
|
|
238406
|
-
var
|
|
239536
|
+
var import_fastify_plugin27 = __toESM(require_plugin2(), 1);
|
|
239537
|
+
var routes26 = async (fastify2) => {
|
|
238407
239538
|
const ensureProjectAccess = async (req, reply) => {
|
|
238408
239539
|
const userId = requireAuth(req, reply);
|
|
238409
239540
|
if (userId === null) return null;
|
|
@@ -238474,10 +239605,10 @@ var routes25 = async (fastify2) => {
|
|
|
238474
239605
|
return reply.code(200).send({ ok: true });
|
|
238475
239606
|
});
|
|
238476
239607
|
};
|
|
238477
|
-
var browser_routes_default = (0,
|
|
239608
|
+
var browser_routes_default = (0, import_fastify_plugin27.default)(routes26, { name: "browser-routes" });
|
|
238478
239609
|
|
|
238479
239610
|
// src/routes/browser-proxy-routes.ts
|
|
238480
|
-
var
|
|
239611
|
+
var import_fastify_plugin28 = __toESM(require_plugin2(), 1);
|
|
238481
239612
|
import { randomUUID as randomUUID20 } from "crypto";
|
|
238482
239613
|
|
|
238483
239614
|
// src/utils/ssrf-guard.ts
|
|
@@ -238973,7 +240104,7 @@ function generateInjectedScript(projectId, targetOrigin, proxyWsPrefix) {
|
|
|
238973
240104
|
})();
|
|
238974
240105
|
</script>`;
|
|
238975
240106
|
}
|
|
238976
|
-
var
|
|
240107
|
+
var routes27 = async (fastify2) => {
|
|
238977
240108
|
fastify2.get("/api/projects/:id/browser/proxy/*", async (req, reply) => {
|
|
238978
240109
|
const userId = requireAuth(req, reply);
|
|
238979
240110
|
if (userId === null) return;
|
|
@@ -239144,10 +240275,10 @@ var routes26 = async (fastify2) => {
|
|
|
239144
240275
|
});
|
|
239145
240276
|
});
|
|
239146
240277
|
};
|
|
239147
|
-
var browser_proxy_routes_default = (0,
|
|
240278
|
+
var browser_proxy_routes_default = (0, import_fastify_plugin28.default)(routes27, { name: "browser-proxy-routes" });
|
|
239148
240279
|
|
|
239149
240280
|
// src/routes/cross-remote-target-routes.ts
|
|
239150
|
-
var
|
|
240281
|
+
var import_fastify_plugin29 = __toESM(require_plugin2(), 1);
|
|
239151
240282
|
import path14 from "path";
|
|
239152
240283
|
import { promises as fs3 } from "fs";
|
|
239153
240284
|
|
|
@@ -239227,7 +240358,7 @@ var clampTimeoutMs = (timeoutSec) => {
|
|
|
239227
240358
|
const requested = typeof timeoutSec === "number" && Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : DEFAULT_TIMEOUT_SEC;
|
|
239228
240359
|
return Math.min(requested, MAX_TIMEOUT_SEC) * 1e3;
|
|
239229
240360
|
};
|
|
239230
|
-
var
|
|
240361
|
+
var routes28 = async (fastify2) => {
|
|
239231
240362
|
fastify2.post(
|
|
239232
240363
|
"/api/path/cross-remote/exec",
|
|
239233
240364
|
async (request, reply) => {
|
|
@@ -239335,10 +240466,10 @@ var routes27 = async (fastify2) => {
|
|
|
239335
240466
|
return reply.send(result);
|
|
239336
240467
|
});
|
|
239337
240468
|
};
|
|
239338
|
-
var cross_remote_target_routes_default = (0,
|
|
240469
|
+
var cross_remote_target_routes_default = (0, import_fastify_plugin29.default)(routes28, { name: "cross-remote-target-routes" });
|
|
239339
240470
|
|
|
239340
240471
|
// src/routes/cross-remote-mcp-routes.ts
|
|
239341
|
-
var
|
|
240472
|
+
var import_fastify_plugin30 = __toESM(require_plugin2(), 1);
|
|
239342
240473
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
239343
240474
|
var AUDIT_ARGS_MAX = 1024;
|
|
239344
240475
|
var NOT_ACCESSIBLE = "remote not found or not accessible";
|
|
@@ -239441,7 +240572,7 @@ var textResult = (text2, isError = false) => ({
|
|
|
239441
240572
|
content: [{ type: "text", text: text2 }],
|
|
239442
240573
|
...isError ? { isError: true } : {}
|
|
239443
240574
|
});
|
|
239444
|
-
var
|
|
240575
|
+
var routes29 = async (fastify2) => {
|
|
239445
240576
|
const guard = new SessionConcurrencyGuard();
|
|
239446
240577
|
let cachedSecret;
|
|
239447
240578
|
const getSecret = () => {
|
|
@@ -239564,10 +240695,10 @@ var routes28 = async (fastify2) => {
|
|
|
239564
240695
|
}
|
|
239565
240696
|
});
|
|
239566
240697
|
};
|
|
239567
|
-
var cross_remote_mcp_routes_default = (0,
|
|
240698
|
+
var cross_remote_mcp_routes_default = (0, import_fastify_plugin30.default)(routes29, { name: "cross-remote-mcp-routes" });
|
|
239568
240699
|
|
|
239569
240700
|
// src/routes/schedule-routes.ts
|
|
239570
|
-
var
|
|
240701
|
+
var import_fastify_plugin31 = __toESM(require_plugin2(), 1);
|
|
239571
240702
|
import { randomUUID as randomUUID21 } from "crypto";
|
|
239572
240703
|
import path15 from "path";
|
|
239573
240704
|
var RUN_TYPES = ["command", "prompt"];
|
|
@@ -239583,7 +240714,7 @@ function validateResolved(b2) {
|
|
|
239583
240714
|
if (cronError) return `Invalid cron expression: ${cronError}`;
|
|
239584
240715
|
return null;
|
|
239585
240716
|
}
|
|
239586
|
-
var
|
|
240717
|
+
var routes30 = async (fastify2) => {
|
|
239587
240718
|
const getAuthorizedSchedule = async (id, userId, reply) => {
|
|
239588
240719
|
const schedule = await fastify2.storage.scheduledTasks.getById(id);
|
|
239589
240720
|
if (!schedule) {
|
|
@@ -239755,7 +240886,7 @@ var routes29 = async (fastify2) => {
|
|
|
239755
240886
|
}
|
|
239756
240887
|
);
|
|
239757
240888
|
};
|
|
239758
|
-
var schedule_routes_default = (0,
|
|
240889
|
+
var schedule_routes_default = (0, import_fastify_plugin31.default)(routes30, { name: "schedule-routes" });
|
|
239759
240890
|
|
|
239760
240891
|
// src/search/catalog.ts
|
|
239761
240892
|
var parseDbTimestamp3 = (ts) => {
|
|
@@ -239959,7 +241090,8 @@ var searchRoutes = async (fastify2) => {
|
|
|
239959
241090
|
target.projectId,
|
|
239960
241091
|
target.targetId,
|
|
239961
241092
|
s3.id,
|
|
239962
|
-
mapBranch
|
|
241093
|
+
mapBranch,
|
|
241094
|
+
"from_now"
|
|
239963
241095
|
);
|
|
239964
241096
|
return { ...s3, id: localSessionId };
|
|
239965
241097
|
}));
|
|
@@ -239996,6 +241128,44 @@ var searchRoutes = async (fastify2) => {
|
|
|
239996
241128
|
};
|
|
239997
241129
|
var search_routes_default = searchRoutes;
|
|
239998
241130
|
|
|
241131
|
+
// src/routes/notification-routes.ts
|
|
241132
|
+
var import_fastify_plugin32 = __toESM(require_plugin2(), 1);
|
|
241133
|
+
var MAX_LIMIT = 500;
|
|
241134
|
+
var DEFAULT_LIMIT = 100;
|
|
241135
|
+
var routes31 = async (fastify2) => {
|
|
241136
|
+
fastify2.get(
|
|
241137
|
+
"/api/notifications",
|
|
241138
|
+
async (req, reply) => {
|
|
241139
|
+
const authResult = requireAuth(req, reply);
|
|
241140
|
+
if (authResult === null) return;
|
|
241141
|
+
const userId = resolveUserId(authResult);
|
|
241142
|
+
const requested = Number(req.query.limit);
|
|
241143
|
+
const limit = Number.isFinite(requested) && requested > 0 ? Math.min(Math.floor(requested), MAX_LIMIT) : DEFAULT_LIMIT;
|
|
241144
|
+
const notifications = await fastify2.storage.notifications.listForUser(userId, {
|
|
241145
|
+
limit,
|
|
241146
|
+
unreadOnly: req.query.unread === "true"
|
|
241147
|
+
});
|
|
241148
|
+
return reply.code(200).send({ notifications });
|
|
241149
|
+
}
|
|
241150
|
+
);
|
|
241151
|
+
fastify2.patch("/api/notifications/:id/read", async (req, reply) => {
|
|
241152
|
+
const authResult = requireAuth(req, reply);
|
|
241153
|
+
if (authResult === null) return;
|
|
241154
|
+
const userId = resolveUserId(authResult);
|
|
241155
|
+
const ok = await fastify2.storage.notifications.markRead(req.params.id, userId);
|
|
241156
|
+
if (!ok) return reply.code(404).send({ error: "Not found" });
|
|
241157
|
+
return reply.code(200).send({ ok: true });
|
|
241158
|
+
});
|
|
241159
|
+
fastify2.post("/api/notifications/read-all", async (req, reply) => {
|
|
241160
|
+
const authResult = requireAuth(req, reply);
|
|
241161
|
+
if (authResult === null) return;
|
|
241162
|
+
const userId = resolveUserId(authResult);
|
|
241163
|
+
await fastify2.storage.notifications.markAllRead(userId);
|
|
241164
|
+
return reply.code(200).send({ ok: true });
|
|
241165
|
+
});
|
|
241166
|
+
};
|
|
241167
|
+
var notification_routes_default = (0, import_fastify_plugin32.default)(routes31, { name: "notification-routes" });
|
|
241168
|
+
|
|
239999
241169
|
// ../../node_modules/.pnpm/@clerk+fastify@3.1.2_fastify@5.7.2_react-dom@19.2.3_react@19.2.3__react@19.2.3/node_modules/@clerk/fastify/dist/chunk-RY4T2JMQ.mjs
|
|
240000
241170
|
import { Readable as Readable3 } from "stream";
|
|
240001
241171
|
var fastifyRequestToRequest = (req) => {
|
|
@@ -240046,7 +241216,7 @@ var requestToProxyRequest = (req) => {
|
|
|
240046
241216
|
|
|
240047
241217
|
// ../../node_modules/.pnpm/@clerk+fastify@3.1.2_fastify@5.7.2_react-dom@19.2.3_react@19.2.3__react@19.2.3/node_modules/@clerk/fastify/dist/index.mjs
|
|
240048
241218
|
init_dist();
|
|
240049
|
-
var
|
|
241219
|
+
var import_fastify_plugin33 = __toESM(require_plugin2(), 1);
|
|
240050
241220
|
|
|
240051
241221
|
// ../../node_modules/.pnpm/@clerk+backend@3.2.0_react-dom@19.2.3_react@19.2.3__react@19.2.3/node_modules/@clerk/backend/dist/internal.mjs
|
|
240052
241222
|
init_chunk_JW73PKED();
|
|
@@ -240334,7 +241504,7 @@ var plugin = (instance, opts, done) => {
|
|
|
240334
241504
|
instance.addHook(hookName, withClerkMiddleware(opts));
|
|
240335
241505
|
done();
|
|
240336
241506
|
};
|
|
240337
|
-
var clerkPlugin = (0,
|
|
241507
|
+
var clerkPlugin = (0, import_fastify_plugin33.default)(plugin, {
|
|
240338
241508
|
name: "@clerk/fastify",
|
|
240339
241509
|
fastify: "5.x"
|
|
240340
241510
|
});
|
|
@@ -241223,6 +242393,7 @@ var createServer = async (opts) => {
|
|
|
241223
242393
|
authEnabled,
|
|
241224
242394
|
clerkPublishableKey: authEnabled ? process.env.CLERK_PUBLISHABLE_KEY : void 0,
|
|
241225
242395
|
localProjectsEnabled: !noLocalProjects,
|
|
242396
|
+
discordInviteUrl: process.env.VIBEDECKX_DISCORD_URL || void 0,
|
|
241226
242397
|
// Capability flag for the reverse-connect identity preflight. Workers
|
|
241227
242398
|
// check this before calling /api/reverse-connect/identity so an auth
|
|
241228
242399
|
// middleware 401 from an older hub (which lacks the endpoint AND its
|
|
@@ -241282,6 +242453,8 @@ var createServer = async (opts) => {
|
|
|
241282
242453
|
server.register(settings_routes_default);
|
|
241283
242454
|
server.register(translate_routes_default);
|
|
241284
242455
|
server.register(event_routes_default);
|
|
242456
|
+
server.register(notification_routes_default);
|
|
242457
|
+
server.register(notification_outbox_routes_default);
|
|
241285
242458
|
server.register(terminal_routes_default);
|
|
241286
242459
|
server.register(browser_routes_default);
|
|
241287
242460
|
server.register(browser_proxy_routes_default);
|
|
@@ -243623,7 +244796,7 @@ var connectRoutes = buildRouteMap({
|
|
|
243623
244796
|
defaultCommand: "run",
|
|
243624
244797
|
docs: { brief: "Connect to a Vibedeckx server as an inbound node" }
|
|
243625
244798
|
});
|
|
243626
|
-
var
|
|
244799
|
+
var routes32 = buildRouteMap({
|
|
243627
244800
|
routes: {
|
|
243628
244801
|
start: startCommand,
|
|
243629
244802
|
connect: connectRoutes
|
|
@@ -243633,7 +244806,7 @@ var routes30 = buildRouteMap({
|
|
|
243633
244806
|
brief: "Vibedeckx - AI-powered app generator"
|
|
243634
244807
|
}
|
|
243635
244808
|
});
|
|
243636
|
-
var program = buildApplication(
|
|
244809
|
+
var program = buildApplication(routes32, {
|
|
243637
244810
|
name: "vibedeckx",
|
|
243638
244811
|
versionInfo: {
|
|
243639
244812
|
currentVersion: readPackageVersion()
|