@vibedeckx/linux-x64 0.2.1 → 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 +932 -86
- 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
|
}
|
|
@@ -223716,6 +223977,17 @@ var AgentSessionManager = class {
|
|
|
223716
223977
|
setEventBus(eventBus) {
|
|
223717
223978
|
this.eventBus = eventBus;
|
|
223718
223979
|
}
|
|
223980
|
+
/**
|
|
223981
|
+
* Broadcast a freshly-generated title on the global event bus so the sidebar
|
|
223982
|
+
* (`useResidentSessions`) updates regardless of which workspace is currently
|
|
223983
|
+
* focused. The per-session WS `titleUpdated` broadcast only reaches the one
|
|
223984
|
+
* mounted AgentConversation, which is lost when the user navigates to another
|
|
223985
|
+
* workspace before the ~1-2s title generation completes. Used by both the
|
|
223986
|
+
* local title path and the remote proxy path.
|
|
223987
|
+
*/
|
|
223988
|
+
emitSessionTitle(projectId, branch, sessionId, title) {
|
|
223989
|
+
this.eventBus?.emit({ type: "session:title", projectId, branch, sessionId, title });
|
|
223990
|
+
}
|
|
223719
223991
|
/**
|
|
223720
223992
|
* Single emit path for `branch:activity` events. Derives the current
|
|
223721
223993
|
* activity from local DB state (the source of truth — see
|
|
@@ -223953,7 +224225,8 @@ var AgentSessionManager = class {
|
|
|
223953
224225
|
eventChain: Promise.resolve(),
|
|
223954
224226
|
bgSpawnHintsThisTurn: 0,
|
|
223955
224227
|
taskStartedThisTurn: 0,
|
|
223956
|
-
lastActiveAt: Date.now()
|
|
224228
|
+
lastActiveAt: Date.now(),
|
|
224229
|
+
turnOpenSince: null
|
|
223957
224230
|
};
|
|
223958
224231
|
this.sessions.set(sessionId, session);
|
|
223959
224232
|
const provider = getProvider(agentType);
|
|
@@ -224093,21 +224366,26 @@ var AgentSessionManager = class {
|
|
|
224093
224366
|
if (action.kind === "commit") {
|
|
224094
224367
|
await this.commitCompletion(session, action.payload);
|
|
224095
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");
|
|
224096
224383
|
session.status = code === 0 ? "stopped" : "error";
|
|
224097
224384
|
if (!session.skipDb) {
|
|
224098
224385
|
this.storage.agentSessions.updateStatus(session.id, session.status).catch((err) => {
|
|
224099
224386
|
console.error(`[AgentSession] Failed to update status for ${session.id}:`, err);
|
|
224100
224387
|
});
|
|
224101
224388
|
}
|
|
224102
|
-
if (code !== 0 && !spawnFailed && !session.producedOutput) {
|
|
224103
|
-
this.pushEntry(session.id, {
|
|
224104
|
-
type: "error",
|
|
224105
|
-
message: this.buildStartupFailureMessage(session.agentType, stderrTail),
|
|
224106
|
-
timestamp: Date.now()
|
|
224107
|
-
}, true).catch((err) => {
|
|
224108
|
-
console.error(`[AgentSession] Failed to push startup-failure entry for ${session.id}:`, err);
|
|
224109
|
-
});
|
|
224110
|
-
}
|
|
224111
224389
|
this.broadcastPatch(session.id, ConversationPatch.updateStatus(session.status));
|
|
224112
224390
|
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: session.status });
|
|
224113
224391
|
this.broadcastRaw(session.id, { finished: true });
|
|
@@ -224216,6 +224494,7 @@ var AgentSessionManager = class {
|
|
|
224216
224494
|
summaryText
|
|
224217
224495
|
});
|
|
224218
224496
|
await this.emitDerivedBranchActivity(session.projectId, session.branch);
|
|
224497
|
+
await this.endActiveTurn(session, "completed");
|
|
224219
224498
|
if (session.status !== "stopped") {
|
|
224220
224499
|
session.status = "stopped";
|
|
224221
224500
|
if (!session.skipDb) await this.storage.agentSessions.updateStatus(sessionId, "stopped");
|
|
@@ -224410,6 +224689,7 @@ var AgentSessionManager = class {
|
|
|
224410
224689
|
}
|
|
224411
224690
|
if (event.subtype === "error") {
|
|
224412
224691
|
this.applyCompletionTimerAction(session, session.completion.errorResult());
|
|
224692
|
+
await this.endActiveTurn(session, "failed");
|
|
224413
224693
|
if (session.status !== "stopped") {
|
|
224414
224694
|
session.status = "stopped";
|
|
224415
224695
|
if (!session.skipDb) await this.storage.agentSessions.updateStatus(sessionId, "stopped");
|
|
@@ -224570,6 +224850,20 @@ ${details}`;
|
|
|
224570
224850
|
await this.persistEntry(session, index, entry);
|
|
224571
224851
|
}
|
|
224572
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
|
+
}
|
|
224573
224867
|
/**
|
|
224574
224868
|
* Send a user message to the agent
|
|
224575
224869
|
*/
|
|
@@ -224615,6 +224909,7 @@ ${details}`;
|
|
|
224615
224909
|
`[AgentSession] sendUserMessage: wrote ${formatted.length}B to ${session.agentType} stdin (session=${sessionId})`
|
|
224616
224910
|
);
|
|
224617
224911
|
session.process.stdin.write(formatted);
|
|
224912
|
+
if (session.turnOpenSince === null) session.turnOpenSince = Date.now();
|
|
224618
224913
|
return true;
|
|
224619
224914
|
} catch (error48) {
|
|
224620
224915
|
console.error(`[AgentSession] Failed to send message:`, error48);
|
|
@@ -224733,6 +225028,7 @@ ${details}`;
|
|
|
224733
225028
|
content: "Session stopped by user.",
|
|
224734
225029
|
timestamp: Date.now()
|
|
224735
225030
|
});
|
|
225031
|
+
await this.endActiveTurn(session, "stopped");
|
|
224736
225032
|
session.dormant = true;
|
|
224737
225033
|
this.resetCompletion(session);
|
|
224738
225034
|
session.status = "stopped";
|
|
@@ -224842,6 +225138,7 @@ ${details}`;
|
|
|
224842
225138
|
session.store.currentAssistantIndex = null;
|
|
224843
225139
|
session.buffer = "";
|
|
224844
225140
|
session.dormant = false;
|
|
225141
|
+
session.turnOpenSince = null;
|
|
224845
225142
|
this.touchSession(session);
|
|
224846
225143
|
const clearPatch = ConversationPatch.clearAll();
|
|
224847
225144
|
this.broadcastPatch(sessionId, clearPatch);
|
|
@@ -225002,6 +225299,8 @@ ${details}`;
|
|
|
225002
225299
|
break;
|
|
225003
225300
|
case "system":
|
|
225004
225301
|
break;
|
|
225302
|
+
case "turn_end":
|
|
225303
|
+
break;
|
|
225005
225304
|
}
|
|
225006
225305
|
}
|
|
225007
225306
|
if (lines.length === 0) return null;
|
|
@@ -225044,6 +225343,7 @@ ${details}`;
|
|
|
225044
225343
|
content: userMessage,
|
|
225045
225344
|
timestamp: Date.now()
|
|
225046
225345
|
}, true, userId);
|
|
225346
|
+
session.turnOpenSince = Date.now();
|
|
225047
225347
|
setTimeout(() => {
|
|
225048
225348
|
const context2 = this.buildFullConversationContext(session.store.entries);
|
|
225049
225349
|
if (context2) {
|
|
@@ -225094,6 +225394,34 @@ ${details}`;
|
|
|
225094
225394
|
indexProvider.setIndex(maxIndex + 1);
|
|
225095
225395
|
return store;
|
|
225096
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
|
+
}
|
|
225097
225425
|
/**
|
|
225098
225426
|
* Restore sessions from database on startup.
|
|
225099
225427
|
* Creates dormant RunningSession objects with process=null for sessions that have entries.
|
|
@@ -225103,8 +225431,11 @@ ${details}`;
|
|
|
225103
225431
|
let restoredCount = 0;
|
|
225104
225432
|
for (const dbSession of allSessions) {
|
|
225105
225433
|
if (this.sessions.has(dbSession.id)) continue;
|
|
225106
|
-
|
|
225434
|
+
let entries = await this.storage.agentSessions.getEntries(dbSession.id);
|
|
225107
225435
|
if (entries.length === 0) continue;
|
|
225436
|
+
if (dbSession.status === "running") {
|
|
225437
|
+
entries = await this.repairInterruptedTurn(dbSession.id, entries);
|
|
225438
|
+
}
|
|
225108
225439
|
const store = this.rebuildStoreFromRows(entries, dbSession.id);
|
|
225109
225440
|
const permissionMode = dbSession.permission_mode === "plan" ? "plan" : "edit";
|
|
225110
225441
|
const runningSession = {
|
|
@@ -225125,7 +225456,8 @@ ${details}`;
|
|
|
225125
225456
|
eventChain: Promise.resolve(),
|
|
225126
225457
|
bgSpawnHintsThisTurn: 0,
|
|
225127
225458
|
taskStartedThisTurn: 0,
|
|
225128
|
-
lastActiveAt: Date.now()
|
|
225459
|
+
lastActiveAt: Date.now(),
|
|
225460
|
+
turnOpenSince: null
|
|
225129
225461
|
};
|
|
225130
225462
|
this.sessions.set(dbSession.id, runningSession);
|
|
225131
225463
|
await this.storage.agentSessions.updateStatusPreservingTimestamp(dbSession.id, "stopped");
|
|
@@ -225143,22 +225475,43 @@ ${details}`;
|
|
|
225143
225475
|
* message goes through wakeDormantSession, which replays the full copied
|
|
225144
225476
|
* context to a fresh process, so a branch also works with a different
|
|
225145
225477
|
* agent type than the source.
|
|
225146
|
-
*
|
|
225147
|
-
*
|
|
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.
|
|
225148
225486
|
*/
|
|
225149
|
-
async branchSession(sourceSessionId, agentTypeOverride) {
|
|
225487
|
+
async branchSession(sourceSessionId, agentTypeOverride, opts = {}) {
|
|
225150
225488
|
const source = this.sessions.get(sourceSessionId);
|
|
225151
225489
|
const sourceRow = await this.storage.agentSessions.getById(sourceSessionId);
|
|
225152
|
-
if (!source && !sourceRow) return
|
|
225153
|
-
if (source?.skipDb) return
|
|
225154
|
-
if (
|
|
225155
|
-
|
|
225156
|
-
|
|
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" };
|
|
225157
225510
|
const projectId = source?.projectId ?? sourceRow.project_id;
|
|
225158
225511
|
const branch = source?.branch ?? (sourceRow.branch || null);
|
|
225159
225512
|
const permissionMode = source?.permissionMode ?? (sourceRow?.permission_mode === "plan" ? "plan" : "edit");
|
|
225160
225513
|
const agentType = agentTypeOverride ?? source?.agentType ?? (sourceRow?.agent_type || "claude-code");
|
|
225161
|
-
const newId = randomUUID();
|
|
225514
|
+
const newId = opts.sessionId ?? randomUUID();
|
|
225162
225515
|
await this.storage.agentSessions.create({
|
|
225163
225516
|
id: newId,
|
|
225164
225517
|
project_id: projectId,
|
|
@@ -225204,12 +225557,14 @@ ${details}`;
|
|
|
225204
225557
|
eventChain: Promise.resolve(),
|
|
225205
225558
|
bgSpawnHintsThisTurn: 0,
|
|
225206
225559
|
taskStartedThisTurn: 0,
|
|
225207
|
-
lastActiveAt: Date.now()
|
|
225560
|
+
lastActiveAt: Date.now(),
|
|
225561
|
+
turnOpenSince: null,
|
|
225562
|
+
crossRemoteMcp: opts.crossRemoteMcp
|
|
225208
225563
|
};
|
|
225209
225564
|
this.sessions.set(newId, branched);
|
|
225210
225565
|
await this.emitDerivedBranchActivity(projectId, branch);
|
|
225211
225566
|
console.log(`[AgentSession] branchSession: ${sourceSessionId} \u2192 ${newId} (entries=${entryRows.length}, agentType=${agentType})`);
|
|
225212
|
-
return newId;
|
|
225567
|
+
return { ok: true, sessionId: newId };
|
|
225213
225568
|
}
|
|
225214
225569
|
/**
|
|
225215
225570
|
* Kill all active session processes and clear state for graceful shutdown
|
|
@@ -225249,6 +225604,7 @@ ${details}`;
|
|
|
225249
225604
|
if (!dbRow || dbRow.title !== null && dbRow.title !== void 0) return;
|
|
225250
225605
|
await this.storage.agentSessions.updateTitle(session.id, finalTitle);
|
|
225251
225606
|
this.broadcastRaw(session.id, { titleUpdated: { title: finalTitle } });
|
|
225607
|
+
this.emitSessionTitle(session.projectId, session.branch, session.id, finalTitle);
|
|
225252
225608
|
} catch (error48) {
|
|
225253
225609
|
console.error(`[AgentSession] Failed to persist generated title for ${session.id}:`, error48);
|
|
225254
225610
|
}
|
|
@@ -225868,6 +226224,12 @@ async function generateAndPushRemoteSessionTitle(deps, localSessionId, userText,
|
|
|
225868
226224
|
localSessionId,
|
|
225869
226225
|
JSON.stringify({ titleUpdated: { title: finalTitle } })
|
|
225870
226226
|
);
|
|
226227
|
+
deps.agentSessionManager.emitSessionTitle(
|
|
226228
|
+
projectIdFromRemoteSessionId(localSessionId, remoteInfo),
|
|
226229
|
+
remoteInfo.branch ?? null,
|
|
226230
|
+
localSessionId,
|
|
226231
|
+
finalTitle
|
|
226232
|
+
);
|
|
225871
226233
|
}
|
|
225872
226234
|
|
|
225873
226235
|
// src/chat-session-manager.ts
|
|
@@ -225888,12 +226250,12 @@ function extractLogText(logs, tailLines) {
|
|
|
225888
226250
|
const lines = joined.split("\n");
|
|
225889
226251
|
return stripAnsi(lines.slice(-tailLines).join("\n"));
|
|
225890
226252
|
}
|
|
225891
|
-
function
|
|
226253
|
+
function parseDbTimestamp2(s3) {
|
|
225892
226254
|
if (s3.includes("T") || s3.endsWith("Z")) return new Date(s3).getTime();
|
|
225893
226255
|
return (/* @__PURE__ */ new Date(s3.replace(" ", "T") + "Z")).getTime();
|
|
225894
226256
|
}
|
|
225895
226257
|
function formatRelativeAge(timestamp, nowMs) {
|
|
225896
|
-
const t =
|
|
226258
|
+
const t = parseDbTimestamp2(timestamp);
|
|
225897
226259
|
if (Number.isNaN(t)) return "unknown";
|
|
225898
226260
|
const sec = Math.max(0, Math.round((nowMs - t) / 1e3));
|
|
225899
226261
|
if (sec < 60) return `${sec}s ago`;
|
|
@@ -227104,8 +227466,8 @@ var ChatSessionManager = class {
|
|
|
227104
227466
|
};
|
|
227105
227467
|
}
|
|
227106
227468
|
const TURN_GRACE_MS = 1e3;
|
|
227107
|
-
const startedThisTurn = turnStartedAt != null &&
|
|
227108
|
-
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;
|
|
227109
227471
|
return {
|
|
227110
227472
|
name: executor.name,
|
|
227111
227473
|
command: executor.command,
|
|
@@ -231843,6 +232205,15 @@ function isAncestor(repoPath, ancestor, descendant) {
|
|
|
231843
232205
|
return false;
|
|
231844
232206
|
}
|
|
231845
232207
|
}
|
|
232208
|
+
function branchContentContainedInTarget(repoPath, branch, target) {
|
|
232209
|
+
try {
|
|
232210
|
+
const mergedTree = git(repoPath, ["merge-tree", "--write-tree", target, branch]).split("\n", 1)[0].trim();
|
|
232211
|
+
const targetTree = revParse(repoPath, `${target}^{tree}`);
|
|
232212
|
+
return targetTree !== null && mergedTree === targetTree;
|
|
232213
|
+
} catch {
|
|
232214
|
+
return false;
|
|
232215
|
+
}
|
|
232216
|
+
}
|
|
231846
232217
|
function computeBranchMergeStatus(repoPath, branch, target) {
|
|
231847
232218
|
const branchTip = revParse(repoPath, `refs/heads/${branch}`);
|
|
231848
232219
|
const targetTip = revParse(repoPath, `refs/heads/${target}`);
|
|
@@ -231868,6 +232239,10 @@ function computeBranchMergeStatus(repoPath, branch, target) {
|
|
|
231868
232239
|
else if (unmergedCount === 0) status = "merged";
|
|
231869
232240
|
else if (unmergedCount < lines.length) status = "partial";
|
|
231870
232241
|
else status = "unmerged";
|
|
232242
|
+
if (unmergedCount > 0 && branchContentContainedInTarget(repoPath, branch, target)) {
|
|
232243
|
+
status = "merged";
|
|
232244
|
+
unmergedCount = 0;
|
|
232245
|
+
}
|
|
231871
232246
|
}
|
|
231872
232247
|
statusCache.set(cacheKey, { branchTip, targetTip, status, unmergedCount });
|
|
231873
232248
|
return { status, unmergedCount };
|
|
@@ -233112,6 +233487,48 @@ var routes11 = async (fastify2) => {
|
|
|
233112
233487
|
if (!project) return null;
|
|
233113
233488
|
return remoteInfo;
|
|
233114
233489
|
}
|
|
233490
|
+
async function performLocalBranch(sourceSessionId, userId, opts) {
|
|
233491
|
+
const sourceRow = await fastify2.storage.agentSessions.getById(sourceSessionId);
|
|
233492
|
+
if (!sourceRow || !await fastify2.storage.projects.getById(sourceRow.project_id, userId)) {
|
|
233493
|
+
return { ok: false, code: 404, error: "Session not found" };
|
|
233494
|
+
}
|
|
233495
|
+
const result = await fastify2.agentSessionManager.branchSession(
|
|
233496
|
+
sourceSessionId,
|
|
233497
|
+
opts.agentType,
|
|
233498
|
+
{ sessionId: opts.sessionId, crossRemoteMcp: opts.crossRemoteMcp, upToEntryIndex: opts.upToEntryIndex }
|
|
233499
|
+
);
|
|
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
|
+
}
|
|
233507
|
+
return { ok: false, code: 404, error: "Session not found or has no history to branch" };
|
|
233508
|
+
}
|
|
233509
|
+
const newSessionId = result.sessionId;
|
|
233510
|
+
const session = fastify2.agentSessionManager.getSession(newSessionId);
|
|
233511
|
+
const messages = fastify2.agentSessionManager.getMessages(newSessionId);
|
|
233512
|
+
const dbRow = await fastify2.storage.agentSessions.getById(newSessionId);
|
|
233513
|
+
return {
|
|
233514
|
+
ok: true,
|
|
233515
|
+
payload: {
|
|
233516
|
+
session: {
|
|
233517
|
+
id: newSessionId,
|
|
233518
|
+
projectId: session?.projectId,
|
|
233519
|
+
branch: session?.branch ?? null,
|
|
233520
|
+
status: session?.status || "stopped",
|
|
233521
|
+
permissionMode: session?.permissionMode || "edit",
|
|
233522
|
+
agentType: session?.agentType || "claude-code",
|
|
233523
|
+
title: dbRow?.title ?? null
|
|
233524
|
+
},
|
|
233525
|
+
messages
|
|
233526
|
+
}
|
|
233527
|
+
};
|
|
233528
|
+
}
|
|
233529
|
+
function broadcastRenamedTitle(sessionId, projectId, branch, title) {
|
|
233530
|
+
fastify2.agentSessionManager.emitSessionTitle(projectId, branch, sessionId, title);
|
|
233531
|
+
}
|
|
233115
233532
|
fastify2.get("/api/agent-providers", async (_req, reply) => {
|
|
233116
233533
|
const providers2 = getAllProviders().map((provider) => ({
|
|
233117
233534
|
type: provider.getAgentType(),
|
|
@@ -233839,82 +234256,115 @@ var routes11 = async (fastify2) => {
|
|
|
233839
234256
|
fastify2.post(
|
|
233840
234257
|
"/api/agent-sessions/:sessionId/branch",
|
|
233841
234258
|
async (req, reply) => {
|
|
233842
|
-
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
|
+
}
|
|
234263
|
+
const userId = requireAuth(req, reply);
|
|
234264
|
+
if (userId === null) return;
|
|
233843
234265
|
if (req.params.sessionId.startsWith("remote-")) {
|
|
233844
|
-
const userId = requireAuth(req, reply);
|
|
233845
|
-
if (userId === null) return;
|
|
233846
234266
|
const remoteInfo = await getAuthorizedRemoteSessionInfo(req.params.sessionId, userId);
|
|
233847
234267
|
if (!remoteInfo) {
|
|
233848
234268
|
return reply.code(404).send({ error: "Remote session not found" });
|
|
233849
234269
|
}
|
|
234270
|
+
const projectId = projectIdFromRemoteSessionId(req.params.sessionId, remoteInfo);
|
|
234271
|
+
const newRemoteSessionId = randomUUID14();
|
|
234272
|
+
const localSessionId = `remote-${remoteInfo.remoteServerId}-${projectId}-${newRemoteSessionId}`;
|
|
234273
|
+
const crossRemoteMcp2 = await mintCrossRemoteMcpConfig(
|
|
234274
|
+
{ storage: fastify2.storage },
|
|
234275
|
+
{ userId, sessionId: localSessionId, sourceRemoteServerId: remoteInfo.remoteServerId }
|
|
234276
|
+
);
|
|
233850
234277
|
const result = await proxyAuto(
|
|
233851
234278
|
remoteInfo.remoteServerId,
|
|
233852
234279
|
remoteInfo.remoteUrl,
|
|
233853
234280
|
remoteInfo.remoteApiKey,
|
|
233854
234281
|
"POST",
|
|
233855
|
-
`/api/agent-sessions/${remoteInfo.remoteSessionId}/branch`,
|
|
233856
|
-
{ agentType }
|
|
234282
|
+
`/api/path/agent-sessions/${remoteInfo.remoteSessionId}/branch`,
|
|
234283
|
+
{ agentType, sessionId: newRemoteSessionId, crossRemoteMcp: crossRemoteMcp2, upToEntryIndex }
|
|
233857
234284
|
);
|
|
233858
234285
|
if (!result.ok) {
|
|
233859
234286
|
return reply.code(proxyStatus(result)).send(result.data);
|
|
233860
234287
|
}
|
|
233861
234288
|
const remoteData = result.data;
|
|
233862
|
-
|
|
233863
|
-
|
|
234289
|
+
if (remoteData.session.id !== newRemoteSessionId) {
|
|
234290
|
+
return reply.code(409).send({ error: "Remote returned an unexpected session id; upgrade the remote" });
|
|
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
|
+
}
|
|
233864
234298
|
fastify2.remoteSessionMap.set(localSessionId, {
|
|
233865
234299
|
remoteServerId: remoteInfo.remoteServerId,
|
|
233866
234300
|
remoteUrl: remoteInfo.remoteUrl,
|
|
233867
234301
|
remoteApiKey: remoteInfo.remoteApiKey,
|
|
233868
|
-
remoteSessionId:
|
|
234302
|
+
remoteSessionId: newRemoteSessionId,
|
|
233869
234303
|
branch: remoteInfo.branch ?? null
|
|
233870
234304
|
});
|
|
233871
|
-
|
|
233872
|
-
|
|
233873
|
-
|
|
233874
|
-
|
|
233875
|
-
|
|
233876
|
-
|
|
233877
|
-
|
|
233878
|
-
|
|
233879
|
-
|
|
233880
|
-
|
|
233881
|
-
|
|
233882
|
-
|
|
233883
|
-
|
|
233884
|
-
|
|
233885
|
-
|
|
234305
|
+
try {
|
|
234306
|
+
await fastify2.storage.remoteSessionMappings.upsert(
|
|
234307
|
+
localSessionId,
|
|
234308
|
+
projectId,
|
|
234309
|
+
remoteInfo.remoteServerId,
|
|
234310
|
+
newRemoteSessionId,
|
|
234311
|
+
remoteInfo.branch ?? null
|
|
234312
|
+
);
|
|
234313
|
+
await fastify2.storage.remoteSessionMappings.markTitleResolved(localSessionId);
|
|
234314
|
+
fastify2.agentSessionManager.markTitleResolved(localSessionId);
|
|
234315
|
+
if (remoteData.messages && remoteData.messages.length > 0) {
|
|
234316
|
+
const cacheEntry = fastify2.remotePatchCache.getOrCreate(localSessionId);
|
|
234317
|
+
if (cacheEntry.messages.length === 0) {
|
|
234318
|
+
for (let i = 0; i < remoteData.messages.length; i++) {
|
|
234319
|
+
const patch = ConversationPatch.addEntry(i, remoteData.messages[i]);
|
|
234320
|
+
fastify2.remotePatchCache.appendMessage(localSessionId, JSON.stringify({ JsonPatch: patch }), true);
|
|
234321
|
+
}
|
|
233886
234322
|
}
|
|
233887
234323
|
}
|
|
234324
|
+
} catch (err) {
|
|
234325
|
+
fastify2.remoteSessionMap.delete(localSessionId);
|
|
234326
|
+
throw err;
|
|
233888
234327
|
}
|
|
233889
234328
|
return reply.code(200).send({
|
|
233890
234329
|
session: { ...remoteData.session, id: localSessionId, projectId },
|
|
233891
234330
|
messages: remoteData.messages
|
|
233892
234331
|
});
|
|
233893
234332
|
}
|
|
233894
|
-
const
|
|
233895
|
-
|
|
233896
|
-
|
|
234333
|
+
const preSessionId = randomUUID14();
|
|
234334
|
+
const crossRemoteMcp = await mintCrossRemoteMcpConfig(
|
|
234335
|
+
{ storage: fastify2.storage },
|
|
234336
|
+
{ userId, sessionId: preSessionId, sourceRemoteServerId: null }
|
|
233897
234337
|
);
|
|
233898
|
-
|
|
233899
|
-
|
|
233900
|
-
|
|
233901
|
-
|
|
233902
|
-
|
|
233903
|
-
const dbRow = await fastify2.storage.agentSessions.getById(newSessionId);
|
|
233904
|
-
return reply.code(200).send({
|
|
233905
|
-
session: {
|
|
233906
|
-
id: newSessionId,
|
|
233907
|
-
projectId: session?.projectId,
|
|
233908
|
-
branch: session?.branch ?? null,
|
|
233909
|
-
status: session?.status || "stopped",
|
|
233910
|
-
permissionMode: session?.permissionMode || "edit",
|
|
233911
|
-
agentType: session?.agentType || "claude-code",
|
|
233912
|
-
title: dbRow?.title ?? null
|
|
233913
|
-
},
|
|
233914
|
-
messages
|
|
234338
|
+
const branched = await performLocalBranch(req.params.sessionId, userId, {
|
|
234339
|
+
agentType,
|
|
234340
|
+
sessionId: preSessionId,
|
|
234341
|
+
crossRemoteMcp,
|
|
234342
|
+
upToEntryIndex
|
|
233915
234343
|
});
|
|
234344
|
+
if (!branched.ok) {
|
|
234345
|
+
return reply.code(branched.code).send({ error: branched.error });
|
|
234346
|
+
}
|
|
234347
|
+
return reply.code(200).send(branched.payload);
|
|
233916
234348
|
}
|
|
233917
234349
|
);
|
|
234350
|
+
fastify2.post("/api/path/agent-sessions/:sessionId/branch", async (req, reply) => {
|
|
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
|
+
}
|
|
234355
|
+
const userId = requireAuth(req, reply);
|
|
234356
|
+
if (userId === null) return;
|
|
234357
|
+
const branched = await performLocalBranch(req.params.sessionId, userId, {
|
|
234358
|
+
agentType,
|
|
234359
|
+
sessionId,
|
|
234360
|
+
crossRemoteMcp,
|
|
234361
|
+
upToEntryIndex
|
|
234362
|
+
});
|
|
234363
|
+
if (!branched.ok) {
|
|
234364
|
+
return reply.code(branched.code).send({ error: branched.error });
|
|
234365
|
+
}
|
|
234366
|
+
return reply.code(200).send(branched.payload);
|
|
234367
|
+
});
|
|
233918
234368
|
fastify2.post(
|
|
233919
234369
|
"/api/agent-sessions/:sessionId/switch-mode",
|
|
233920
234370
|
async (req, reply) => {
|
|
@@ -234073,6 +234523,7 @@ var routes11 = async (fastify2) => {
|
|
|
234073
234523
|
if (title !== null && (typeof title !== "string" || title.length > 200)) {
|
|
234074
234524
|
return reply.code(400).send({ error: "title must be null or a string up to 200 chars" });
|
|
234075
234525
|
}
|
|
234526
|
+
const normalizedTitle = title && title.trim().length > 0 ? title.trim() : null;
|
|
234076
234527
|
if (req.params.sessionId.startsWith("remote-")) {
|
|
234077
234528
|
const userId = requireAuth(req, reply);
|
|
234078
234529
|
if (userId === null) return;
|
|
@@ -234084,14 +234535,35 @@ var routes11 = async (fastify2) => {
|
|
|
234084
234535
|
remoteInfo.remoteApiKey,
|
|
234085
234536
|
"PATCH",
|
|
234086
234537
|
`/api/agent-sessions/${remoteInfo.remoteSessionId}/title`,
|
|
234087
|
-
{ title }
|
|
234538
|
+
{ title: normalizedTitle }
|
|
234088
234539
|
);
|
|
234540
|
+
if (result.ok) {
|
|
234541
|
+
broadcastRenamedTitle(
|
|
234542
|
+
req.params.sessionId,
|
|
234543
|
+
projectIdFromRemoteSessionId(req.params.sessionId, remoteInfo),
|
|
234544
|
+
remoteInfo.branch ?? null,
|
|
234545
|
+
normalizedTitle
|
|
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
|
+
}
|
|
234554
|
+
}
|
|
234089
234555
|
return reply.code(proxyStatus(result)).send(result.data);
|
|
234090
234556
|
}
|
|
234091
234557
|
const session = await fastify2.storage.agentSessions.getById(req.params.sessionId);
|
|
234092
234558
|
if (!session) return reply.code(404).send({ error: "Session not found" });
|
|
234093
|
-
await fastify2.storage.agentSessions.updateTitle(req.params.sessionId,
|
|
234094
|
-
|
|
234559
|
+
await fastify2.storage.agentSessions.updateTitle(req.params.sessionId, normalizedTitle);
|
|
234560
|
+
broadcastRenamedTitle(
|
|
234561
|
+
req.params.sessionId,
|
|
234562
|
+
session.project_id,
|
|
234563
|
+
session.branch,
|
|
234564
|
+
normalizedTitle
|
|
234565
|
+
);
|
|
234566
|
+
return reply.code(200).send({ success: true, title: normalizedTitle });
|
|
234095
234567
|
});
|
|
234096
234568
|
fastify2.patch("/api/agent-sessions/:sessionId/favorite", async (req, reply) => {
|
|
234097
234569
|
const { favorited } = req.body;
|
|
@@ -234208,6 +234680,77 @@ var branch_activity_routes_default = (0, import_fastify_plugin13.default)(routes
|
|
|
234208
234680
|
|
|
234209
234681
|
// src/routes/merge-status-routes.ts
|
|
234210
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
|
+
}
|
|
234211
234754
|
function legacyRemoteLabel(remoteUrl) {
|
|
234212
234755
|
try {
|
|
234213
234756
|
return new URL(remoteUrl).hostname;
|
|
@@ -234239,6 +234782,10 @@ async function getRemoteConfig4(fastify2, project) {
|
|
|
234239
234782
|
return null;
|
|
234240
234783
|
}
|
|
234241
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
|
+
}
|
|
234242
234789
|
function parseComparisons(body) {
|
|
234243
234790
|
if (!body || typeof body !== "object") return null;
|
|
234244
234791
|
const comparisons = body.comparisons;
|
|
@@ -234294,6 +234841,11 @@ var routes13 = async (fastify2) => {
|
|
|
234294
234841
|
if (!comparisons) {
|
|
234295
234842
|
return reply.code(400).send({ error: "Invalid comparisons" });
|
|
234296
234843
|
}
|
|
234844
|
+
const storedTargets = await fastify2.storage.mergeTargets.getForBranches(
|
|
234845
|
+
project.id,
|
|
234846
|
+
comparisons.map(({ branch }) => branch)
|
|
234847
|
+
);
|
|
234848
|
+
const resolved = resolveTargets(comparisons, storedTargets);
|
|
234297
234849
|
if (!project.path) {
|
|
234298
234850
|
const remoteConfig = await getRemoteConfig4(fastify2, project);
|
|
234299
234851
|
if (!remoteConfig) {
|
|
@@ -234305,28 +234857,83 @@ var routes13 = async (fastify2) => {
|
|
|
234305
234857
|
remoteConfig.apiKey,
|
|
234306
234858
|
"POST",
|
|
234307
234859
|
"/api/path/branches/merge-status",
|
|
234308
|
-
{ path: remoteConfig.remotePath, comparisons },
|
|
234860
|
+
{ path: remoteConfig.remotePath, comparisons: resolved.comparisons },
|
|
234309
234861
|
{ reverseConnectManager: fastify2.reverseConnectManager }
|
|
234310
234862
|
);
|
|
234311
234863
|
if (!result.ok) {
|
|
234312
234864
|
return reply.code(proxyStatus(result)).send(result.data);
|
|
234313
234865
|
}
|
|
234314
|
-
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
|
+
}
|
|
234315
234870
|
return reply.code(200).send({
|
|
234316
234871
|
repository: {
|
|
234317
234872
|
kind: "remote",
|
|
234318
234873
|
remoteServerId: remoteConfig.serverId,
|
|
234319
234874
|
label: remoteConfig.serverName
|
|
234320
234875
|
},
|
|
234321
|
-
entries:
|
|
234876
|
+
entries: annotateEntries(
|
|
234877
|
+
entries,
|
|
234878
|
+
resolved.comparisons,
|
|
234879
|
+
resolved.sources
|
|
234880
|
+
)
|
|
234322
234881
|
});
|
|
234323
234882
|
}
|
|
234324
|
-
|
|
234325
|
-
|
|
234326
|
-
|
|
234327
|
-
|
|
234328
|
-
|
|
234329
|
-
|
|
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 });
|
|
234330
234937
|
}
|
|
234331
234938
|
);
|
|
234332
234939
|
};
|
|
@@ -234772,7 +235379,7 @@ var SCROLLBACK_MAX = 1e5;
|
|
|
234772
235379
|
var FONT_SIZE_MIN = 8;
|
|
234773
235380
|
var FONT_SIZE_MAX = 32;
|
|
234774
235381
|
var DEFAULT_CONVERSATION_SETTINGS = {
|
|
234775
|
-
agentFontSize:
|
|
235382
|
+
agentFontSize: 16,
|
|
234776
235383
|
chatFontSize: 15,
|
|
234777
235384
|
filesTreeFontSize: 14,
|
|
234778
235385
|
filesContentFontSize: 14
|
|
@@ -237538,6 +238145,244 @@ var routes28 = async (fastify2) => {
|
|
|
237538
238145
|
};
|
|
237539
238146
|
var schedule_routes_default = (0, import_fastify_plugin29.default)(routes28, { name: "schedule-routes" });
|
|
237540
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
|
+
|
|
237541
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
|
|
237542
238387
|
import { Readable as Readable3 } from "stream";
|
|
237543
238388
|
var fastifyRequestToRequest = (req) => {
|
|
@@ -238795,6 +239640,7 @@ var createServer = async (opts) => {
|
|
|
238795
239640
|
server.register(remote_routes_default);
|
|
238796
239641
|
server.register(remote_server_routes_default);
|
|
238797
239642
|
server.register(project_remote_routes_default);
|
|
239643
|
+
server.register(search_routes_default);
|
|
238798
239644
|
server.register(executor_group_routes_default);
|
|
238799
239645
|
server.register(executor_routes_default);
|
|
238800
239646
|
server.register(schedule_routes_default);
|