@vibedeckx/linux-x64 0.2.2 → 0.2.3
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 +790 -45
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -9400,7 +9400,7 @@ var require_InstrumentDescriptor = __commonJS({
|
|
|
9400
9400
|
var api_1 = (init_esm(), __toCommonJS(esm_exports));
|
|
9401
9401
|
var utils_1 = require_utils5();
|
|
9402
9402
|
function createInstrumentDescriptor(name25, type, options) {
|
|
9403
|
-
if (!
|
|
9403
|
+
if (!isValidName2(name25)) {
|
|
9404
9404
|
api_1.diag.warn(`Invalid metric name: "${name25}". The metric name should be a ASCII string with a length no greater than 255 characters.`);
|
|
9405
9405
|
}
|
|
9406
9406
|
return {
|
|
@@ -9429,10 +9429,10 @@ var require_InstrumentDescriptor = __commonJS({
|
|
|
9429
9429
|
}
|
|
9430
9430
|
exports.isDescriptorCompatibleWith = isDescriptorCompatibleWith;
|
|
9431
9431
|
var NAME_REGEXP = /^[a-z][a-z0-9_.\-/]{0,254}$/i;
|
|
9432
|
-
function
|
|
9432
|
+
function isValidName2(name25) {
|
|
9433
9433
|
return NAME_REGEXP.test(name25);
|
|
9434
9434
|
}
|
|
9435
|
-
exports.isValidName =
|
|
9435
|
+
exports.isValidName = isValidName2;
|
|
9436
9436
|
}
|
|
9437
9437
|
});
|
|
9438
9438
|
|
|
@@ -185912,6 +185912,217 @@ var createCrossRemoteAuditRepo = (kdb) => ({
|
|
|
185912
185912
|
}
|
|
185913
185913
|
});
|
|
185914
185914
|
|
|
185915
|
+
// src/storage/repositories/merge-targets.ts
|
|
185916
|
+
var createMergeTargetsRepo = (kdb) => ({
|
|
185917
|
+
mergeTargets: {
|
|
185918
|
+
getForBranches: async (projectId, branches) => {
|
|
185919
|
+
if (branches.length === 0) return /* @__PURE__ */ new Map();
|
|
185920
|
+
const rows = await kdb.selectFrom("branch_merge_targets").select(["branch", "target"]).where("project_id", "=", projectId).where("branch", "in", branches).execute();
|
|
185921
|
+
return new Map(rows.map((row) => [row.branch, row.target]));
|
|
185922
|
+
},
|
|
185923
|
+
upsert: async (projectId, branch, target) => {
|
|
185924
|
+
const result = await kdb.insertInto("branch_merge_targets").values({ project_id: projectId, branch, target }).onConflict(
|
|
185925
|
+
(conflict) => conflict.columns(["project_id", "branch"]).doUpdateSet({
|
|
185926
|
+
target,
|
|
185927
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
185928
|
+
}).where("target", "!=", target)
|
|
185929
|
+
).executeTakeFirst();
|
|
185930
|
+
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
185931
|
+
},
|
|
185932
|
+
insertIfAbsent: async (projectId, branch, target) => {
|
|
185933
|
+
const result = await kdb.insertInto("branch_merge_targets").values({ project_id: projectId, branch, target }).onConflict((conflict) => conflict.columns(["project_id", "branch"]).doNothing()).executeTakeFirst();
|
|
185934
|
+
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
185935
|
+
},
|
|
185936
|
+
delete: async (projectId, branch) => {
|
|
185937
|
+
const result = await kdb.deleteFrom("branch_merge_targets").where("project_id", "=", projectId).where("branch", "=", branch).executeTakeFirst();
|
|
185938
|
+
return result.numDeletedRows > 0n;
|
|
185939
|
+
}
|
|
185940
|
+
}
|
|
185941
|
+
});
|
|
185942
|
+
|
|
185943
|
+
// src/storage/repositories/search-cache.ts
|
|
185944
|
+
var toDbBranch = (branch) => branch ?? "";
|
|
185945
|
+
var fromDbBranch = (branch) => branch === "" ? null : branch;
|
|
185946
|
+
var escapeLike = (s3) => s3.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
185947
|
+
var matchTier = (text2, q) => {
|
|
185948
|
+
if (!q) return 2;
|
|
185949
|
+
if (!text2) return 3;
|
|
185950
|
+
const t = text2.toLowerCase();
|
|
185951
|
+
if (t === q) return 0;
|
|
185952
|
+
if (t.startsWith(q)) return 1;
|
|
185953
|
+
if (t.includes(q)) return 2;
|
|
185954
|
+
return 3;
|
|
185955
|
+
};
|
|
185956
|
+
var parseDbTimestamp = (ts) => {
|
|
185957
|
+
if (!ts) return null;
|
|
185958
|
+
const ms = Date.parse(ts.replace(" ", "T") + "Z");
|
|
185959
|
+
return Number.isNaN(ms) ? null : ms;
|
|
185960
|
+
};
|
|
185961
|
+
var rankAndCap = (items, limit) => items.filter((x2) => x2.tier < 3).sort((a, b2) => a.tier - b2.tier || Number(b2.favorited) - Number(a.favorited) || b2.recency - a.recency).slice(0, limit).map((x2) => x2.item);
|
|
185962
|
+
var createSearchCacheRepos = (kdb, _h) => ({
|
|
185963
|
+
searchCache: {
|
|
185964
|
+
// Generation-based reconciliation: only a FULLY successful snapshot may
|
|
185965
|
+
// mark rows deleted. Runs in one transaction so a crash mid-apply can't
|
|
185966
|
+
// leave a half-deleted cache.
|
|
185967
|
+
applyCatalogSnapshot: async (projectId, targetId, snapshot) => {
|
|
185968
|
+
const now2 = Date.now();
|
|
185969
|
+
await kdb.transaction().execute(async (trx) => {
|
|
185970
|
+
const state = await trx.selectFrom("search_catalog_sync_state").select("snapshot_generation").where("project_id", "=", projectId).where("target_id", "=", targetId).executeTakeFirst();
|
|
185971
|
+
const generation = (state?.snapshot_generation ?? 0) + 1;
|
|
185972
|
+
for (const w2 of snapshot.workspaces) {
|
|
185973
|
+
await trx.insertInto("workspace_search_cache").values({ project_id: projectId, target_id: targetId, branch: toDbBranch(w2.branch), generation, deleted_at: null }).onConflict((oc) => oc.columns(["project_id", "target_id", "branch"]).doUpdateSet({ generation, deleted_at: null })).execute();
|
|
185974
|
+
}
|
|
185975
|
+
for (const s3 of snapshot.sessions) {
|
|
185976
|
+
await trx.insertInto("session_search_cache").values({
|
|
185977
|
+
local_session_id: s3.id,
|
|
185978
|
+
project_id: projectId,
|
|
185979
|
+
target_id: targetId,
|
|
185980
|
+
branch: toDbBranch(s3.branch),
|
|
185981
|
+
title: s3.title,
|
|
185982
|
+
last_active_at: s3.lastActiveAt,
|
|
185983
|
+
favorited_at: s3.favoritedAt,
|
|
185984
|
+
entry_count: s3.entryCount,
|
|
185985
|
+
generation,
|
|
185986
|
+
deleted_at: null
|
|
185987
|
+
}).onConflict((oc) => oc.column("local_session_id").doUpdateSet({
|
|
185988
|
+
project_id: projectId,
|
|
185989
|
+
target_id: targetId,
|
|
185990
|
+
branch: toDbBranch(s3.branch),
|
|
185991
|
+
title: s3.title,
|
|
185992
|
+
last_active_at: s3.lastActiveAt,
|
|
185993
|
+
favorited_at: s3.favoritedAt,
|
|
185994
|
+
entry_count: s3.entryCount,
|
|
185995
|
+
generation,
|
|
185996
|
+
deleted_at: null
|
|
185997
|
+
})).execute();
|
|
185998
|
+
}
|
|
185999
|
+
await trx.updateTable("workspace_search_cache").set({ deleted_at: now2 }).where("project_id", "=", projectId).where("target_id", "=", targetId).where("generation", "<", generation).where("deleted_at", "is", null).execute();
|
|
186000
|
+
await trx.updateTable("session_search_cache").set({ deleted_at: now2 }).where("project_id", "=", projectId).where("target_id", "=", targetId).where("generation", "<", generation).where("deleted_at", "is", null).execute();
|
|
186001
|
+
await trx.insertInto("search_catalog_sync_state").values({
|
|
186002
|
+
project_id: projectId,
|
|
186003
|
+
target_id: targetId,
|
|
186004
|
+
last_success_at: now2,
|
|
186005
|
+
last_attempt_at: now2,
|
|
186006
|
+
snapshot_generation: generation,
|
|
186007
|
+
last_error: null
|
|
186008
|
+
}).onConflict((oc) => oc.columns(["project_id", "target_id"]).doUpdateSet({
|
|
186009
|
+
last_success_at: now2,
|
|
186010
|
+
last_attempt_at: now2,
|
|
186011
|
+
snapshot_generation: generation,
|
|
186012
|
+
last_error: null
|
|
186013
|
+
})).execute();
|
|
186014
|
+
});
|
|
186015
|
+
},
|
|
186016
|
+
recordSyncFailure: async (projectId, targetId, error48) => {
|
|
186017
|
+
const now2 = Date.now();
|
|
186018
|
+
await kdb.insertInto("search_catalog_sync_state").values({
|
|
186019
|
+
project_id: projectId,
|
|
186020
|
+
target_id: targetId,
|
|
186021
|
+
last_success_at: null,
|
|
186022
|
+
last_attempt_at: now2,
|
|
186023
|
+
snapshot_generation: 0,
|
|
186024
|
+
last_error: error48
|
|
186025
|
+
}).onConflict((oc) => oc.columns(["project_id", "target_id"]).doUpdateSet({ last_attempt_at: now2, last_error: error48 })).execute();
|
|
186026
|
+
},
|
|
186027
|
+
getSyncStates: async (projectIds) => {
|
|
186028
|
+
if (projectIds.length === 0) return [];
|
|
186029
|
+
return kdb.selectFrom("search_catalog_sync_state").select(["project_id", "target_id", "last_success_at", "last_attempt_at", "last_error"]).where("project_id", "in", projectIds).execute();
|
|
186030
|
+
},
|
|
186031
|
+
// Opportunistic freshness: called where a title transits the server
|
|
186032
|
+
// anyway (remote title PATCH proxy). UPDATE-only — inserting here would
|
|
186033
|
+
// fabricate a row outside snapshot generations.
|
|
186034
|
+
updateCachedSessionTitle: async (localSessionId, title) => {
|
|
186035
|
+
await kdb.updateTable("session_search_cache").set({ title }).where("local_session_id", "=", localSessionId).execute();
|
|
186036
|
+
},
|
|
186037
|
+
search: async ({ userId, query, limitPerGroup }) => {
|
|
186038
|
+
const q = query.trim().slice(0, 256).toLowerCase();
|
|
186039
|
+
let projQuery = kdb.selectFrom("projects").select(["id", "name", "path"]).where("id", "not like", "path:%");
|
|
186040
|
+
if (userId) projQuery = projQuery.where("user_id", "=", userId);
|
|
186041
|
+
const allProjects = await projQuery.execute();
|
|
186042
|
+
const projectIds = allProjects.map((p2) => p2.id);
|
|
186043
|
+
const nameById = new Map(allProjects.map((p2) => [p2.id, p2.name]));
|
|
186044
|
+
if (projectIds.length === 0) return { projects: [], workspaces: [], sessions: [] };
|
|
186045
|
+
const pattern = `%${escapeLike(q)}%`;
|
|
186046
|
+
let localBase = kdb.selectFrom("agent_sessions as s").select(["s.id", "s.project_id", "s.branch", "s.title", "s.last_user_message_at", "s.updated_at", "s.favorited_at"]).where("s.project_id", "in", projectIds).where((eb) => eb.or([
|
|
186047
|
+
eb("s.title", "is not", null),
|
|
186048
|
+
eb.exists(
|
|
186049
|
+
eb.selectFrom("agent_session_entries").select("agent_session_entries.session_id").whereRef("agent_session_entries.session_id", "=", "s.id")
|
|
186050
|
+
)
|
|
186051
|
+
]));
|
|
186052
|
+
if (q) localBase = localBase.where(sql`lower(coalesce(s.title, '')) like ${pattern} escape '\\'`);
|
|
186053
|
+
const localRows = await localBase.orderBy("s.updated_at", "desc").limit(200).execute();
|
|
186054
|
+
const localFavRows = await localBase.where("s.favorited_at", "is not", null).execute();
|
|
186055
|
+
let cacheBase = kdb.selectFrom("session_search_cache as c").leftJoin("project_remotes as pr", (join2) => join2.onRef("pr.project_id", "=", "c.project_id").onRef("pr.remote_server_id", "=", "c.target_id")).select(["c.local_session_id", "c.project_id", "c.target_id", "c.branch", "c.title", "c.last_active_at", "c.favorited_at"]).where("c.project_id", "in", projectIds).where("c.deleted_at", "is", null).where((eb) => eb.or([
|
|
186056
|
+
eb("c.target_id", "=", "local"),
|
|
186057
|
+
eb("pr.id", "is not", null)
|
|
186058
|
+
]));
|
|
186059
|
+
if (q) cacheBase = cacheBase.where(sql`lower(coalesce(c.title, '')) like ${pattern} escape '\\'`);
|
|
186060
|
+
const cacheRows = await cacheBase.orderBy("c.last_active_at", "desc").limit(200).execute();
|
|
186061
|
+
const cacheFavRows = await cacheBase.where("c.favorited_at", "is not", null).execute();
|
|
186062
|
+
const candidateById = /* @__PURE__ */ new Map();
|
|
186063
|
+
for (const r of [...localRows, ...localFavRows]) {
|
|
186064
|
+
if (candidateById.has(r.id)) continue;
|
|
186065
|
+
candidateById.set(r.id, {
|
|
186066
|
+
sessionId: r.id,
|
|
186067
|
+
projectId: r.project_id,
|
|
186068
|
+
projectName: nameById.get(r.project_id) ?? "",
|
|
186069
|
+
targetId: "local",
|
|
186070
|
+
branch: fromDbBranch(r.branch),
|
|
186071
|
+
title: r.title ?? null,
|
|
186072
|
+
lastActiveAt: r.last_user_message_at ?? parseDbTimestamp(r.updated_at),
|
|
186073
|
+
favoritedAt: r.favorited_at ?? null
|
|
186074
|
+
});
|
|
186075
|
+
}
|
|
186076
|
+
for (const r of [...cacheRows, ...cacheFavRows]) {
|
|
186077
|
+
if (candidateById.has(r.local_session_id)) continue;
|
|
186078
|
+
candidateById.set(r.local_session_id, {
|
|
186079
|
+
sessionId: r.local_session_id,
|
|
186080
|
+
projectId: r.project_id,
|
|
186081
|
+
projectName: nameById.get(r.project_id) ?? "",
|
|
186082
|
+
targetId: r.target_id,
|
|
186083
|
+
branch: fromDbBranch(r.branch),
|
|
186084
|
+
title: r.title ?? null,
|
|
186085
|
+
lastActiveAt: r.last_active_at ?? null,
|
|
186086
|
+
favoritedAt: r.favorited_at ?? null
|
|
186087
|
+
});
|
|
186088
|
+
}
|
|
186089
|
+
const sessionCandidates = [...candidateById.values()];
|
|
186090
|
+
if (!q) {
|
|
186091
|
+
const sessions2 = [...sessionCandidates].sort((a, b2) => Number(!!b2.favoritedAt) - Number(!!a.favoritedAt) || (b2.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0)).slice(0, limitPerGroup);
|
|
186092
|
+
return { projects: [], workspaces: [], sessions: sessions2 };
|
|
186093
|
+
}
|
|
186094
|
+
const projects = rankAndCap(allProjects.map((p2) => ({
|
|
186095
|
+
item: { id: p2.id, name: p2.name, path: p2.path ?? null },
|
|
186096
|
+
tier: Math.min(matchTier(p2.name, q), matchTier(p2.path, q)),
|
|
186097
|
+
favorited: false,
|
|
186098
|
+
recency: 0
|
|
186099
|
+
})), limitPerGroup);
|
|
186100
|
+
const wsRows = await kdb.selectFrom("workspace_search_cache as w").leftJoin("project_remotes as pr", (join2) => join2.onRef("pr.project_id", "=", "w.project_id").onRef("pr.remote_server_id", "=", "w.target_id")).select(["w.project_id", "w.target_id", "w.branch"]).where("w.project_id", "in", projectIds).where("w.deleted_at", "is", null).where((eb) => eb.or([
|
|
186101
|
+
eb("w.target_id", "=", "local"),
|
|
186102
|
+
eb("pr.id", "is not", null)
|
|
186103
|
+
])).execute();
|
|
186104
|
+
const workspaces = rankAndCap(wsRows.map((w2) => ({
|
|
186105
|
+
item: {
|
|
186106
|
+
projectId: w2.project_id,
|
|
186107
|
+
projectName: nameById.get(w2.project_id) ?? "",
|
|
186108
|
+
targetId: w2.target_id,
|
|
186109
|
+
branch: fromDbBranch(w2.branch)
|
|
186110
|
+
},
|
|
186111
|
+
tier: matchTier(fromDbBranch(w2.branch) ?? "main", q),
|
|
186112
|
+
favorited: false,
|
|
186113
|
+
recency: 0
|
|
186114
|
+
})), limitPerGroup);
|
|
186115
|
+
const sessions = rankAndCap(sessionCandidates.map((s3) => ({
|
|
186116
|
+
item: s3,
|
|
186117
|
+
tier: matchTier(s3.title, q),
|
|
186118
|
+
favorited: !!s3.favoritedAt,
|
|
186119
|
+
recency: s3.lastActiveAt ?? 0
|
|
186120
|
+
})), limitPerGroup);
|
|
186121
|
+
return { projects, workspaces, sessions };
|
|
186122
|
+
}
|
|
186123
|
+
}
|
|
186124
|
+
});
|
|
186125
|
+
|
|
185915
186126
|
// src/storage/sqlite.ts
|
|
185916
186127
|
var createDatabase = (dbPath) => {
|
|
185917
186128
|
const db = new Database(dbPath);
|
|
@@ -186534,6 +186745,54 @@ var createDatabase = (dbPath) => {
|
|
|
186534
186745
|
if (!scheduledRunCols.some((c) => c.name === "report")) {
|
|
186535
186746
|
db.exec("ALTER TABLE scheduled_task_runs ADD COLUMN report TEXT DEFAULT NULL");
|
|
186536
186747
|
}
|
|
186748
|
+
db.exec(`
|
|
186749
|
+
CREATE TABLE IF NOT EXISTS branch_merge_targets (
|
|
186750
|
+
project_id TEXT NOT NULL,
|
|
186751
|
+
branch TEXT NOT NULL,
|
|
186752
|
+
target TEXT NOT NULL,
|
|
186753
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
186754
|
+
PRIMARY KEY (project_id, branch),
|
|
186755
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
186756
|
+
);
|
|
186757
|
+
|
|
186758
|
+
-- Search caches: server-side searchable copies of worker catalogs.
|
|
186759
|
+
-- remote_session_mappings stays routing-only; these tables are reconciled
|
|
186760
|
+
-- from full catalog snapshots (generation-based) and rows are soft-deleted,
|
|
186761
|
+
-- so wiping them never breaks existing remote session URLs.
|
|
186762
|
+
CREATE TABLE IF NOT EXISTS session_search_cache (
|
|
186763
|
+
local_session_id TEXT PRIMARY KEY,
|
|
186764
|
+
project_id TEXT NOT NULL,
|
|
186765
|
+
target_id TEXT NOT NULL,
|
|
186766
|
+
branch TEXT NOT NULL DEFAULT '',
|
|
186767
|
+
title TEXT,
|
|
186768
|
+
last_active_at INTEGER,
|
|
186769
|
+
favorited_at INTEGER,
|
|
186770
|
+
entry_count INTEGER NOT NULL DEFAULT 0,
|
|
186771
|
+
generation INTEGER NOT NULL,
|
|
186772
|
+
deleted_at INTEGER
|
|
186773
|
+
);
|
|
186774
|
+
CREATE INDEX IF NOT EXISTS idx_session_search_cache_project
|
|
186775
|
+
ON session_search_cache(project_id, target_id);
|
|
186776
|
+
|
|
186777
|
+
CREATE TABLE IF NOT EXISTS workspace_search_cache (
|
|
186778
|
+
project_id TEXT NOT NULL,
|
|
186779
|
+
target_id TEXT NOT NULL,
|
|
186780
|
+
branch TEXT NOT NULL,
|
|
186781
|
+
generation INTEGER NOT NULL,
|
|
186782
|
+
deleted_at INTEGER,
|
|
186783
|
+
PRIMARY KEY (project_id, target_id, branch)
|
|
186784
|
+
);
|
|
186785
|
+
|
|
186786
|
+
CREATE TABLE IF NOT EXISTS search_catalog_sync_state (
|
|
186787
|
+
project_id TEXT NOT NULL,
|
|
186788
|
+
target_id TEXT NOT NULL,
|
|
186789
|
+
last_success_at INTEGER,
|
|
186790
|
+
last_attempt_at INTEGER,
|
|
186791
|
+
snapshot_generation INTEGER NOT NULL DEFAULT 0,
|
|
186792
|
+
last_error TEXT,
|
|
186793
|
+
PRIMARY KEY (project_id, target_id)
|
|
186794
|
+
);
|
|
186795
|
+
`);
|
|
186537
186796
|
db.pragma("foreign_keys = ON");
|
|
186538
186797
|
return db;
|
|
186539
186798
|
};
|
|
@@ -186550,6 +186809,8 @@ var createSqliteStorage = async (dbPath) => {
|
|
|
186550
186809
|
...createAgentSessionRepos(kdb, h),
|
|
186551
186810
|
...createWorkspaceRepos(kdb, h),
|
|
186552
186811
|
...createCrossRemoteAuditRepo(kdb),
|
|
186812
|
+
...createMergeTargetsRepo(kdb),
|
|
186813
|
+
...createSearchCacheRepos(kdb, h),
|
|
186553
186814
|
close: async () => {
|
|
186554
186815
|
await kdb.destroy();
|
|
186555
186816
|
}
|
|
@@ -223964,7 +224225,8 @@ var AgentSessionManager = class {
|
|
|
223964
224225
|
eventChain: Promise.resolve(),
|
|
223965
224226
|
bgSpawnHintsThisTurn: 0,
|
|
223966
224227
|
taskStartedThisTurn: 0,
|
|
223967
|
-
lastActiveAt: Date.now()
|
|
224228
|
+
lastActiveAt: Date.now(),
|
|
224229
|
+
turnOpenSince: null
|
|
223968
224230
|
};
|
|
223969
224231
|
this.sessions.set(sessionId, session);
|
|
223970
224232
|
const provider = getProvider(agentType);
|
|
@@ -224104,21 +224366,26 @@ var AgentSessionManager = class {
|
|
|
224104
224366
|
if (action.kind === "commit") {
|
|
224105
224367
|
await this.commitCompletion(session, action.payload);
|
|
224106
224368
|
}
|
|
224369
|
+
if (code !== 0 && !spawnFailed && !session.producedOutput) {
|
|
224370
|
+
try {
|
|
224371
|
+
await this.pushEntry(session.id, {
|
|
224372
|
+
type: "error",
|
|
224373
|
+
message: this.buildStartupFailureMessage(session.agentType, stderrTail),
|
|
224374
|
+
timestamp: Date.now()
|
|
224375
|
+
}, true);
|
|
224376
|
+
} catch (err) {
|
|
224377
|
+
console.error(`[AgentSession] Failed to push startup-failure entry for ${session.id}:`, err);
|
|
224378
|
+
}
|
|
224379
|
+
}
|
|
224380
|
+
await this.finalizeStreamingEntry(session);
|
|
224381
|
+
session.store.currentAssistantIndex = null;
|
|
224382
|
+
await this.endActiveTurn(session, "process_exit");
|
|
224107
224383
|
session.status = code === 0 ? "stopped" : "error";
|
|
224108
224384
|
if (!session.skipDb) {
|
|
224109
224385
|
this.storage.agentSessions.updateStatus(session.id, session.status).catch((err) => {
|
|
224110
224386
|
console.error(`[AgentSession] Failed to update status for ${session.id}:`, err);
|
|
224111
224387
|
});
|
|
224112
224388
|
}
|
|
224113
|
-
if (code !== 0 && !spawnFailed && !session.producedOutput) {
|
|
224114
|
-
this.pushEntry(session.id, {
|
|
224115
|
-
type: "error",
|
|
224116
|
-
message: this.buildStartupFailureMessage(session.agentType, stderrTail),
|
|
224117
|
-
timestamp: Date.now()
|
|
224118
|
-
}, true).catch((err) => {
|
|
224119
|
-
console.error(`[AgentSession] Failed to push startup-failure entry for ${session.id}:`, err);
|
|
224120
|
-
});
|
|
224121
|
-
}
|
|
224122
224389
|
this.broadcastPatch(session.id, ConversationPatch.updateStatus(session.status));
|
|
224123
224390
|
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: session.status });
|
|
224124
224391
|
this.broadcastRaw(session.id, { finished: true });
|
|
@@ -224227,6 +224494,7 @@ var AgentSessionManager = class {
|
|
|
224227
224494
|
summaryText
|
|
224228
224495
|
});
|
|
224229
224496
|
await this.emitDerivedBranchActivity(session.projectId, session.branch);
|
|
224497
|
+
await this.endActiveTurn(session, "completed");
|
|
224230
224498
|
if (session.status !== "stopped") {
|
|
224231
224499
|
session.status = "stopped";
|
|
224232
224500
|
if (!session.skipDb) await this.storage.agentSessions.updateStatus(sessionId, "stopped");
|
|
@@ -224421,6 +224689,7 @@ var AgentSessionManager = class {
|
|
|
224421
224689
|
}
|
|
224422
224690
|
if (event.subtype === "error") {
|
|
224423
224691
|
this.applyCompletionTimerAction(session, session.completion.errorResult());
|
|
224692
|
+
await this.endActiveTurn(session, "failed");
|
|
224424
224693
|
if (session.status !== "stopped") {
|
|
224425
224694
|
session.status = "stopped";
|
|
224426
224695
|
if (!session.skipDb) await this.storage.agentSessions.updateStatus(sessionId, "stopped");
|
|
@@ -224581,6 +224850,20 @@ ${details}`;
|
|
|
224581
224850
|
await this.persistEntry(session, index, entry);
|
|
224582
224851
|
}
|
|
224583
224852
|
}
|
|
224853
|
+
/**
|
|
224854
|
+
* Close the open turn with a persisted turn_end stop-point entry.
|
|
224855
|
+
* turn_end entries are constructed ONLY here and in repairInterruptedTurn
|
|
224856
|
+
* (restore path). Wall clock only — see design doc for why the CLI's
|
|
224857
|
+
* payload.duration_ms is not used. Rides the normal best-effort entry
|
|
224858
|
+
* persistence on purpose (no strict path — design decision).
|
|
224859
|
+
*/
|
|
224860
|
+
async endActiveTurn(session, outcome) {
|
|
224861
|
+
if (session.turnOpenSince === null) return;
|
|
224862
|
+
const endedAt = Date.now();
|
|
224863
|
+
const durationMs = endedAt - session.turnOpenSince;
|
|
224864
|
+
await this.pushEntry(session.id, { type: "turn_end", timestamp: endedAt, durationMs, outcome }, true);
|
|
224865
|
+
session.turnOpenSince = null;
|
|
224866
|
+
}
|
|
224584
224867
|
/**
|
|
224585
224868
|
* Send a user message to the agent
|
|
224586
224869
|
*/
|
|
@@ -224626,6 +224909,7 @@ ${details}`;
|
|
|
224626
224909
|
`[AgentSession] sendUserMessage: wrote ${formatted.length}B to ${session.agentType} stdin (session=${sessionId})`
|
|
224627
224910
|
);
|
|
224628
224911
|
session.process.stdin.write(formatted);
|
|
224912
|
+
if (session.turnOpenSince === null) session.turnOpenSince = Date.now();
|
|
224629
224913
|
return true;
|
|
224630
224914
|
} catch (error48) {
|
|
224631
224915
|
console.error(`[AgentSession] Failed to send message:`, error48);
|
|
@@ -224744,6 +225028,7 @@ ${details}`;
|
|
|
224744
225028
|
content: "Session stopped by user.",
|
|
224745
225029
|
timestamp: Date.now()
|
|
224746
225030
|
});
|
|
225031
|
+
await this.endActiveTurn(session, "stopped");
|
|
224747
225032
|
session.dormant = true;
|
|
224748
225033
|
this.resetCompletion(session);
|
|
224749
225034
|
session.status = "stopped";
|
|
@@ -224853,6 +225138,7 @@ ${details}`;
|
|
|
224853
225138
|
session.store.currentAssistantIndex = null;
|
|
224854
225139
|
session.buffer = "";
|
|
224855
225140
|
session.dormant = false;
|
|
225141
|
+
session.turnOpenSince = null;
|
|
224856
225142
|
this.touchSession(session);
|
|
224857
225143
|
const clearPatch = ConversationPatch.clearAll();
|
|
224858
225144
|
this.broadcastPatch(sessionId, clearPatch);
|
|
@@ -225013,6 +225299,8 @@ ${details}`;
|
|
|
225013
225299
|
break;
|
|
225014
225300
|
case "system":
|
|
225015
225301
|
break;
|
|
225302
|
+
case "turn_end":
|
|
225303
|
+
break;
|
|
225016
225304
|
}
|
|
225017
225305
|
}
|
|
225018
225306
|
if (lines.length === 0) return null;
|
|
@@ -225055,6 +225343,7 @@ ${details}`;
|
|
|
225055
225343
|
content: userMessage,
|
|
225056
225344
|
timestamp: Date.now()
|
|
225057
225345
|
}, true, userId);
|
|
225346
|
+
session.turnOpenSince = Date.now();
|
|
225058
225347
|
setTimeout(() => {
|
|
225059
225348
|
const context2 = this.buildFullConversationContext(session.store.entries);
|
|
225060
225349
|
if (context2) {
|
|
@@ -225105,6 +225394,34 @@ ${details}`;
|
|
|
225105
225394
|
indexProvider.setIndex(maxIndex + 1);
|
|
225106
225395
|
return store;
|
|
225107
225396
|
}
|
|
225397
|
+
/**
|
|
225398
|
+
* Crash repair (restore path): if the previous process died mid-turn, the
|
|
225399
|
+
* history has no closing turn_end — append one with outcome
|
|
225400
|
+
* "server_restart" and no duration (the crash time is unknown; the UI
|
|
225401
|
+
* shows "interrupted" instead of a fabricated number). Runs BEFORE
|
|
225402
|
+
* rebuildStoreFromRows so the store is built from the repaired rows.
|
|
225403
|
+
* The other constructor of turn_end entries is endActiveTurn (live paths).
|
|
225404
|
+
*/
|
|
225405
|
+
async repairInterruptedTurn(sessionId, rows) {
|
|
225406
|
+
let landingType = null;
|
|
225407
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
225408
|
+
try {
|
|
225409
|
+
const msg = JSON.parse(rows[i].data);
|
|
225410
|
+
if (msg.type === "system") continue;
|
|
225411
|
+
landingType = msg.type;
|
|
225412
|
+
} catch {
|
|
225413
|
+
landingType = "unparsable";
|
|
225414
|
+
}
|
|
225415
|
+
break;
|
|
225416
|
+
}
|
|
225417
|
+
if (landingType === null || landingType === "turn_end") return rows;
|
|
225418
|
+
const maxIndex = rows.reduce((m2, r) => Math.max(m2, r.entry_index), -1);
|
|
225419
|
+
const repair = { type: "turn_end", timestamp: Date.now(), outcome: "server_restart" };
|
|
225420
|
+
const data = JSON.stringify(repair);
|
|
225421
|
+
await this.storage.agentSessions.upsertEntry(sessionId, maxIndex + 1, data);
|
|
225422
|
+
console.log(`[AgentSession] Repaired interrupted turn for ${sessionId} (server_restart turn_end at ${maxIndex + 1})`);
|
|
225423
|
+
return [...rows, { entry_index: maxIndex + 1, data }];
|
|
225424
|
+
}
|
|
225108
225425
|
/**
|
|
225109
225426
|
* Restore sessions from database on startup.
|
|
225110
225427
|
* Creates dormant RunningSession objects with process=null for sessions that have entries.
|
|
@@ -225114,8 +225431,11 @@ ${details}`;
|
|
|
225114
225431
|
let restoredCount = 0;
|
|
225115
225432
|
for (const dbSession of allSessions) {
|
|
225116
225433
|
if (this.sessions.has(dbSession.id)) continue;
|
|
225117
|
-
|
|
225434
|
+
let entries = await this.storage.agentSessions.getEntries(dbSession.id);
|
|
225118
225435
|
if (entries.length === 0) continue;
|
|
225436
|
+
if (dbSession.status === "running") {
|
|
225437
|
+
entries = await this.repairInterruptedTurn(dbSession.id, entries);
|
|
225438
|
+
}
|
|
225119
225439
|
const store = this.rebuildStoreFromRows(entries, dbSession.id);
|
|
225120
225440
|
const permissionMode = dbSession.permission_mode === "plan" ? "plan" : "edit";
|
|
225121
225441
|
const runningSession = {
|
|
@@ -225136,7 +225456,8 @@ ${details}`;
|
|
|
225136
225456
|
eventChain: Promise.resolve(),
|
|
225137
225457
|
bgSpawnHintsThisTurn: 0,
|
|
225138
225458
|
taskStartedThisTurn: 0,
|
|
225139
|
-
lastActiveAt: Date.now()
|
|
225459
|
+
lastActiveAt: Date.now(),
|
|
225460
|
+
turnOpenSince: null
|
|
225140
225461
|
};
|
|
225141
225462
|
this.sessions.set(dbSession.id, runningSession);
|
|
225142
225463
|
await this.storage.agentSessions.updateStatusPreservingTimestamp(dbSession.id, "stopped");
|
|
@@ -225154,17 +225475,38 @@ ${details}`;
|
|
|
225154
225475
|
* message goes through wakeDormantSession, which replays the full copied
|
|
225155
225476
|
* context to a fresh process, so a branch also works with a different
|
|
225156
225477
|
* agent type than the source.
|
|
225157
|
-
*
|
|
225158
|
-
*
|
|
225478
|
+
*
|
|
225479
|
+
* `opts.upToEntryIndex`, when given, is an inclusive cutoff that must land
|
|
225480
|
+
* on a `turn_end` row — every branch then ends with its own tail divider.
|
|
225481
|
+
* With a cutoff the source's live process is never touched (no
|
|
225482
|
+
* finalizeStreamingEntry), so a historical branch is safe even while the
|
|
225483
|
+
* source is running. Without a cutoff (legacy full-copy callers) a running
|
|
225484
|
+
* source is refused outright — copying mid-turn would leave the branch
|
|
225485
|
+
* with a half-finished turn and no closing turn_end.
|
|
225159
225486
|
*/
|
|
225160
225487
|
async branchSession(sourceSessionId, agentTypeOverride, opts = {}) {
|
|
225161
225488
|
const source = this.sessions.get(sourceSessionId);
|
|
225162
225489
|
const sourceRow = await this.storage.agentSessions.getById(sourceSessionId);
|
|
225163
|
-
if (!source && !sourceRow) return
|
|
225164
|
-
if (source?.skipDb) return
|
|
225165
|
-
if (
|
|
225166
|
-
|
|
225167
|
-
|
|
225490
|
+
if (!source && !sourceRow) return { ok: false, reason: "not-found" };
|
|
225491
|
+
if (source?.skipDb) return { ok: false, reason: "empty-history" };
|
|
225492
|
+
if (opts.upToEntryIndex === void 0) {
|
|
225493
|
+
if (source?.status === "running") return { ok: false, reason: "running-needs-cutoff" };
|
|
225494
|
+
if (source) await this.finalizeStreamingEntry(source);
|
|
225495
|
+
}
|
|
225496
|
+
let entryRows = await this.storage.agentSessions.getEntries(sourceSessionId);
|
|
225497
|
+
if (opts.upToEntryIndex !== void 0) {
|
|
225498
|
+
const cut = entryRows.find((r) => r.entry_index === opts.upToEntryIndex);
|
|
225499
|
+
let cutType = null;
|
|
225500
|
+
if (cut) {
|
|
225501
|
+
try {
|
|
225502
|
+
cutType = JSON.parse(cut.data).type;
|
|
225503
|
+
} catch {
|
|
225504
|
+
}
|
|
225505
|
+
}
|
|
225506
|
+
if (cutType !== "turn_end") return { ok: false, reason: "invalid-cutoff" };
|
|
225507
|
+
entryRows = entryRows.filter((r) => r.entry_index <= opts.upToEntryIndex);
|
|
225508
|
+
}
|
|
225509
|
+
if (entryRows.length === 0) return { ok: false, reason: "empty-history" };
|
|
225168
225510
|
const projectId = source?.projectId ?? sourceRow.project_id;
|
|
225169
225511
|
const branch = source?.branch ?? (sourceRow.branch || null);
|
|
225170
225512
|
const permissionMode = source?.permissionMode ?? (sourceRow?.permission_mode === "plan" ? "plan" : "edit");
|
|
@@ -225216,12 +225558,13 @@ ${details}`;
|
|
|
225216
225558
|
bgSpawnHintsThisTurn: 0,
|
|
225217
225559
|
taskStartedThisTurn: 0,
|
|
225218
225560
|
lastActiveAt: Date.now(),
|
|
225561
|
+
turnOpenSince: null,
|
|
225219
225562
|
crossRemoteMcp: opts.crossRemoteMcp
|
|
225220
225563
|
};
|
|
225221
225564
|
this.sessions.set(newId, branched);
|
|
225222
225565
|
await this.emitDerivedBranchActivity(projectId, branch);
|
|
225223
225566
|
console.log(`[AgentSession] branchSession: ${sourceSessionId} \u2192 ${newId} (entries=${entryRows.length}, agentType=${agentType})`);
|
|
225224
|
-
return newId;
|
|
225567
|
+
return { ok: true, sessionId: newId };
|
|
225225
225568
|
}
|
|
225226
225569
|
/**
|
|
225227
225570
|
* Kill all active session processes and clear state for graceful shutdown
|
|
@@ -225907,12 +226250,12 @@ function extractLogText(logs, tailLines) {
|
|
|
225907
226250
|
const lines = joined.split("\n");
|
|
225908
226251
|
return stripAnsi(lines.slice(-tailLines).join("\n"));
|
|
225909
226252
|
}
|
|
225910
|
-
function
|
|
226253
|
+
function parseDbTimestamp2(s3) {
|
|
225911
226254
|
if (s3.includes("T") || s3.endsWith("Z")) return new Date(s3).getTime();
|
|
225912
226255
|
return (/* @__PURE__ */ new Date(s3.replace(" ", "T") + "Z")).getTime();
|
|
225913
226256
|
}
|
|
225914
226257
|
function formatRelativeAge(timestamp, nowMs) {
|
|
225915
|
-
const t =
|
|
226258
|
+
const t = parseDbTimestamp2(timestamp);
|
|
225916
226259
|
if (Number.isNaN(t)) return "unknown";
|
|
225917
226260
|
const sec = Math.max(0, Math.round((nowMs - t) / 1e3));
|
|
225918
226261
|
if (sec < 60) return `${sec}s ago`;
|
|
@@ -227123,8 +227466,8 @@ var ChatSessionManager = class {
|
|
|
227123
227466
|
};
|
|
227124
227467
|
}
|
|
227125
227468
|
const TURN_GRACE_MS = 1e3;
|
|
227126
|
-
const startedThisTurn = turnStartedAt != null &&
|
|
227127
|
-
const finishedThisTurn = turnStartedAt != null && lastRow.finished_at != null &&
|
|
227469
|
+
const startedThisTurn = turnStartedAt != null && parseDbTimestamp2(lastRow.started_at) >= turnStartedAt - TURN_GRACE_MS;
|
|
227470
|
+
const finishedThisTurn = turnStartedAt != null && lastRow.finished_at != null && parseDbTimestamp2(lastRow.finished_at) >= turnStartedAt - TURN_GRACE_MS;
|
|
227128
227471
|
return {
|
|
227129
227472
|
name: executor.name,
|
|
227130
227473
|
command: executor.command,
|
|
@@ -233149,14 +233492,21 @@ var routes11 = async (fastify2) => {
|
|
|
233149
233492
|
if (!sourceRow || !await fastify2.storage.projects.getById(sourceRow.project_id, userId)) {
|
|
233150
233493
|
return { ok: false, code: 404, error: "Session not found" };
|
|
233151
233494
|
}
|
|
233152
|
-
const
|
|
233495
|
+
const result = await fastify2.agentSessionManager.branchSession(
|
|
233153
233496
|
sourceSessionId,
|
|
233154
233497
|
opts.agentType,
|
|
233155
|
-
{ sessionId: opts.sessionId, crossRemoteMcp: opts.crossRemoteMcp }
|
|
233498
|
+
{ sessionId: opts.sessionId, crossRemoteMcp: opts.crossRemoteMcp, upToEntryIndex: opts.upToEntryIndex }
|
|
233156
233499
|
);
|
|
233157
|
-
if (!
|
|
233500
|
+
if (!result.ok) {
|
|
233501
|
+
if (result.reason === "invalid-cutoff") {
|
|
233502
|
+
return { ok: false, code: 400, error: "upToEntryIndex must reference a turn_end stop point" };
|
|
233503
|
+
}
|
|
233504
|
+
if (result.reason === "running-needs-cutoff") {
|
|
233505
|
+
return { ok: false, code: 409, error: "Session is running; branching requires a stop-point cutoff" };
|
|
233506
|
+
}
|
|
233158
233507
|
return { ok: false, code: 404, error: "Session not found or has no history to branch" };
|
|
233159
233508
|
}
|
|
233509
|
+
const newSessionId = result.sessionId;
|
|
233160
233510
|
const session = fastify2.agentSessionManager.getSession(newSessionId);
|
|
233161
233511
|
const messages = fastify2.agentSessionManager.getMessages(newSessionId);
|
|
233162
233512
|
const dbRow = await fastify2.storage.agentSessions.getById(newSessionId);
|
|
@@ -233906,7 +234256,10 @@ var routes11 = async (fastify2) => {
|
|
|
233906
234256
|
fastify2.post(
|
|
233907
234257
|
"/api/agent-sessions/:sessionId/branch",
|
|
233908
234258
|
async (req, reply) => {
|
|
233909
|
-
const { agentType } = req.body || {};
|
|
234259
|
+
const { agentType, upToEntryIndex } = req.body || {};
|
|
234260
|
+
if (upToEntryIndex !== void 0 && (!Number.isInteger(upToEntryIndex) || upToEntryIndex < 0)) {
|
|
234261
|
+
return reply.code(400).send({ error: "upToEntryIndex must be a non-negative integer" });
|
|
234262
|
+
}
|
|
233910
234263
|
const userId = requireAuth(req, reply);
|
|
233911
234264
|
if (userId === null) return;
|
|
233912
234265
|
if (req.params.sessionId.startsWith("remote-")) {
|
|
@@ -233927,7 +234280,7 @@ var routes11 = async (fastify2) => {
|
|
|
233927
234280
|
remoteInfo.remoteApiKey,
|
|
233928
234281
|
"POST",
|
|
233929
234282
|
`/api/path/agent-sessions/${remoteInfo.remoteSessionId}/branch`,
|
|
233930
|
-
{ agentType, sessionId: newRemoteSessionId, crossRemoteMcp: crossRemoteMcp2 }
|
|
234283
|
+
{ agentType, sessionId: newRemoteSessionId, crossRemoteMcp: crossRemoteMcp2, upToEntryIndex }
|
|
233931
234284
|
);
|
|
233932
234285
|
if (!result.ok) {
|
|
233933
234286
|
return reply.code(proxyStatus(result)).send(result.data);
|
|
@@ -233936,6 +234289,12 @@ var routes11 = async (fastify2) => {
|
|
|
233936
234289
|
if (remoteData.session.id !== newRemoteSessionId) {
|
|
233937
234290
|
return reply.code(409).send({ error: "Remote returned an unexpected session id; upgrade the remote" });
|
|
233938
234291
|
}
|
|
234292
|
+
if (upToEntryIndex !== void 0 && remoteData.messages.length > upToEntryIndex + 1) {
|
|
234293
|
+
console.error(
|
|
234294
|
+
`[Branch] Remote ${remoteInfo.remoteServerId} ignored branch cutoff (${remoteData.messages.length} messages > cutoff ${upToEntryIndex}) \u2014 version drift, upgrade the remote`
|
|
234295
|
+
);
|
|
234296
|
+
return reply.code(409).send({ error: "Remote ignored branch cutoff; upgrade the remote" });
|
|
234297
|
+
}
|
|
233939
234298
|
fastify2.remoteSessionMap.set(localSessionId, {
|
|
233940
234299
|
remoteServerId: remoteInfo.remoteServerId,
|
|
233941
234300
|
remoteUrl: remoteInfo.remoteUrl,
|
|
@@ -233979,7 +234338,8 @@ var routes11 = async (fastify2) => {
|
|
|
233979
234338
|
const branched = await performLocalBranch(req.params.sessionId, userId, {
|
|
233980
234339
|
agentType,
|
|
233981
234340
|
sessionId: preSessionId,
|
|
233982
|
-
crossRemoteMcp
|
|
234341
|
+
crossRemoteMcp,
|
|
234342
|
+
upToEntryIndex
|
|
233983
234343
|
});
|
|
233984
234344
|
if (!branched.ok) {
|
|
233985
234345
|
return reply.code(branched.code).send({ error: branched.error });
|
|
@@ -233988,13 +234348,17 @@ var routes11 = async (fastify2) => {
|
|
|
233988
234348
|
}
|
|
233989
234349
|
);
|
|
233990
234350
|
fastify2.post("/api/path/agent-sessions/:sessionId/branch", async (req, reply) => {
|
|
233991
|
-
const { agentType, sessionId, crossRemoteMcp } = req.body || {};
|
|
234351
|
+
const { agentType, sessionId, crossRemoteMcp, upToEntryIndex } = req.body || {};
|
|
234352
|
+
if (upToEntryIndex !== void 0 && (!Number.isInteger(upToEntryIndex) || upToEntryIndex < 0)) {
|
|
234353
|
+
return reply.code(400).send({ error: "upToEntryIndex must be a non-negative integer" });
|
|
234354
|
+
}
|
|
233992
234355
|
const userId = requireAuth(req, reply);
|
|
233993
234356
|
if (userId === null) return;
|
|
233994
234357
|
const branched = await performLocalBranch(req.params.sessionId, userId, {
|
|
233995
234358
|
agentType,
|
|
233996
234359
|
sessionId,
|
|
233997
|
-
crossRemoteMcp
|
|
234360
|
+
crossRemoteMcp,
|
|
234361
|
+
upToEntryIndex
|
|
233998
234362
|
});
|
|
233999
234363
|
if (!branched.ok) {
|
|
234000
234364
|
return reply.code(branched.code).send({ error: branched.error });
|
|
@@ -234180,6 +234544,13 @@ var routes11 = async (fastify2) => {
|
|
|
234180
234544
|
remoteInfo.branch ?? null,
|
|
234181
234545
|
normalizedTitle
|
|
234182
234546
|
);
|
|
234547
|
+
if (normalizedTitle) {
|
|
234548
|
+
try {
|
|
234549
|
+
await fastify2.storage.searchCache.updateCachedSessionTitle(req.params.sessionId, normalizedTitle);
|
|
234550
|
+
} catch (err) {
|
|
234551
|
+
console.error("[API] searchCache.updateCachedSessionTitle failed:", err);
|
|
234552
|
+
}
|
|
234553
|
+
}
|
|
234183
234554
|
}
|
|
234184
234555
|
return reply.code(proxyStatus(result)).send(result.data);
|
|
234185
234556
|
}
|
|
@@ -234309,6 +234680,77 @@ var branch_activity_routes_default = (0, import_fastify_plugin13.default)(routes
|
|
|
234309
234680
|
|
|
234310
234681
|
// src/routes/merge-status-routes.ts
|
|
234311
234682
|
var import_fastify_plugin14 = __toESM(require_plugin2(), 1);
|
|
234683
|
+
function resolveTargets(comparisons, storedTargets) {
|
|
234684
|
+
const sources = [];
|
|
234685
|
+
const effective = comparisons.map((comparison) => {
|
|
234686
|
+
if (comparison.target !== void 0) {
|
|
234687
|
+
sources.push("request");
|
|
234688
|
+
return comparison;
|
|
234689
|
+
}
|
|
234690
|
+
const storedTarget = storedTargets.get(comparison.branch);
|
|
234691
|
+
if (storedTarget !== void 0) {
|
|
234692
|
+
sources.push("stored");
|
|
234693
|
+
return { branch: comparison.branch, target: storedTarget };
|
|
234694
|
+
}
|
|
234695
|
+
sources.push("default");
|
|
234696
|
+
return comparison;
|
|
234697
|
+
});
|
|
234698
|
+
return { comparisons: effective, sources };
|
|
234699
|
+
}
|
|
234700
|
+
function annotateEntries(entries, comparisons, sources) {
|
|
234701
|
+
return entries.map((entry, index) => ({
|
|
234702
|
+
...entry,
|
|
234703
|
+
targetSource: sources[index],
|
|
234704
|
+
requestedTarget: comparisons[index]?.target ?? entry.target
|
|
234705
|
+
}));
|
|
234706
|
+
}
|
|
234707
|
+
var MERGE_PAIR_ERRORS = /* @__PURE__ */ new Set([
|
|
234708
|
+
"target-not-found",
|
|
234709
|
+
"branch-not-found",
|
|
234710
|
+
"no-default-branch"
|
|
234711
|
+
]);
|
|
234712
|
+
var MERGE_STATUS_VALUES = /* @__PURE__ */ new Set([
|
|
234713
|
+
"merged",
|
|
234714
|
+
"partial",
|
|
234715
|
+
"unmerged",
|
|
234716
|
+
"no-unique-commits"
|
|
234717
|
+
]);
|
|
234718
|
+
function isRecord(value) {
|
|
234719
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
234720
|
+
}
|
|
234721
|
+
function isRemoteMergeStatusEntry(value, comparison) {
|
|
234722
|
+
if (!isRecord(value) || value.branch !== comparison.branch) return false;
|
|
234723
|
+
if (value.target !== null && typeof value.target !== "string") return false;
|
|
234724
|
+
if (value.error !== void 0) {
|
|
234725
|
+
if (!MERGE_PAIR_ERRORS.has(value.error)) return false;
|
|
234726
|
+
if (value.status !== void 0 || value.unmergedCount !== void 0 || value.dirty !== void 0) {
|
|
234727
|
+
return false;
|
|
234728
|
+
}
|
|
234729
|
+
if (value.error === "target-not-found") {
|
|
234730
|
+
return comparison.target !== void 0 && value.target === null;
|
|
234731
|
+
}
|
|
234732
|
+
if (value.error === "no-default-branch") {
|
|
234733
|
+
return comparison.target === void 0 && value.target === null;
|
|
234734
|
+
}
|
|
234735
|
+
return typeof value.target === "string" && (comparison.target === void 0 || value.target === comparison.target);
|
|
234736
|
+
}
|
|
234737
|
+
return typeof value.target === "string" && (comparison.target === void 0 || value.target === comparison.target) && MERGE_STATUS_VALUES.has(value.status) && Number.isInteger(value.unmergedCount) && value.unmergedCount >= 0 && typeof value.dirty === "boolean";
|
|
234738
|
+
}
|
|
234739
|
+
function parseRemoteEntries(data, comparisons) {
|
|
234740
|
+
if (!isRecord(data) || !Array.isArray(data.entries)) return null;
|
|
234741
|
+
if (data.entries.length !== comparisons.length) return null;
|
|
234742
|
+
if (!data.entries.every((entry, index) => isRemoteMergeStatusEntry(entry, comparisons[index]))) {
|
|
234743
|
+
return null;
|
|
234744
|
+
}
|
|
234745
|
+
const entries = data.entries;
|
|
234746
|
+
const implicitEntries = entries.filter((_entry, index) => comparisons[index].target === void 0);
|
|
234747
|
+
const hasNoDefault = implicitEntries.some((entry) => entry.error === "no-default-branch");
|
|
234748
|
+
if (hasNoDefault) {
|
|
234749
|
+
return implicitEntries.every((entry) => entry.error === "no-default-branch") ? entries : null;
|
|
234750
|
+
}
|
|
234751
|
+
const resolvedDefault = implicitEntries[0]?.target;
|
|
234752
|
+
return implicitEntries.every((entry) => entry.target === resolvedDefault) ? entries : null;
|
|
234753
|
+
}
|
|
234312
234754
|
function legacyRemoteLabel(remoteUrl) {
|
|
234313
234755
|
try {
|
|
234314
234756
|
return new URL(remoteUrl).hostname;
|
|
@@ -234340,6 +234782,10 @@ async function getRemoteConfig4(fastify2, project) {
|
|
|
234340
234782
|
return null;
|
|
234341
234783
|
}
|
|
234342
234784
|
var MAX_COMPARISONS = 50;
|
|
234785
|
+
var MAX_NAME_LENGTH = 256;
|
|
234786
|
+
function isValidName(value) {
|
|
234787
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_NAME_LENGTH;
|
|
234788
|
+
}
|
|
234343
234789
|
function parseComparisons(body) {
|
|
234344
234790
|
if (!body || typeof body !== "object") return null;
|
|
234345
234791
|
const comparisons = body.comparisons;
|
|
@@ -234395,6 +234841,11 @@ var routes13 = async (fastify2) => {
|
|
|
234395
234841
|
if (!comparisons) {
|
|
234396
234842
|
return reply.code(400).send({ error: "Invalid comparisons" });
|
|
234397
234843
|
}
|
|
234844
|
+
const storedTargets = await fastify2.storage.mergeTargets.getForBranches(
|
|
234845
|
+
project.id,
|
|
234846
|
+
comparisons.map(({ branch }) => branch)
|
|
234847
|
+
);
|
|
234848
|
+
const resolved = resolveTargets(comparisons, storedTargets);
|
|
234398
234849
|
if (!project.path) {
|
|
234399
234850
|
const remoteConfig = await getRemoteConfig4(fastify2, project);
|
|
234400
234851
|
if (!remoteConfig) {
|
|
@@ -234406,28 +234857,83 @@ var routes13 = async (fastify2) => {
|
|
|
234406
234857
|
remoteConfig.apiKey,
|
|
234407
234858
|
"POST",
|
|
234408
234859
|
"/api/path/branches/merge-status",
|
|
234409
|
-
{ path: remoteConfig.remotePath, comparisons },
|
|
234860
|
+
{ path: remoteConfig.remotePath, comparisons: resolved.comparisons },
|
|
234410
234861
|
{ reverseConnectManager: fastify2.reverseConnectManager }
|
|
234411
234862
|
);
|
|
234412
234863
|
if (!result.ok) {
|
|
234413
234864
|
return reply.code(proxyStatus(result)).send(result.data);
|
|
234414
234865
|
}
|
|
234415
|
-
const
|
|
234866
|
+
const entries = parseRemoteEntries(result.data, resolved.comparisons);
|
|
234867
|
+
if (!entries) {
|
|
234868
|
+
return reply.code(502).send({ error: "Remote merge-status response invalid" });
|
|
234869
|
+
}
|
|
234416
234870
|
return reply.code(200).send({
|
|
234417
234871
|
repository: {
|
|
234418
234872
|
kind: "remote",
|
|
234419
234873
|
remoteServerId: remoteConfig.serverId,
|
|
234420
234874
|
label: remoteConfig.serverName
|
|
234421
234875
|
},
|
|
234422
|
-
entries:
|
|
234876
|
+
entries: annotateEntries(
|
|
234877
|
+
entries,
|
|
234878
|
+
resolved.comparisons,
|
|
234879
|
+
resolved.sources
|
|
234880
|
+
)
|
|
234423
234881
|
});
|
|
234424
234882
|
}
|
|
234425
|
-
|
|
234426
|
-
|
|
234427
|
-
|
|
234428
|
-
|
|
234429
|
-
|
|
234430
|
-
|
|
234883
|
+
try {
|
|
234884
|
+
const entries = computeMergeStatusPairs(project.path, resolved.comparisons);
|
|
234885
|
+
return reply.code(200).send({
|
|
234886
|
+
repository: { kind: "local", label: "Local" },
|
|
234887
|
+
entries: annotateEntries(entries, resolved.comparisons, resolved.sources)
|
|
234888
|
+
});
|
|
234889
|
+
} catch (error48) {
|
|
234890
|
+
const statusCode = error48.statusCode ?? 500;
|
|
234891
|
+
const message = error48 instanceof Error ? error48.message : "Failed to compute merge status";
|
|
234892
|
+
return reply.code(statusCode).send({ error: message });
|
|
234893
|
+
}
|
|
234894
|
+
}
|
|
234895
|
+
);
|
|
234896
|
+
fastify2.put(
|
|
234897
|
+
"/api/projects/:id/branches/merge-target",
|
|
234898
|
+
async (req, reply) => {
|
|
234899
|
+
const userId = requireAuth(req, reply);
|
|
234900
|
+
if (userId === null) return;
|
|
234901
|
+
const project = await fastify2.storage.projects.getById(req.params.id, userId);
|
|
234902
|
+
if (!project) {
|
|
234903
|
+
return reply.code(404).send({ error: "Project not found" });
|
|
234904
|
+
}
|
|
234905
|
+
if (!req.body || typeof req.body !== "object") {
|
|
234906
|
+
return reply.code(400).send({ error: "Invalid merge target" });
|
|
234907
|
+
}
|
|
234908
|
+
const body = req.body;
|
|
234909
|
+
const { branch, target, ifAbsent } = body;
|
|
234910
|
+
if (!isValidName(branch) || target !== null && !isValidName(target) || ifAbsent !== void 0 && typeof ifAbsent !== "boolean" || ifAbsent === true && target === null) {
|
|
234911
|
+
return reply.code(400).send({ error: "Invalid merge target" });
|
|
234912
|
+
}
|
|
234913
|
+
let changed;
|
|
234914
|
+
let storedTarget;
|
|
234915
|
+
if (target === null) {
|
|
234916
|
+
changed = await fastify2.storage.mergeTargets.delete(project.id, branch);
|
|
234917
|
+
storedTarget = null;
|
|
234918
|
+
} else if (ifAbsent === true) {
|
|
234919
|
+
changed = await fastify2.storage.mergeTargets.insertIfAbsent(project.id, branch, target);
|
|
234920
|
+
if (changed) {
|
|
234921
|
+
storedTarget = target;
|
|
234922
|
+
} else {
|
|
234923
|
+
storedTarget = (await fastify2.storage.mergeTargets.getForBranches(project.id, [branch])).get(branch) ?? null;
|
|
234924
|
+
}
|
|
234925
|
+
} else {
|
|
234926
|
+
changed = await fastify2.storage.mergeTargets.upsert(project.id, branch, target);
|
|
234927
|
+
storedTarget = target;
|
|
234928
|
+
}
|
|
234929
|
+
if (changed) {
|
|
234930
|
+
fastify2.eventBus.emit({
|
|
234931
|
+
type: "merge-target:updated",
|
|
234932
|
+
projectId: project.id,
|
|
234933
|
+
branch
|
|
234934
|
+
});
|
|
234935
|
+
}
|
|
234936
|
+
return reply.code(200).send({ branch, target: storedTarget });
|
|
234431
234937
|
}
|
|
234432
234938
|
);
|
|
234433
234939
|
};
|
|
@@ -234873,7 +235379,7 @@ var SCROLLBACK_MAX = 1e5;
|
|
|
234873
235379
|
var FONT_SIZE_MIN = 8;
|
|
234874
235380
|
var FONT_SIZE_MAX = 32;
|
|
234875
235381
|
var DEFAULT_CONVERSATION_SETTINGS = {
|
|
234876
|
-
agentFontSize:
|
|
235382
|
+
agentFontSize: 16,
|
|
234877
235383
|
chatFontSize: 15,
|
|
234878
235384
|
filesTreeFontSize: 14,
|
|
234879
235385
|
filesContentFontSize: 14
|
|
@@ -237639,6 +238145,244 @@ var routes28 = async (fastify2) => {
|
|
|
237639
238145
|
};
|
|
237640
238146
|
var schedule_routes_default = (0, import_fastify_plugin29.default)(routes28, { name: "schedule-routes" });
|
|
237641
238147
|
|
|
238148
|
+
// src/search/catalog.ts
|
|
238149
|
+
var parseDbTimestamp3 = (ts) => {
|
|
238150
|
+
if (!ts) return null;
|
|
238151
|
+
const ms = Date.parse(ts.replace(" ", "T") + "Z");
|
|
238152
|
+
return Number.isNaN(ms) ? null : ms;
|
|
238153
|
+
};
|
|
238154
|
+
async function buildSearchCatalog(deps, projectId, projectPath) {
|
|
238155
|
+
pruneWorktrees(projectPath);
|
|
238156
|
+
const workspaces = getWorktreeBranches(projectPath);
|
|
238157
|
+
const sessions = await deps.storage.agentSessions.getByProjectId(projectId);
|
|
238158
|
+
const counts = new Map(
|
|
238159
|
+
(await deps.storage.agentSessions.countEntries()).map((r) => [r.session_id, r.cnt])
|
|
238160
|
+
);
|
|
238161
|
+
return {
|
|
238162
|
+
snapshotAt: Date.now(),
|
|
238163
|
+
workspaces,
|
|
238164
|
+
sessions: sessions.map((s3) => ({ s: s3, entryCount: counts.get(s3.id) ?? 0 })).filter(({ s: s3, entryCount }) => shouldShowBranchSessionInList({
|
|
238165
|
+
entryCount,
|
|
238166
|
+
processAlive: deps.getProcessAlive?.(s3.id) ?? false
|
|
238167
|
+
})).map(({ s: s3, entryCount }) => ({
|
|
238168
|
+
id: s3.id,
|
|
238169
|
+
branch: s3.branch === "" ? null : s3.branch,
|
|
238170
|
+
title: s3.title ?? null,
|
|
238171
|
+
lastActiveAt: s3.last_user_message_at ?? parseDbTimestamp3(s3.updated_at),
|
|
238172
|
+
favoritedAt: s3.favorited_at ?? null,
|
|
238173
|
+
entryCount
|
|
238174
|
+
}))
|
|
238175
|
+
};
|
|
238176
|
+
}
|
|
238177
|
+
|
|
238178
|
+
// src/search/refresh.ts
|
|
238179
|
+
var DEFAULT_TTL_MS = 3e4;
|
|
238180
|
+
var DEFAULT_DEADLINE_MS = 5e3;
|
|
238181
|
+
var PER_WORKER_CONCURRENCY = 3;
|
|
238182
|
+
var LOCAL_CONCURRENCY = 4;
|
|
238183
|
+
async function listSearchTargets(storage, userId) {
|
|
238184
|
+
const projects = await storage.projects.getAll(userId);
|
|
238185
|
+
const targets = [];
|
|
238186
|
+
for (const p2 of projects) {
|
|
238187
|
+
if (p2.path) targets.push({ projectId: p2.id, targetId: "local", projectPath: p2.path });
|
|
238188
|
+
const remotes = await storage.projectRemotes.getByProject(p2.id);
|
|
238189
|
+
for (const r of remotes) {
|
|
238190
|
+
targets.push({
|
|
238191
|
+
projectId: p2.id,
|
|
238192
|
+
targetId: r.remote_server_id,
|
|
238193
|
+
remote: {
|
|
238194
|
+
serverId: r.remote_server_id,
|
|
238195
|
+
url: r.server_url ?? "",
|
|
238196
|
+
apiKey: r.server_api_key ?? "",
|
|
238197
|
+
remotePath: r.remote_path
|
|
238198
|
+
}
|
|
238199
|
+
});
|
|
238200
|
+
}
|
|
238201
|
+
}
|
|
238202
|
+
return targets;
|
|
238203
|
+
}
|
|
238204
|
+
function computeCacheState(states, targets, now2, ttlMs = DEFAULT_TTL_MS) {
|
|
238205
|
+
if (targets.length === 0) return "fresh";
|
|
238206
|
+
const stateByKey = new Map(states.map((s3) => [`${s3.project_id}:${s3.target_id}`, s3]));
|
|
238207
|
+
const matched = targets.map((t) => stateByKey.get(`${t.projectId}:${t.targetId}`));
|
|
238208
|
+
if (matched.some((s3) => s3?.last_success_at == null)) return "cold";
|
|
238209
|
+
return matched.every((s3) => now2 - (s3.last_success_at ?? 0) <= ttlMs) ? "fresh" : "stale";
|
|
238210
|
+
}
|
|
238211
|
+
async function runWithConcurrency(tasks, limit) {
|
|
238212
|
+
const queue = [...tasks];
|
|
238213
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, queue.length)) }, async () => {
|
|
238214
|
+
for (let task = queue.shift(); task; task = queue.shift()) {
|
|
238215
|
+
await task();
|
|
238216
|
+
}
|
|
238217
|
+
});
|
|
238218
|
+
await Promise.all(workers);
|
|
238219
|
+
}
|
|
238220
|
+
function createSearchRefresher(deps) {
|
|
238221
|
+
const ttlMs = deps.ttlMs ?? DEFAULT_TTL_MS;
|
|
238222
|
+
const deadlineMs = deps.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
238223
|
+
const now2 = deps.now ?? Date.now;
|
|
238224
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
238225
|
+
function refreshTarget(t) {
|
|
238226
|
+
const key = `${t.projectId}:${t.targetId}`;
|
|
238227
|
+
const existing = inflight.get(key);
|
|
238228
|
+
if (existing) return existing;
|
|
238229
|
+
const run2 = (async () => {
|
|
238230
|
+
try {
|
|
238231
|
+
const snapshot = t.targetId === "local" ? await deps.buildLocalCatalog(t.projectId, t.projectPath ?? "") : await deps.fetchRemoteCatalog(t);
|
|
238232
|
+
await deps.storage.searchCache.applyCatalogSnapshot(t.projectId, t.targetId, snapshot);
|
|
238233
|
+
} catch (err) {
|
|
238234
|
+
await deps.storage.searchCache.recordSyncFailure(
|
|
238235
|
+
t.projectId,
|
|
238236
|
+
t.targetId,
|
|
238237
|
+
err instanceof Error ? err.message : String(err)
|
|
238238
|
+
).catch(() => {
|
|
238239
|
+
});
|
|
238240
|
+
}
|
|
238241
|
+
})();
|
|
238242
|
+
inflight.set(key, run2);
|
|
238243
|
+
void run2.finally(() => inflight.delete(key));
|
|
238244
|
+
return run2;
|
|
238245
|
+
}
|
|
238246
|
+
async function refreshAll(userId) {
|
|
238247
|
+
const targets = await listSearchTargets(deps.storage, userId);
|
|
238248
|
+
const states = await deps.storage.searchCache.getSyncStates([...new Set(targets.map((t) => t.projectId))]);
|
|
238249
|
+
const stateByKey = new Map(states.map((s3) => [`${s3.project_id}:${s3.target_id}`, s3]));
|
|
238250
|
+
const due = targets.filter((t) => {
|
|
238251
|
+
const s3 = stateByKey.get(`${t.projectId}:${t.targetId}`);
|
|
238252
|
+
return !s3?.last_success_at || now2() - s3.last_success_at > ttlMs;
|
|
238253
|
+
});
|
|
238254
|
+
const byWorker = /* @__PURE__ */ new Map();
|
|
238255
|
+
for (const t of due) {
|
|
238256
|
+
const k2 = t.targetId;
|
|
238257
|
+
byWorker.set(k2, [...byWorker.get(k2) ?? [], t]);
|
|
238258
|
+
}
|
|
238259
|
+
const lanes = [...byWorker.entries()].map(
|
|
238260
|
+
([workerId, ts]) => runWithConcurrency(
|
|
238261
|
+
ts.map((t) => () => refreshTarget(t)),
|
|
238262
|
+
workerId === "local" ? LOCAL_CONCURRENCY : PER_WORKER_CONCURRENCY
|
|
238263
|
+
)
|
|
238264
|
+
);
|
|
238265
|
+
const all = Promise.all(lanes).then(() => void 0);
|
|
238266
|
+
await Promise.race([
|
|
238267
|
+
all,
|
|
238268
|
+
new Promise((resolve3) => {
|
|
238269
|
+
const timer = setTimeout(resolve3, deadlineMs);
|
|
238270
|
+
timer.unref?.();
|
|
238271
|
+
})
|
|
238272
|
+
]);
|
|
238273
|
+
}
|
|
238274
|
+
return { refreshAll };
|
|
238275
|
+
}
|
|
238276
|
+
|
|
238277
|
+
// src/routes/search-routes.ts
|
|
238278
|
+
var searchRoutes = async (fastify2) => {
|
|
238279
|
+
fastify2.get(
|
|
238280
|
+
"/api/path/search-catalog",
|
|
238281
|
+
async (req, reply) => {
|
|
238282
|
+
const projectPath = req.query.path;
|
|
238283
|
+
if (!projectPath) {
|
|
238284
|
+
return reply.code(400).send({ error: "path is required" });
|
|
238285
|
+
}
|
|
238286
|
+
const project = await fastify2.storage.projects.getByPath(projectPath);
|
|
238287
|
+
if (!project) {
|
|
238288
|
+
return reply.code(200).send({ snapshotAt: Date.now(), workspaces: [], sessions: [] });
|
|
238289
|
+
}
|
|
238290
|
+
try {
|
|
238291
|
+
const catalog = await buildSearchCatalog(
|
|
238292
|
+
{
|
|
238293
|
+
storage: fastify2.storage,
|
|
238294
|
+
getProcessAlive: (id) => fastify2.agentSessionManager.getSessionProcessAlive(id)
|
|
238295
|
+
},
|
|
238296
|
+
project.id,
|
|
238297
|
+
projectPath
|
|
238298
|
+
);
|
|
238299
|
+
return reply.code(200).send(catalog);
|
|
238300
|
+
} catch (error48) {
|
|
238301
|
+
return reply.code(500).send({ error: String(error48) });
|
|
238302
|
+
}
|
|
238303
|
+
}
|
|
238304
|
+
);
|
|
238305
|
+
const refresher = createSearchRefresher({
|
|
238306
|
+
storage: fastify2.storage,
|
|
238307
|
+
buildLocalCatalog: (projectId, projectPath) => buildSearchCatalog(
|
|
238308
|
+
{
|
|
238309
|
+
storage: fastify2.storage,
|
|
238310
|
+
getProcessAlive: (id) => fastify2.agentSessionManager.getSessionProcessAlive(id)
|
|
238311
|
+
},
|
|
238312
|
+
projectId,
|
|
238313
|
+
projectPath
|
|
238314
|
+
),
|
|
238315
|
+
fetchRemoteCatalog: async (target) => {
|
|
238316
|
+
const r = target.remote;
|
|
238317
|
+
if (!r) throw new Error("remote target without remote config");
|
|
238318
|
+
const params = new URLSearchParams({ path: r.remotePath });
|
|
238319
|
+
const result = await proxyToRemoteAuto(
|
|
238320
|
+
r.serverId,
|
|
238321
|
+
r.url,
|
|
238322
|
+
r.apiKey,
|
|
238323
|
+
"GET",
|
|
238324
|
+
`/api/path/search-catalog?${params.toString()}`,
|
|
238325
|
+
void 0,
|
|
238326
|
+
{ reverseConnectManager: fastify2.reverseConnectManager, timeoutMs: 2e3 }
|
|
238327
|
+
);
|
|
238328
|
+
if (!result.ok) {
|
|
238329
|
+
throw new Error(`catalog fetch failed: ${result.status} ${result.errorCode ?? ""}`);
|
|
238330
|
+
}
|
|
238331
|
+
const data = result.data;
|
|
238332
|
+
const sessions = await Promise.all(data.sessions.map(async (s3) => {
|
|
238333
|
+
const localSessionId = `remote-${target.targetId}-${target.projectId}-${s3.id}`;
|
|
238334
|
+
const mapBranch = s3.branch ?? "";
|
|
238335
|
+
if (!fastify2.remoteSessionMap.has(localSessionId)) {
|
|
238336
|
+
fastify2.remoteSessionMap.set(localSessionId, {
|
|
238337
|
+
remoteServerId: target.targetId,
|
|
238338
|
+
remoteUrl: r.url,
|
|
238339
|
+
remoteApiKey: r.apiKey,
|
|
238340
|
+
remoteSessionId: s3.id,
|
|
238341
|
+
branch: mapBranch
|
|
238342
|
+
});
|
|
238343
|
+
}
|
|
238344
|
+
await fastify2.storage.remoteSessionMappings.upsert(
|
|
238345
|
+
localSessionId,
|
|
238346
|
+
target.projectId,
|
|
238347
|
+
target.targetId,
|
|
238348
|
+
s3.id,
|
|
238349
|
+
mapBranch
|
|
238350
|
+
);
|
|
238351
|
+
return { ...s3, id: localSessionId };
|
|
238352
|
+
}));
|
|
238353
|
+
return { workspaces: data.workspaces, sessions };
|
|
238354
|
+
}
|
|
238355
|
+
});
|
|
238356
|
+
async function currentCacheState(userId) {
|
|
238357
|
+
const targets = await listSearchTargets(fastify2.storage, userId);
|
|
238358
|
+
const states = await fastify2.storage.searchCache.getSyncStates(
|
|
238359
|
+
[...new Set(targets.map((t) => t.projectId))]
|
|
238360
|
+
);
|
|
238361
|
+
return computeCacheState(states, targets, Date.now());
|
|
238362
|
+
}
|
|
238363
|
+
fastify2.get(
|
|
238364
|
+
"/api/search",
|
|
238365
|
+
async (req, reply) => {
|
|
238366
|
+
const userId = requireAuth(req, reply);
|
|
238367
|
+
if (userId === null) return;
|
|
238368
|
+
const query = (req.query.q ?? "").slice(0, 256);
|
|
238369
|
+
const parsed = parseInt(req.query.limitPerGroup ?? "10", 10);
|
|
238370
|
+
const limitPerGroup = Math.min(Math.max(Number.isNaN(parsed) ? 10 : parsed, 1), 50);
|
|
238371
|
+
const results = await fastify2.storage.searchCache.search({ userId, query, limitPerGroup });
|
|
238372
|
+
const cacheState = await currentCacheState(userId);
|
|
238373
|
+
return reply.code(200).send({ ...results, cacheState });
|
|
238374
|
+
}
|
|
238375
|
+
);
|
|
238376
|
+
fastify2.post("/api/search/refresh", async (req, reply) => {
|
|
238377
|
+
const userId = requireAuth(req, reply);
|
|
238378
|
+
if (userId === null) return;
|
|
238379
|
+
await refresher.refreshAll(userId);
|
|
238380
|
+
const cacheState = await currentCacheState(userId);
|
|
238381
|
+
return reply.code(200).send({ ok: true, cacheState });
|
|
238382
|
+
});
|
|
238383
|
+
};
|
|
238384
|
+
var search_routes_default = searchRoutes;
|
|
238385
|
+
|
|
237642
238386
|
// ../../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
|
|
237643
238387
|
import { Readable as Readable3 } from "stream";
|
|
237644
238388
|
var fastifyRequestToRequest = (req) => {
|
|
@@ -238896,6 +239640,7 @@ var createServer = async (opts) => {
|
|
|
238896
239640
|
server.register(remote_routes_default);
|
|
238897
239641
|
server.register(remote_server_routes_default);
|
|
238898
239642
|
server.register(project_remote_routes_default);
|
|
239643
|
+
server.register(search_routes_default);
|
|
238899
239644
|
server.register(executor_group_routes_default);
|
|
238900
239645
|
server.register(executor_routes_default);
|
|
238901
239646
|
server.register(schedule_routes_default);
|