@turmind/halo 1.5.2-alpha → 1.5.3-alpha
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/{803-7128009ee0e1c10e.js → 133-33aa091fba853037.js} +1 -1
- package/admin-out/_next/static/chunks/989-c6c7772db8a50926.js +1 -0
- package/admin-out/_next/static/chunks/app/page-5f41b086cd5ad353.js +1 -0
- package/admin-out/_next/static/css/8809d33379458514.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +3 -3
- package/bundled-docs/design/architecture.md +3 -3
- package/bundled-docs/design/background-dispatch.md +52 -45
- package/bundled-docs/design/express-self.md +1 -1
- package/bundled-docs/design/prompt-system.md +2 -2
- package/bundled-docs/design/session.md +22 -14
- package/bundled-docs/design/storage.md +3 -3
- package/bundled-docs/design/ws.md +30 -25
- package/bundled-docs/dev/add-model-provider.md +2 -2
- package/bundled-docs/dev/api.md +37 -18
- package/bundled-docs/dev/env.md +2 -2
- package/bundled-docs/guide/agents.md +2 -2
- package/bundled-docs/guide/chat.md +16 -9
- package/bundled-docs/guide/cli.md +11 -1
- package/bundled-docs/guide/sessions.md +7 -7
- package/bundled-docs/guide/testing-agents-and-skills.md +5 -6
- package/bundled-docs/requirements/chat.md +23 -9
- package/bundled-docs/requirements/command.md +1 -1
- package/bundled-docs/requirements/session.md +10 -7
- package/bundled-docs/requirements/terminal.md +2 -0
- package/dist/index.js +222 -211
- package/package.json +1 -1
- package/templates/agents/__apply_agent__/agent.yaml +2 -2
- package/templates/agents/__evo_agent__/agent.yaml +2 -2
- package/templates/agents/__score__/agent.yaml +2 -2
- package/templates/agents/deep-executor/agent.yaml +2 -2
- package/templates/agents/default/agent.yaml +2 -2
- package/templates/agents/executor/agent.yaml +2 -2
- package/templates/agents/goal/agent.yaml +2 -2
- package/templates/models/anthropic.yaml +1 -1
- package/templates/models/aws-bedrock-claude-invoke.yaml +10 -10
- package/templates/models/aws-bedrock-mantle.yaml +4 -4
- package/templates/models/aws-bedrock-openai.yaml +6 -6
- package/templates/models/deepseek.yaml +2 -2
- package/templates/models/kimi.yaml +1 -1
- package/templates/models/mimo-token-plan-china.yaml +4 -4
- package/templates/models/minimax.yaml +1 -1
- package/templates/models/qwen.yaml +2 -2
- package/templates/models/zhipu.yaml +4 -4
- package/templates/prompts/root/DELEGATION.md +17 -7
- package/templates/skills/agent/SKILL.md +2 -2
- package/admin-out/_next/static/chunks/989-7420ee650083df43.js +0 -1
- package/admin-out/_next/static/chunks/app/page-fa5e935dbaa74165.js +0 -1
- package/admin-out/_next/static/css/b8ffb5202b5a6619.css +0 -3
- /package/admin-out/_next/static/{DLRpRENVwZ-EHTRGc9AzB → Wqi_ugna9MYsiV7ECuawQ}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{DLRpRENVwZ-EHTRGc9AzB → Wqi_ugna9MYsiV7ECuawQ}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -508,7 +508,7 @@ function generalSection() {
|
|
|
508
508
|
{ key: "session.max_queue_size", type: "int", globalOnly: true, description: "Maximum queued messages per session", description_zh: "\u6BCF\u4E2A\u4F1A\u8BDD\u6700\u5927\u6392\u961F\u6D88\u606F\u6570", default: "256" },
|
|
509
509
|
{ key: "session.max_nesting_depth", type: "int", globalOnly: true, description: "Maximum session nesting depth for agent delegation", description_zh: "Agent \u59D4\u6D3E\u7684\u6700\u5927\u4F1A\u8BDD\u5D4C\u5957\u6DF1\u5EA6", default: "16" },
|
|
510
510
|
// compact
|
|
511
|
-
{ key: "compact.compress_at", type: "float", globalOnly: true, description: "Auto-compact threshold as a fraction of max context (e.g. 0.
|
|
511
|
+
{ key: "compact.compress_at", type: "float", globalOnly: true, description: "Auto-compact threshold as a fraction of max context (e.g. 0.9 = compact when 90% full)", description_zh: "\u81EA\u52A8\u538B\u7F29\u9608\u503C\uFF0C\u6700\u5927\u4E0A\u4E0B\u6587\u7684\u6BD4\u4F8B\uFF08\u5982 0.9 \u8868\u793A\u7528\u6EE1 90% \u65F6\u538B\u7F29\uFF09", default: "0.9" },
|
|
512
512
|
{ key: "compact.keep_messages", type: "int", globalOnly: true, description: "Recent messages kept intact during compaction", description_zh: "\u538B\u7F29\u65F6\u4FDD\u7559\u6700\u540E\u591A\u5C11\u6761\u6D88\u606F\u4E0D\u52A8", default: "5" },
|
|
513
513
|
{ key: "compact.max_summary_input", type: "int", globalOnly: true, description: "Max chars fed into local truncation fallback", description_zh: "\u672C\u5730\u622A\u65AD\u515C\u5E95\u65F6\u7684\u603B\u8F93\u5165\u5B57\u7B26\u4E0A\u9650", default: "15000" },
|
|
514
514
|
{ key: "compact.max_message_slice", type: "int", globalOnly: true, description: "Max chars kept per old message during local truncation", description_zh: "\u672C\u5730\u622A\u65AD\u515C\u5E95\u65F6\u6BCF\u6761\u65E7\u6D88\u606F\u4FDD\u7559\u7684\u6700\u5927\u5B57\u7B26\u6570", default: "800" },
|
|
@@ -1102,7 +1102,7 @@ var init_config = __esm({
|
|
|
1102
1102
|
// value is a fraction of `maxContextTokens`; auto-compact triggers when
|
|
1103
1103
|
// the running context exceeds `maxContextTokens * compressAt`.
|
|
1104
1104
|
model: {
|
|
1105
|
-
maxContextTokens: envInt("HALO_MAX_CONTEXT_TOKENS",
|
|
1105
|
+
maxContextTokens: envInt("HALO_MAX_CONTEXT_TOKENS", 272e3),
|
|
1106
1106
|
get compressAt() {
|
|
1107
1107
|
const raw = settingsValue("general.compact.compress_at");
|
|
1108
1108
|
if (raw !== void 0) {
|
|
@@ -1110,7 +1110,7 @@ var init_config = __esm({
|
|
|
1110
1110
|
if (Number.isFinite(n) && n > 0 && n <= 1)
|
|
1111
1111
|
return n;
|
|
1112
1112
|
}
|
|
1113
|
-
return 0.
|
|
1113
|
+
return 0.9;
|
|
1114
1114
|
}
|
|
1115
1115
|
},
|
|
1116
1116
|
/** System language preference. `general.language` in settings.yaml stores
|
|
@@ -2621,7 +2621,7 @@ var init_init = __esm({
|
|
|
2621
2621
|
path8.resolve(__dirname, "..", "bundled-docs"),
|
|
2622
2622
|
path8.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
2623
2623
|
];
|
|
2624
|
-
TEMPLATE_VERSION =
|
|
2624
|
+
TEMPLATE_VERSION = 77;
|
|
2625
2625
|
VERSION_FILE = ".template-version";
|
|
2626
2626
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
2627
2627
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -7265,9 +7265,10 @@ var init_session_ui_store = __esm({
|
|
|
7265
7265
|
* taskId: `state.subSessionLogs[id]`, which holds the sub's full history —
|
|
7266
7266
|
* see loadSubHistory — plus the in-flight turn as a temp assistant, the
|
|
7267
7267
|
* same snapshot the root writes). `archive` is passed ONLY by
|
|
7268
|
-
* archiveOldMessages (the commit step of an archive write
|
|
7269
|
-
* new segment count and `userDelta` the main user
|
|
7270
|
-
* file. Every other call leaves both on-disk
|
|
7268
|
+
* archiveOldMessages (the commit step of an archive write, root or sub
|
|
7269
|
+
* alike): `count` is the new segment count and `userDelta` the main user
|
|
7270
|
+
* turns leaving the active file. Every other call leaves both on-disk
|
|
7271
|
+
* values alone. */
|
|
7271
7272
|
persistLog(id, state, archive) {
|
|
7272
7273
|
const rootId = this.findRootSessionId(id);
|
|
7273
7274
|
const sub = id === rootId ? void 0 : state.subSessionLogs.get(id);
|
|
@@ -7290,7 +7291,9 @@ var init_session_ui_store = __esm({
|
|
|
7290
7291
|
agentName: sub.agentName,
|
|
7291
7292
|
source: "delegated",
|
|
7292
7293
|
description: sub.description,
|
|
7293
|
-
parentSessionId: parts.slice(0, -1).join(">")
|
|
7294
|
+
parentSessionId: parts.slice(0, -1).join(">"),
|
|
7295
|
+
archiveCount: archive?.count,
|
|
7296
|
+
archivedUserDelta: archive?.userDelta
|
|
7294
7297
|
});
|
|
7295
7298
|
return;
|
|
7296
7299
|
}
|
|
@@ -7316,10 +7319,14 @@ var init_session_ui_store = __esm({
|
|
|
7316
7319
|
}
|
|
7317
7320
|
}
|
|
7318
7321
|
/**
|
|
7319
|
-
* Move a
|
|
7320
|
-
*
|
|
7322
|
+
* Move a session's UI log out into a gzipped archive segment, keeping only
|
|
7323
|
+
* the newest main exchange in the active file. Called from the compact
|
|
7321
7324
|
* path — the only place that already accepts "history shrinks here".
|
|
7322
7325
|
*
|
|
7326
|
+
* Root and sub-session alike: a sub's log lives in its root's UIState
|
|
7327
|
+
* (`subSessionLogs`, keyed by the full `root>…` id) but is written to the
|
|
7328
|
+
* sub's own file, so archiving it never touches the parent's log or file.
|
|
7329
|
+
*
|
|
7323
7330
|
* Gated on the active file's SIZE (`ARCHIVE_SIZE_THRESHOLD`), not its exchange
|
|
7324
7331
|
* count: bytes are what make a log slow to load, and per-exchange size varies
|
|
7325
7332
|
* by orders of magnitude. Under the threshold this returns 0 after a single
|
|
@@ -7340,35 +7347,36 @@ var init_session_ui_store = __esm({
|
|
|
7340
7347
|
*/
|
|
7341
7348
|
archiveOldMessages(sessionId) {
|
|
7342
7349
|
const rootId = this.findRootSessionId(sessionId);
|
|
7343
|
-
|
|
7350
|
+
const isRoot = sessionId === rootId;
|
|
7351
|
+
if (this.host.isSessionDeleted(rootId) || this.host.isSessionDeleted(sessionId))
|
|
7352
|
+
return 0;
|
|
7353
|
+
const state = isRoot ? this.ensureUIState(rootId) : this.uiStates.get(rootId);
|
|
7354
|
+
const log2 = isRoot ? state : state?.subSessionLogs.get(sessionId);
|
|
7355
|
+
if (!state || !log2)
|
|
7344
7356
|
return 0;
|
|
7345
|
-
const state = this.ensureUIState(rootId);
|
|
7346
7357
|
try {
|
|
7347
|
-
const
|
|
7348
|
-
const
|
|
7349
|
-
const agentId = inMem?.agentId ?? row?.agentId ?? "default";
|
|
7350
|
-
const dir = getSessionDir(agentId, this.uiStateProjectPaths.get(rootId) ?? this.host.workspaceRoot);
|
|
7351
|
-
const seg = fileSegment(rootId);
|
|
7358
|
+
const dir = getSessionDir(this.resolveAgentId(sessionId), this.uiStateProjectPaths.get(rootId) ?? this.host.workspaceRoot);
|
|
7359
|
+
const seg = fileSegment(sessionId);
|
|
7352
7360
|
if (activeFileSize(dir, seg) <= ARCHIVE_SIZE_THRESHOLD)
|
|
7353
7361
|
return 0;
|
|
7354
|
-
const cut = archiveSplitIndex(
|
|
7362
|
+
const cut = archiveSplitIndex(log2.messageLog, 1);
|
|
7355
7363
|
if (cut === 0)
|
|
7356
7364
|
return 0;
|
|
7357
|
-
const older =
|
|
7358
|
-
const kept =
|
|
7365
|
+
const older = log2.messageLog.slice(0, cut);
|
|
7366
|
+
const kept = log2.messageLog.slice(cut);
|
|
7359
7367
|
const n = readArchiveCount(dir, seg) + 1;
|
|
7360
7368
|
writeArchiveSegment(dir, seg, n, older);
|
|
7361
|
-
|
|
7362
|
-
this.persistLog(
|
|
7369
|
+
log2.messageLog = kept;
|
|
7370
|
+
this.persistLog(sessionId, state, { count: n, userDelta: countMainUserMessages(older) });
|
|
7363
7371
|
if (readArchiveCount(dir, seg) < n) {
|
|
7364
|
-
|
|
7365
|
-
console.warn(`[SessionUIStore] archive commit for ${
|
|
7372
|
+
log2.messageLog = [...older, ...kept];
|
|
7373
|
+
console.warn(`[SessionUIStore] archive commit for ${sessionId} segment ${n} did not land \u2014 rolled back`);
|
|
7366
7374
|
return 0;
|
|
7367
7375
|
}
|
|
7368
|
-
console.debug(`[SessionUIStore] archived ${older.length} UI messages of ${
|
|
7376
|
+
console.debug(`[SessionUIStore] archived ${older.length} UI messages of ${sessionId} into segment ${n}`);
|
|
7369
7377
|
return older.length;
|
|
7370
7378
|
} catch (err) {
|
|
7371
|
-
console.error(`[SessionUIStore] archiveOldMessages failed for ${
|
|
7379
|
+
console.error(`[SessionUIStore] archiveOldMessages failed for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7372
7380
|
return 0;
|
|
7373
7381
|
}
|
|
7374
7382
|
}
|
|
@@ -12061,7 +12069,7 @@ ${roster}`;
|
|
|
12061
12069
|
buildModelRuntime(args) {
|
|
12062
12070
|
const { yamlConfig, modelId, endpoint, providerId, sessionId, systemPrompt, tools } = args;
|
|
12063
12071
|
const contextConfig = {
|
|
12064
|
-
// Priority: explicit agent.yaml config > model registry contextWindow > global default (
|
|
12072
|
+
// Priority: explicit agent.yaml config > model registry contextWindow > global default (272K).
|
|
12065
12073
|
maxTokens: yamlConfig?.context?.maxTokens ?? resolveContextWindow(modelId) ?? config.model.maxContextTokens,
|
|
12066
12074
|
compressAt: yamlConfig?.context?.compressAt ?? config.model.compressAt
|
|
12067
12075
|
};
|
|
@@ -12273,7 +12281,7 @@ var init_session_state_store = __esm({
|
|
|
12273
12281
|
existing.agentName = session.agentName;
|
|
12274
12282
|
if (session.parentId)
|
|
12275
12283
|
existing.parentSessionId = session.parentId;
|
|
12276
|
-
if (!existing.title)
|
|
12284
|
+
if (!existing.title && session.parentId)
|
|
12277
12285
|
existing.title = session.description?.slice(0, 60) || `${session.agentId} session`;
|
|
12278
12286
|
if (!existing.createdAt)
|
|
12279
12287
|
existing.createdAt = now;
|
|
@@ -12369,7 +12377,7 @@ async function initObservability() {
|
|
|
12369
12377
|
return true;
|
|
12370
12378
|
}
|
|
12371
12379
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
12372
|
-
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.5.
|
|
12380
|
+
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.5.3-alpha");
|
|
12373
12381
|
console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, capture_content=${config.observability.captureContent})`);
|
|
12374
12382
|
return true;
|
|
12375
12383
|
}
|
|
@@ -14561,8 +14569,7 @@ ${summaryText}` }]
|
|
|
14561
14569
|
const finalTail = tailMicro.compacted ? tailMicro.messages : cleanRecent;
|
|
14562
14570
|
session.agent.messages = [summaryMsg, ...finalTail];
|
|
14563
14571
|
rebuilt = true;
|
|
14564
|
-
|
|
14565
|
-
this.uiStore.archiveOldMessages(session.id);
|
|
14572
|
+
this.uiStore.archiveOldMessages(session.id);
|
|
14566
14573
|
const estimatedTokens = estimateMessageTokens(session.agent.messages) + estimateMessageTokens([{ role: "user", content: session.systemPrompt }]);
|
|
14567
14574
|
return { summary: summaryText, olderCount, estimatedTokens };
|
|
14568
14575
|
} finally {
|
|
@@ -15222,6 +15229,11 @@ function listSessions(workspace) {
|
|
|
15222
15229
|
const { sessions } = sm.listSessions({ limit: 200 });
|
|
15223
15230
|
return sessions.map((s) => ({ id: s.id, description: s.description, createdAt: s.createdAt }));
|
|
15224
15231
|
}
|
|
15232
|
+
async function agentSupportsImage(sm, workspace, agentId) {
|
|
15233
|
+
const agents = await scanAvailableAgents(workspace, getDisabledSet(sm.getDb(), "agent"));
|
|
15234
|
+
const entry = agents.find((a) => a.id === agentId);
|
|
15235
|
+
return entry ? modelSupportsImage(entry.model) : false;
|
|
15236
|
+
}
|
|
15225
15237
|
async function createHarness(opts) {
|
|
15226
15238
|
const initialPath = path32.resolve(opts.workspace);
|
|
15227
15239
|
if (!fs26.existsSync(initialPath)) {
|
|
@@ -15257,10 +15269,7 @@ async function createHarness(opts) {
|
|
|
15257
15269
|
}
|
|
15258
15270
|
activeOverrides2.set(USER_ID, sessionId);
|
|
15259
15271
|
const sessionAgentId = sm.getSessionById(sessionId)?.agentId ?? agentId;
|
|
15260
|
-
const
|
|
15261
|
-
const agents = await scanAvailableAgents(workspace, disabledSet);
|
|
15262
|
-
const agentEntry = agents.find((a) => a.id === agentId);
|
|
15263
|
-
const supportsImage = agentEntry ? modelSupportsImage(agentEntry.model) : false;
|
|
15272
|
+
const supportsImage = await agentSupportsImage(sm, workspace, sessionAgentId);
|
|
15264
15273
|
const unsubPersistent = sm.registerEventListener(sessionId, (event) => {
|
|
15265
15274
|
for (const h of eventHandlers) h(event);
|
|
15266
15275
|
});
|
|
@@ -15328,10 +15337,12 @@ async function createHarness(opts) {
|
|
|
15328
15337
|
if (result?.switchTo) {
|
|
15329
15338
|
activeOverrides2.set(USER_ID, result.switchTo);
|
|
15330
15339
|
state.unsubPersistent();
|
|
15340
|
+
const switchedAgentId = state.sm.getSessionById(result.switchTo)?.agentId ?? agentId;
|
|
15331
15341
|
state = {
|
|
15332
15342
|
...state,
|
|
15333
15343
|
sessionId: result.switchTo,
|
|
15334
|
-
agentId:
|
|
15344
|
+
agentId: switchedAgentId,
|
|
15345
|
+
supportsImage: await agentSupportsImage(state.sm, state.workspace, switchedAgentId),
|
|
15335
15346
|
unsubPersistent: state.sm.registerEventListener(result.switchTo, (event) => {
|
|
15336
15347
|
for (const h of eventHandlers) h(event);
|
|
15337
15348
|
})
|
|
@@ -18028,7 +18039,7 @@ var init_app = __esm({
|
|
|
18028
18039
|
init_input_box();
|
|
18029
18040
|
init_log_navigator();
|
|
18030
18041
|
init_log_viewer();
|
|
18031
|
-
MAX_CONTEXT_TOKENS_FALLBACK =
|
|
18042
|
+
MAX_CONTEXT_TOKENS_FALLBACK = 272e3;
|
|
18032
18043
|
blockSeq = 0;
|
|
18033
18044
|
ANSI2 = {
|
|
18034
18045
|
reset: "\x1B[0m",
|
|
@@ -18099,6 +18110,7 @@ async function runCli(harness, message, opts) {
|
|
|
18099
18110
|
let turnAll = "";
|
|
18100
18111
|
const toolCalls = [];
|
|
18101
18112
|
let errorText = "";
|
|
18113
|
+
const notices = [];
|
|
18102
18114
|
let usage = null;
|
|
18103
18115
|
let lastToolName = "";
|
|
18104
18116
|
let hadMeta = false;
|
|
@@ -18219,6 +18231,16 @@ ${tag}${formatUsageLine(event)}
|
|
|
18219
18231
|
`);
|
|
18220
18232
|
hadMeta = true;
|
|
18221
18233
|
break;
|
|
18234
|
+
case "system":
|
|
18235
|
+
if (!event.text) break;
|
|
18236
|
+
if (opts.verbose) {
|
|
18237
|
+
stopSpinner();
|
|
18238
|
+
process.stderr.write(`${tag}\x1B[2m[system] ${event.text}\x1B[0m
|
|
18239
|
+
`);
|
|
18240
|
+
hadMeta = true;
|
|
18241
|
+
}
|
|
18242
|
+
if (!event.taskId && !event.agentName) notices.push(event.text);
|
|
18243
|
+
break;
|
|
18222
18244
|
case "complete":
|
|
18223
18245
|
stopSpinner();
|
|
18224
18246
|
break;
|
|
@@ -18227,6 +18249,9 @@ ${tag}${formatUsageLine(event)}
|
|
|
18227
18249
|
stopSpinner();
|
|
18228
18250
|
const answer = turnFinal || turnAll;
|
|
18229
18251
|
if (opts.format === "text") {
|
|
18252
|
+
if (notices.length > 0) {
|
|
18253
|
+
process.stdout.write(notices.map((n) => `\u2139\uFE0F ${n}`).join("\n") + (answer ? "\n\n" : "\n"));
|
|
18254
|
+
}
|
|
18230
18255
|
if (answer) {
|
|
18231
18256
|
const out = process.stdout.isTTY ? renderMarkdown(answer) : answer;
|
|
18232
18257
|
process.stdout.write(out.endsWith("\n") ? out : out + "\n");
|
|
@@ -18234,6 +18259,7 @@ ${tag}${formatUsageLine(event)}
|
|
|
18234
18259
|
} else {
|
|
18235
18260
|
const result = {
|
|
18236
18261
|
text: answer,
|
|
18262
|
+
notices,
|
|
18237
18263
|
sessionId: harness.sessionId,
|
|
18238
18264
|
toolCalls,
|
|
18239
18265
|
usage: usage ? {
|
|
@@ -21608,7 +21634,11 @@ function createSessionRoutes(smRegistry) {
|
|
|
21608
21634
|
totalOutputTokens,
|
|
21609
21635
|
// Goal-mode: non-null while this session is the bound worker of an
|
|
21610
21636
|
// active goal — session lists render a 🎯 badge off this.
|
|
21611
|
-
goalSessionId: s.goalSessionId
|
|
21637
|
+
goalSessionId: s.goalSessionId,
|
|
21638
|
+
// Running flag for an Explorer chat tab that hasn't loaded yet (a
|
|
21639
|
+
// loaded tab tracks its own stream). A root with a running sub-session
|
|
21640
|
+
// counts as running.
|
|
21641
|
+
status: s.status
|
|
21612
21642
|
};
|
|
21613
21643
|
};
|
|
21614
21644
|
const enriched = [...rows.map(enrich), ...descendants.map(enrich)];
|
|
@@ -23953,11 +23983,11 @@ function sendWsNotification(event, state, turnId, ctx) {
|
|
|
23953
23983
|
break;
|
|
23954
23984
|
case "system":
|
|
23955
23985
|
if (!taskId && /^Compacting context \(\d+K tokens\)…$/.test(event.text ?? "")) {
|
|
23956
|
-
sendJson(ctx.ws, { type: "compact:started" });
|
|
23986
|
+
sendJson(ctx.ws, { type: "compact:started", sessionId });
|
|
23957
23987
|
}
|
|
23958
23988
|
sendJson(ctx.ws, { type: "chat:system", text: event.text ?? "", taskId, agentName, sessionId });
|
|
23959
23989
|
if (event.compactEnd && !taskId) {
|
|
23960
|
-
sendJson(ctx.ws, { type: "compact:done" });
|
|
23990
|
+
sendJson(ctx.ws, { type: "compact:done", sessionId });
|
|
23961
23991
|
}
|
|
23962
23992
|
break;
|
|
23963
23993
|
case "error":
|
|
@@ -23970,28 +24000,28 @@ function sendWsNotification(event, state, turnId, ctx) {
|
|
|
23970
24000
|
break;
|
|
23971
24001
|
case "compacted":
|
|
23972
24002
|
if (!taskId) {
|
|
23973
|
-
sendJson(ctx.ws, { type: "compact:done" });
|
|
23974
|
-
sendJson(ctx.ws, { type: "session:compacted", contextTokens: event.totalTokens ?? state.contextTokens });
|
|
24003
|
+
sendJson(ctx.ws, { type: "compact:done", sessionId });
|
|
24004
|
+
sendJson(ctx.ws, { type: "session:compacted", contextTokens: event.totalTokens ?? state.contextTokens, sessionId });
|
|
23975
24005
|
}
|
|
23976
24006
|
break;
|
|
23977
24007
|
}
|
|
23978
24008
|
}
|
|
23979
|
-
function bufferDetachedNotification(event, pendingEvents) {
|
|
24009
|
+
function bufferDetachedNotification(event, pendingEvents, sessionId) {
|
|
23980
24010
|
const agentName = event.agentName ?? "default";
|
|
23981
24011
|
const taskId = event.taskId;
|
|
23982
24012
|
switch (event.type) {
|
|
23983
24013
|
case "agent_start":
|
|
23984
|
-
pendingEvents.push({ type: "agent:start", agentName, task: event.text, taskId });
|
|
24014
|
+
pendingEvents.push({ type: "agent:start", agentName, task: event.text, taskId, sessionId });
|
|
23985
24015
|
break;
|
|
23986
24016
|
case "agent_done":
|
|
23987
|
-
pendingEvents.push({ type: "agent:done", agentName, taskId });
|
|
24017
|
+
pendingEvents.push({ type: "agent:done", agentName, taskId, sessionId });
|
|
23988
24018
|
break;
|
|
23989
24019
|
case "error":
|
|
23990
|
-
pendingEvents.push({ type: "error", error: event.error, agentName, taskId });
|
|
24020
|
+
pendingEvents.push({ type: "error", error: event.error, agentName, taskId, sessionId });
|
|
23991
24021
|
break;
|
|
23992
24022
|
case "followup_start":
|
|
23993
24023
|
case "queued_message":
|
|
23994
|
-
pendingEvents.push({ type: "chat:followup", agentName });
|
|
24024
|
+
pendingEvents.push({ type: "chat:followup", agentName, sessionId });
|
|
23995
24025
|
break;
|
|
23996
24026
|
case "complete":
|
|
23997
24027
|
pendingEvents.length = 0;
|
|
@@ -24276,10 +24306,10 @@ function setupWebSocketHandler(deps) {
|
|
|
24276
24306
|
function getSessionManager(workspacePath) {
|
|
24277
24307
|
return registry2.getOrCreate(workspacePath);
|
|
24278
24308
|
}
|
|
24279
|
-
function getState(client) {
|
|
24280
|
-
if (!client.sessionManager
|
|
24309
|
+
function getState(client, sessionId) {
|
|
24310
|
+
if (!client.sessionManager)
|
|
24281
24311
|
return null;
|
|
24282
|
-
return client.sessionManager.getCachedUIState(
|
|
24312
|
+
return client.sessionManager.getCachedUIState(sessionId);
|
|
24283
24313
|
}
|
|
24284
24314
|
function archiveCountFor(client, sessionId, agentId) {
|
|
24285
24315
|
if (!agentId || !client.projectId)
|
|
@@ -24289,21 +24319,21 @@ function setupWebSocketHandler(deps) {
|
|
|
24289
24319
|
return 0;
|
|
24290
24320
|
return readArchiveCount(getSessionDir(agentId, projectPath), fileSegment(sessionId));
|
|
24291
24321
|
}
|
|
24292
|
-
function saveSession(client) {
|
|
24293
|
-
if (!client.
|
|
24322
|
+
function saveSession(client, sessionId) {
|
|
24323
|
+
if (!client.projectId || !client.sessionManager)
|
|
24294
24324
|
return;
|
|
24295
|
-
const state = getState(client);
|
|
24325
|
+
const state = getState(client, sessionId);
|
|
24296
24326
|
if (!state)
|
|
24297
24327
|
return;
|
|
24298
|
-
if (!client.sessionManager.isUIStateDirty(
|
|
24328
|
+
if (!client.sessionManager.isUIStateDirty(sessionId))
|
|
24299
24329
|
return;
|
|
24300
24330
|
const projectPath = resolveProjectPath2(client.projectId);
|
|
24301
24331
|
const snapshot2 = createSaveSnapshot(state);
|
|
24302
24332
|
if (snapshot2.length === 0)
|
|
24303
24333
|
return;
|
|
24304
|
-
const sessionInfo = client.sessionManager.getSessionById(
|
|
24334
|
+
const sessionInfo = client.sessionManager.getSessionById(sessionId);
|
|
24305
24335
|
client.sessionManager.persistSessionFile({
|
|
24306
|
-
sessionId
|
|
24336
|
+
sessionId,
|
|
24307
24337
|
projectPath,
|
|
24308
24338
|
messages: snapshot2,
|
|
24309
24339
|
contextTokens: state.contextTokens,
|
|
@@ -24314,10 +24344,8 @@ function setupWebSocketHandler(deps) {
|
|
|
24314
24344
|
}
|
|
24315
24345
|
const wsActiveOverrides = /* @__PURE__ */ new Map();
|
|
24316
24346
|
let nextCommandUserId = 1;
|
|
24317
|
-
function buildSharedCommandContext(client) {
|
|
24318
|
-
|
|
24319
|
-
if (sid)
|
|
24320
|
-
wsActiveOverrides.set(client.commandUserId, sid);
|
|
24347
|
+
function buildSharedCommandContext(client, sessionId) {
|
|
24348
|
+
wsActiveOverrides.set(client.commandUserId, sessionId);
|
|
24321
24349
|
return {
|
|
24322
24350
|
sm: client.sessionManager,
|
|
24323
24351
|
userId: client.commandUserId,
|
|
@@ -24329,19 +24357,8 @@ function setupWebSocketHandler(deps) {
|
|
|
24329
24357
|
lang: "en"
|
|
24330
24358
|
};
|
|
24331
24359
|
}
|
|
24332
|
-
async function handleSessionClear(client, ws, msg) {
|
|
24333
|
-
const prevSessionId = msg.sessionId ?? client.sessionId;
|
|
24334
|
-
saveSession(client);
|
|
24335
|
-
if (prevSessionId && client.sessionId && client.sessionManager) {
|
|
24336
|
-
client.unsubscribeEvents?.();
|
|
24337
|
-
client.unsubscribeEvents = null;
|
|
24338
|
-
client.backgroundSaves.set(prevSessionId, () => saveSession(client));
|
|
24339
|
-
}
|
|
24340
|
-
client.sessionId = null;
|
|
24341
|
-
sendJson(ws, { type: "session:cleared" });
|
|
24342
|
-
}
|
|
24343
24360
|
async function handleSessionDelete(client, ws, msg) {
|
|
24344
|
-
const delSessionId = msg
|
|
24361
|
+
const delSessionId = frameSessionId(client, msg);
|
|
24345
24362
|
if (!delSessionId) {
|
|
24346
24363
|
sendJson(ws, { type: "error", error: "session:delete requires sessionId" });
|
|
24347
24364
|
return;
|
|
@@ -24350,14 +24367,12 @@ function setupWebSocketHandler(deps) {
|
|
|
24350
24367
|
const projectPath = requestedProjectId ? resolveProjectPath2(requestedProjectId) : null;
|
|
24351
24368
|
const sm = projectPath ? getSessionManager(projectPath) : client.sessionManager;
|
|
24352
24369
|
if (!sm) {
|
|
24353
|
-
sendJson(ws, { type: "error", error: "No workspace context for delete" });
|
|
24370
|
+
sendJson(ws, { type: "error", error: "No workspace context for delete", sessionId: delSessionId });
|
|
24354
24371
|
return;
|
|
24355
24372
|
}
|
|
24356
24373
|
await sm.deleteSession(delSessionId);
|
|
24357
|
-
|
|
24358
|
-
|
|
24359
|
-
}
|
|
24360
|
-
client.backgroundSaves.delete(delSessionId);
|
|
24374
|
+
client.subscriptions.get(delSessionId)?.();
|
|
24375
|
+
client.subscriptions.delete(delSessionId);
|
|
24361
24376
|
sendJson(ws, { type: "session:deleted", sessionId: delSessionId });
|
|
24362
24377
|
}
|
|
24363
24378
|
async function handleExchangeDelete(client, ws, msg) {
|
|
@@ -24370,62 +24385,85 @@ function setupWebSocketHandler(deps) {
|
|
|
24370
24385
|
const projectPath = requestedProjectId ? resolveProjectPath2(requestedProjectId) : null;
|
|
24371
24386
|
const sm = projectPath ? getSessionManager(projectPath) : client.sessionManager;
|
|
24372
24387
|
if (!sm) {
|
|
24373
|
-
sendJson(ws, { type: "error", error: "No workspace context for exchange:delete" });
|
|
24388
|
+
sendJson(ws, { type: "error", error: "No workspace context for exchange:delete", sessionId: targetSessionId });
|
|
24374
24389
|
return;
|
|
24375
24390
|
}
|
|
24376
24391
|
const result = await sm.deleteExchange(targetSessionId, msg.userOrdinal, msg.archiveCount ?? 0);
|
|
24377
24392
|
if (result === "running") {
|
|
24378
|
-
sendJson(ws, { type: "error", error: "Cannot delete while the agent is running" });
|
|
24393
|
+
sendJson(ws, { type: "error", error: "Cannot delete while the agent is running", sessionId: targetSessionId });
|
|
24379
24394
|
return;
|
|
24380
24395
|
}
|
|
24381
24396
|
if (result === "compacting") {
|
|
24382
|
-
sendJson(ws, { type: "error", error: "Cannot delete while compacting" });
|
|
24397
|
+
sendJson(ws, { type: "error", error: "Cannot delete while compacting", sessionId: targetSessionId });
|
|
24383
24398
|
return;
|
|
24384
24399
|
}
|
|
24385
24400
|
if (result === "archived") {
|
|
24386
|
-
sendJson(ws, { type: "error", code: "archived", error: "This session archived history since it was opened \u2014 reopen it to delete individual turns." });
|
|
24401
|
+
sendJson(ws, { type: "error", code: "archived", error: "This session archived history since it was opened \u2014 reopen it to delete individual turns.", sessionId: targetSessionId });
|
|
24387
24402
|
return;
|
|
24388
24403
|
}
|
|
24389
24404
|
if (result === "not_found" || result === "no_exchange") {
|
|
24390
|
-
sendJson(ws, { type: "error", error: "Exchange not found" });
|
|
24405
|
+
sendJson(ws, { type: "error", error: "Exchange not found", sessionId: targetSessionId });
|
|
24391
24406
|
return;
|
|
24392
24407
|
}
|
|
24393
|
-
if (client.
|
|
24394
|
-
const state = getState(client);
|
|
24408
|
+
if (client.subscriptions.has(targetSessionId)) {
|
|
24409
|
+
const state = getState(client, targetSessionId);
|
|
24395
24410
|
const messages2 = state ? [...createSaveSnapshot(state)] : [];
|
|
24396
|
-
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages2, sessionId:
|
|
24411
|
+
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages2, sessionId: targetSessionId } });
|
|
24397
24412
|
}
|
|
24398
24413
|
}
|
|
24399
|
-
function createEventListener(client) {
|
|
24414
|
+
function createEventListener(client, sessionId) {
|
|
24400
24415
|
return (event, state, turnId) => {
|
|
24401
|
-
sendWsNotification(event, state, turnId, {
|
|
24402
|
-
ws: client.ws,
|
|
24403
|
-
sessionId: client.sessionId
|
|
24404
|
-
});
|
|
24416
|
+
sendWsNotification(event, state, turnId, { ws: client.ws, sessionId });
|
|
24405
24417
|
};
|
|
24406
24418
|
}
|
|
24419
|
+
function subscribeSession(client, sm, sessionId) {
|
|
24420
|
+
if (client.subscriptions.has(sessionId))
|
|
24421
|
+
return;
|
|
24422
|
+
client.subscriptions.set(sessionId, sm.registerEventListener(sessionId, createEventListener(client, sessionId)));
|
|
24423
|
+
}
|
|
24424
|
+
function frameSessionId(client, msg) {
|
|
24425
|
+
if (msg.sessionId !== void 0)
|
|
24426
|
+
return msg.sessionId;
|
|
24427
|
+
return client.subscriptions.size === 1 ? client.subscriptions.keys().next().value ?? null : null;
|
|
24428
|
+
}
|
|
24429
|
+
function unsubscribeSession(client, sessionId) {
|
|
24430
|
+
const unsubscribe = client.subscriptions.get(sessionId);
|
|
24431
|
+
if (!unsubscribe)
|
|
24432
|
+
return;
|
|
24433
|
+
saveSession(client, sessionId);
|
|
24434
|
+
unsubscribe();
|
|
24435
|
+
client.subscriptions.delete(sessionId);
|
|
24436
|
+
}
|
|
24437
|
+
function setClientProject(client, projectId) {
|
|
24438
|
+
if (client.projectId === projectId)
|
|
24439
|
+
return;
|
|
24440
|
+
for (const sid of [...client.subscriptions.keys()])
|
|
24441
|
+
unsubscribeSession(client, sid);
|
|
24442
|
+
client.projectId = projectId;
|
|
24443
|
+
}
|
|
24407
24444
|
function reclaimIfAbandoned(client) {
|
|
24408
|
-
if (
|
|
24445
|
+
if (client.subscriptions.size === 0)
|
|
24409
24446
|
return;
|
|
24410
24447
|
const closed = client.ws.readyState === client.ws.CLOSED;
|
|
24411
24448
|
if (!closed && Date.now() - client.lastClientPingAt <= CLIENT_SILENCE_LIMIT_MS)
|
|
24412
24449
|
return;
|
|
24413
|
-
client.
|
|
24414
|
-
client.
|
|
24415
|
-
|
|
24416
|
-
|
|
24450
|
+
const released = [...client.subscriptions.keys()];
|
|
24451
|
+
for (const unsubscribe of client.subscriptions.values())
|
|
24452
|
+
unsubscribe();
|
|
24453
|
+
client.subscriptions.clear();
|
|
24454
|
+
console.debug(`[WS] Released listeners for ${closed ? "closed" : "abandoned"} connection sessions=${released.join(",")}`);
|
|
24455
|
+
for (const sessionId of released)
|
|
24456
|
+
sendJson(client.ws, { type: "listener:released", sessionId });
|
|
24417
24457
|
}
|
|
24418
24458
|
wss2.on("connection", (ws) => {
|
|
24419
24459
|
const terminalManager = new TerminalManager(ws);
|
|
24420
24460
|
const client = {
|
|
24421
24461
|
ws,
|
|
24422
|
-
|
|
24462
|
+
subscriptions: /* @__PURE__ */ new Map(),
|
|
24423
24463
|
projectId: null,
|
|
24424
24464
|
sessionManager: null,
|
|
24425
24465
|
agentId: "default",
|
|
24426
24466
|
terminalManager,
|
|
24427
|
-
backgroundSaves: /* @__PURE__ */ new Map(),
|
|
24428
|
-
unsubscribeEvents: null,
|
|
24429
24467
|
lastClientPingAt: Date.now(),
|
|
24430
24468
|
commandUserId: `ws-${nextCommandUserId++}`
|
|
24431
24469
|
};
|
|
@@ -24474,14 +24512,18 @@ function setupWebSocketHandler(deps) {
|
|
|
24474
24512
|
await handleChat(client, msg);
|
|
24475
24513
|
break;
|
|
24476
24514
|
case "chat:stop":
|
|
24477
|
-
handleChatStop(client);
|
|
24515
|
+
handleChatStop(client, msg);
|
|
24478
24516
|
break;
|
|
24479
24517
|
case "chat:interrupt":
|
|
24480
|
-
handleChatInterrupt(client);
|
|
24518
|
+
handleChatInterrupt(client, msg);
|
|
24481
24519
|
break;
|
|
24482
24520
|
case "subscribe":
|
|
24483
24521
|
await handleSubscribe(client, msg);
|
|
24484
24522
|
break;
|
|
24523
|
+
case "unsubscribe":
|
|
24524
|
+
if (msg.sessionId)
|
|
24525
|
+
unsubscribeSession(client, msg.sessionId);
|
|
24526
|
+
break;
|
|
24485
24527
|
case "terminal:start":
|
|
24486
24528
|
handleTerminalStart(client, msg);
|
|
24487
24529
|
break;
|
|
@@ -24495,9 +24537,6 @@ function setupWebSocketHandler(deps) {
|
|
|
24495
24537
|
case "terminal:reattach":
|
|
24496
24538
|
terminalManager.reattachAll(msg.browserId ?? "", msg.workspacePath ?? "");
|
|
24497
24539
|
break;
|
|
24498
|
-
case "session:clear":
|
|
24499
|
-
await handleSessionClear(client, ws, msg);
|
|
24500
|
-
break;
|
|
24501
24540
|
case "session:delete":
|
|
24502
24541
|
await handleSessionDelete(client, ws, msg);
|
|
24503
24542
|
break;
|
|
@@ -24510,51 +24549,48 @@ function setupWebSocketHandler(deps) {
|
|
|
24510
24549
|
break;
|
|
24511
24550
|
}
|
|
24512
24551
|
const cmdName = msg.type.slice("command:".length);
|
|
24513
|
-
await bindOrCreateSession(client, msg);
|
|
24552
|
+
const sid = msg.sessionId === void 0 ? frameSessionId(client, msg) : await bindOrCreateSession(client, msg);
|
|
24514
24553
|
const sm = client.sessionManager;
|
|
24515
|
-
const sid = client.sessionId;
|
|
24516
24554
|
if (!sm || !sid) {
|
|
24517
24555
|
sendJson(ws, { type: "error", error: "No active session" });
|
|
24518
24556
|
break;
|
|
24519
24557
|
}
|
|
24520
24558
|
if (cmdName === "session" && (msg.message ?? "").trim().split(/\s+/)[0] === "compact") {
|
|
24521
24559
|
sm.compactSession(sid, {
|
|
24522
|
-
onProgress: (status) => sendJson(ws, { type: `compact:${status}
|
|
24560
|
+
onProgress: (status) => sendJson(ws, { type: `compact:${status}`, sessionId: sid })
|
|
24523
24561
|
}).then((result2) => {
|
|
24524
24562
|
if (result2 === "no_session")
|
|
24525
|
-
sendJson(ws, { type: "error", error: "No active session to compact" });
|
|
24563
|
+
sendJson(ws, { type: "error", error: "No active session to compact", sessionId: sid });
|
|
24526
24564
|
else if (result2 === "running")
|
|
24527
|
-
sendJson(ws, { type: "error", error: "Cannot compact while agent is running" });
|
|
24565
|
+
sendJson(ws, { type: "error", error: "Cannot compact while agent is running", sessionId: sid });
|
|
24528
24566
|
else if (result2 === "already")
|
|
24529
|
-
sendJson(ws, { type: "error", error: "Compact already in progress" });
|
|
24567
|
+
sendJson(ws, { type: "error", error: "Compact already in progress", sessionId: sid });
|
|
24530
24568
|
else if (result2 === "nothing") {
|
|
24531
24569
|
const state = sm.getCachedUIState(sid);
|
|
24532
|
-
sendJson(ws, { type: "session:compacted", message: "Nothing to compact", contextTokens: state?.contextTokens ?? 0 });
|
|
24570
|
+
sendJson(ws, { type: "session:compacted", message: "Nothing to compact", contextTokens: state?.contextTokens ?? 0, sessionId: sid });
|
|
24533
24571
|
}
|
|
24534
24572
|
}).catch((err) => {
|
|
24535
|
-
sendJson(ws, { type: "error", error: `Compact failed: ${err instanceof Error ? err.message : String(err)}
|
|
24573
|
+
sendJson(ws, { type: "error", error: `Compact failed: ${err instanceof Error ? err.message : String(err)}`, sessionId: sid });
|
|
24536
24574
|
});
|
|
24537
24575
|
break;
|
|
24538
24576
|
}
|
|
24539
|
-
const sharedCtx = buildSharedCommandContext(client);
|
|
24577
|
+
const sharedCtx = buildSharedCommandContext(client, sid);
|
|
24540
24578
|
const result = await dispatchCommand(sharedCtx, `/${cmdName}`, (msg.message ?? "").trim(), { channelName: "ws" });
|
|
24541
24579
|
if (result) {
|
|
24542
|
-
sendJson(ws, { type: "chat:system", text: result.text });
|
|
24580
|
+
sendJson(ws, { type: "chat:system", text: result.text, sessionId: sid });
|
|
24543
24581
|
if (result.switchTo) {
|
|
24544
|
-
client.
|
|
24545
|
-
|
|
24546
|
-
client.unsubscribeEvents = sm.registerEventListener(result.switchTo, createEventListener(client));
|
|
24547
|
-
sendJson(ws, { type: "session:switched", sessionId: result.switchTo });
|
|
24582
|
+
subscribeSession(client, sm, result.switchTo);
|
|
24583
|
+
sendJson(ws, { type: "session:switched", sessionId: result.switchTo, fromSessionId: sid });
|
|
24548
24584
|
}
|
|
24549
24585
|
} else {
|
|
24550
|
-
sendJson(ws, { type: "error", error: `Unknown command: ${cmdName}
|
|
24586
|
+
sendJson(ws, { type: "error", error: `Unknown command: ${cmdName}`, sessionId: sid });
|
|
24551
24587
|
}
|
|
24552
24588
|
}
|
|
24553
24589
|
}
|
|
24554
24590
|
} catch (err) {
|
|
24555
24591
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
24556
24592
|
console.debug(`[WS] Error handling message: ${errorMessage}`);
|
|
24557
|
-
sendJson(ws, { type: "error", error: errorMessage });
|
|
24593
|
+
sendJson(ws, { type: "error", error: errorMessage, ...msg.sessionId ? { sessionId: msg.sessionId } : {} });
|
|
24558
24594
|
}
|
|
24559
24595
|
});
|
|
24560
24596
|
});
|
|
@@ -24565,23 +24601,24 @@ function setupWebSocketHandler(deps) {
|
|
|
24565
24601
|
terminalManager.detachAll();
|
|
24566
24602
|
wsActiveOverrides.delete(client.commandUserId);
|
|
24567
24603
|
const sm = client.sessionManager;
|
|
24568
|
-
const sid
|
|
24569
|
-
|
|
24570
|
-
|
|
24571
|
-
|
|
24572
|
-
|
|
24573
|
-
|
|
24604
|
+
for (const [sid, unsubscribeLive] of client.subscriptions) {
|
|
24605
|
+
unsubscribeLive();
|
|
24606
|
+
if (!sm || !sm.hasActiveWorkInTree(sid) || detachedSessions.has(sid)) {
|
|
24607
|
+
saveSession(client, sid);
|
|
24608
|
+
continue;
|
|
24609
|
+
}
|
|
24610
|
+
console.debug(`[WS] Detaching active session: ${sid}`);
|
|
24574
24611
|
const pendingEvents = [];
|
|
24575
24612
|
const bgHandler = (_event, _state, _turnId) => {
|
|
24576
|
-
bufferDetachedNotification(_event, pendingEvents);
|
|
24613
|
+
bufferDetachedNotification(_event, pendingEvents, sid);
|
|
24577
24614
|
};
|
|
24578
24615
|
const unsubscribe = sm.registerEventListener(sid, bgHandler);
|
|
24579
24616
|
const graceTimer = setTimeout(() => {
|
|
24580
|
-
detachedSessions.delete(
|
|
24581
|
-
saveSession(client);
|
|
24617
|
+
detachedSessions.delete(sid);
|
|
24618
|
+
saveSession(client, sid);
|
|
24582
24619
|
unsubscribe();
|
|
24583
24620
|
}, config.timeout.sessionGrace);
|
|
24584
|
-
detachedSessions.set(
|
|
24621
|
+
detachedSessions.set(sid, {
|
|
24585
24622
|
sessionManager: sm,
|
|
24586
24623
|
sessionId: sid,
|
|
24587
24624
|
projectId: client.projectId ?? "",
|
|
@@ -24589,15 +24626,8 @@ function setupWebSocketHandler(deps) {
|
|
|
24589
24626
|
pendingEvents,
|
|
24590
24627
|
unsubscribe
|
|
24591
24628
|
});
|
|
24592
|
-
} else {
|
|
24593
|
-
client.unsubscribeEvents?.();
|
|
24594
|
-
client.unsubscribeEvents = null;
|
|
24595
|
-
saveSession(client);
|
|
24596
|
-
}
|
|
24597
|
-
for (const [sid2, saveFn] of client.backgroundSaves) {
|
|
24598
|
-
saveFn();
|
|
24599
|
-
client.backgroundSaves.delete(sid2);
|
|
24600
24629
|
}
|
|
24630
|
+
client.subscriptions.clear();
|
|
24601
24631
|
watchers.detach(ws);
|
|
24602
24632
|
console.debug(`[WS] Client disconnected (total: ${clients.size})`);
|
|
24603
24633
|
}
|
|
@@ -24610,7 +24640,7 @@ function setupWebSocketHandler(deps) {
|
|
|
24610
24640
|
const projectId = msg.projectId ?? client2.projectId;
|
|
24611
24641
|
if (!msg.sessionId || !projectId)
|
|
24612
24642
|
return null;
|
|
24613
|
-
client2
|
|
24643
|
+
setClientProject(client2, projectId);
|
|
24614
24644
|
const agentId = msg.agentId ?? client2.agentId;
|
|
24615
24645
|
const projectPath = resolveProjectPath2(projectId);
|
|
24616
24646
|
if (projectPath) {
|
|
@@ -24620,19 +24650,17 @@ function setupWebSocketHandler(deps) {
|
|
|
24620
24650
|
if (!client2.sessionManager)
|
|
24621
24651
|
return null;
|
|
24622
24652
|
const sm = client2.sessionManager;
|
|
24623
|
-
const
|
|
24653
|
+
const sid = msg.sessionId;
|
|
24654
|
+
const existing = sm.getSessionById(sid);
|
|
24624
24655
|
if (!existing) {
|
|
24625
|
-
|
|
24626
|
-
client2
|
|
24627
|
-
|
|
24628
|
-
|
|
24629
|
-
|
|
24630
|
-
|
|
24631
|
-
|
|
24632
|
-
|
|
24633
|
-
client2.unsubscribeEvents = sm.registerEventListener(client2.sessionId, createEventListener(client2));
|
|
24634
|
-
}
|
|
24635
|
-
return client2.sessionId;
|
|
24656
|
+
await sm.createSession(agentId, null, "Explorer chat", void 0, sid);
|
|
24657
|
+
subscribeSession(client2, sm, sid);
|
|
24658
|
+
const ctxConfig = await sm.getContextConfig(sid);
|
|
24659
|
+
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: [], sessionId: sid, maxContextTokens: ctxConfig.maxTokens, agentId } });
|
|
24660
|
+
} else {
|
|
24661
|
+
subscribeSession(client2, sm, sid);
|
|
24662
|
+
}
|
|
24663
|
+
return sid;
|
|
24636
24664
|
}
|
|
24637
24665
|
function rememberChat(msg) {
|
|
24638
24666
|
if (msg.clientMsgId)
|
|
@@ -24656,17 +24684,16 @@ function setupWebSocketHandler(deps) {
|
|
|
24656
24684
|
const projectPath = resolveProjectPath2(msg.projectId);
|
|
24657
24685
|
let sid = await bindOrCreateSession(client2, msg);
|
|
24658
24686
|
if (!sid || !client2.sessionManager) {
|
|
24659
|
-
sendJson(ws, { type: "error", error: "Cannot resolve project path" });
|
|
24687
|
+
sendJson(ws, { type: "error", error: "Cannot resolve project path", sessionId: msg.sessionId });
|
|
24660
24688
|
return;
|
|
24661
24689
|
}
|
|
24662
24690
|
const sm = client2.sessionManager;
|
|
24663
24691
|
const goalRouted = resolveGoalRoute(sm.getDb(), sid);
|
|
24664
24692
|
if (goalRouted !== sid) {
|
|
24693
|
+
const fromSessionId = sid;
|
|
24665
24694
|
sid = goalRouted;
|
|
24666
|
-
client2
|
|
24667
|
-
|
|
24668
|
-
client2.unsubscribeEvents = sm.registerEventListener(sid, createEventListener(client2));
|
|
24669
|
-
sendJson(ws, { type: "session:switched", sessionId: sid });
|
|
24695
|
+
subscribeSession(client2, sm, sid);
|
|
24696
|
+
sendJson(ws, { type: "session:switched", sessionId: sid, fromSessionId, clientMsgId: msg.clientMsgId });
|
|
24670
24697
|
}
|
|
24671
24698
|
let uiMessage = msg.message;
|
|
24672
24699
|
if (msg.images?.length && projectPath) {
|
|
@@ -24699,7 +24726,7 @@ ${msg.message}` : markers.join("\n");
|
|
|
24699
24726
|
await sm.enqueueUserMessage(sid, msg.message, msg.images);
|
|
24700
24727
|
ackChat(msg);
|
|
24701
24728
|
const queuedText = sm.isSessionRunning(sid) ? "Context compacting, message queued \u2014 will process after the compact and the current step finish." : "Context compacting, message queued \u2014 will process after compact completes.";
|
|
24702
|
-
sendJson(ws, { type: "chat:queued", reason: "compact", message: queuedText });
|
|
24729
|
+
sendJson(ws, { type: "chat:queued", reason: "compact", message: queuedText, sessionId: sid });
|
|
24703
24730
|
return;
|
|
24704
24731
|
}
|
|
24705
24732
|
if (sm.isSessionRunning(sid)) {
|
|
@@ -24717,67 +24744,59 @@ ${msg.message}` : markers.join("\n");
|
|
|
24717
24744
|
const accessLevel = msg.accessLevel === void 0 ? void 0 : msg.accessLevel === "full" || getSandboxBackend() === null ? null : msg.accessLevel;
|
|
24718
24745
|
sm.sendUserMessage(sid, msg.message, msg.images, accessLevel).catch((err) => {
|
|
24719
24746
|
console.debug(`[WS] Chat error: ${err instanceof Error ? err.message : String(err)}`);
|
|
24720
|
-
sendJson(ws, { type: "error", error: err instanceof Error ? err.message : String(err) });
|
|
24721
|
-
saveSession(client2);
|
|
24747
|
+
sendJson(ws, { type: "error", error: err instanceof Error ? err.message : String(err), sessionId: sid });
|
|
24748
|
+
saveSession(client2, sid);
|
|
24722
24749
|
});
|
|
24723
24750
|
}
|
|
24724
|
-
function handleChatStop(client2) {
|
|
24725
|
-
|
|
24751
|
+
function handleChatStop(client2, msg) {
|
|
24752
|
+
const sid = frameSessionId(client2, msg);
|
|
24753
|
+
console.debug(`[WS] Stop requested for session=${sid}`);
|
|
24726
24754
|
const sm = client2.sessionManager;
|
|
24727
|
-
const sid = client2.sessionId;
|
|
24728
24755
|
if (!sm || !sid)
|
|
24729
24756
|
return;
|
|
24730
24757
|
if (sm.isSessionCompacting(sid) && !sm.isSessionRunning(sid)) {
|
|
24731
24758
|
sm.cancelCompact(sid);
|
|
24732
|
-
sendJson(ws, { type: "chat:stopped", sessionId:
|
|
24759
|
+
sendJson(ws, { type: "chat:stopped", sessionId: sid });
|
|
24733
24760
|
return;
|
|
24734
24761
|
}
|
|
24735
24762
|
sm.stopUserSession(sid);
|
|
24736
|
-
sendJson(ws, { type: "chat:stopped", sessionId:
|
|
24737
|
-
saveSession(client2);
|
|
24763
|
+
sendJson(ws, { type: "chat:stopped", sessionId: sid });
|
|
24764
|
+
saveSession(client2, sid);
|
|
24738
24765
|
}
|
|
24739
|
-
function handleChatInterrupt(client2) {
|
|
24740
|
-
|
|
24766
|
+
function handleChatInterrupt(client2, msg) {
|
|
24767
|
+
const sid = frameSessionId(client2, msg);
|
|
24768
|
+
console.debug(`[WS] Interrupt requested for session=${sid}`);
|
|
24741
24769
|
const sm = client2.sessionManager;
|
|
24742
|
-
const sid = client2.sessionId;
|
|
24743
24770
|
if (!sm || !sid)
|
|
24744
24771
|
return;
|
|
24745
24772
|
if (sm.isSessionCompacting(sid) && !sm.isSessionRunning(sid)) {
|
|
24746
24773
|
sm.cancelCompact(sid);
|
|
24747
|
-
sendJson(ws, { type: "chat:stopped", sessionId:
|
|
24774
|
+
sendJson(ws, { type: "chat:stopped", sessionId: sid });
|
|
24748
24775
|
return;
|
|
24749
24776
|
}
|
|
24750
24777
|
sm.interruptSession(sid);
|
|
24751
24778
|
}
|
|
24752
24779
|
async function handleSubscribe(client2, msg) {
|
|
24753
|
-
if (client2.sessionId && client2.sessionId !== msg.sessionId) {
|
|
24754
|
-
saveSession(client2);
|
|
24755
|
-
}
|
|
24756
|
-
if (client2.sessionId) {
|
|
24757
|
-
client2.unsubscribeEvents?.();
|
|
24758
|
-
client2.unsubscribeEvents = null;
|
|
24759
|
-
client2.sessionId = null;
|
|
24760
|
-
}
|
|
24761
|
-
if (msg.sessionId)
|
|
24762
|
-
client2.sessionId = msg.sessionId;
|
|
24763
24780
|
if (msg.projectId)
|
|
24764
|
-
client2
|
|
24765
|
-
const
|
|
24766
|
-
|
|
24781
|
+
setClientProject(client2, msg.projectId);
|
|
24782
|
+
const sid = msg.sessionId || null;
|
|
24783
|
+
const detached = sid ? detachedSessions.get(sid) : void 0;
|
|
24784
|
+
if (detached && sid) {
|
|
24767
24785
|
clearTimeout(detached.timer);
|
|
24768
|
-
detachedSessions.delete(
|
|
24769
|
-
console.debug(`[WS] Reattaching detached session: ${
|
|
24786
|
+
detachedSessions.delete(sid);
|
|
24787
|
+
console.debug(`[WS] Reattaching detached session: ${sid}`);
|
|
24788
|
+
if (detached.projectId)
|
|
24789
|
+
setClientProject(client2, detached.projectId);
|
|
24770
24790
|
client2.sessionManager = detached.sessionManager;
|
|
24771
|
-
|
|
24772
|
-
|
|
24773
|
-
const
|
|
24774
|
-
const
|
|
24775
|
-
const running = client2.sessionManager.isSessionRunning(client2.sessionId);
|
|
24791
|
+
const sm = detached.sessionManager;
|
|
24792
|
+
const state2 = getState(client2, sid);
|
|
24793
|
+
const ctxConfig = await sm.getContextConfig(sid);
|
|
24794
|
+
const running = sm.isSessionRunning(sid);
|
|
24776
24795
|
const messages3 = state2 ? running ? [...state2.messageLog] : [...createSaveSnapshot(state2)] : [];
|
|
24777
|
-
const detachedSession =
|
|
24778
|
-
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages3, sessionId:
|
|
24796
|
+
const detachedSession = sm.getSessionById(sid);
|
|
24797
|
+
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages3, sessionId: sid, maxContextTokens: ctxConfig.maxTokens, agentId: detachedSession?.agentId, archiveCount: archiveCountFor(client2, sid, detachedSession?.agentId), accessLevel: detachedSession?.accessLevel ?? null } });
|
|
24779
24798
|
if (state2 && state2.contextTokens > 0) {
|
|
24780
|
-
sendJson(ws, { type: "chat:usage", contextTokens: state2.contextTokens, outputTokens: state2.outputTokens });
|
|
24799
|
+
sendJson(ws, { type: "chat:usage", contextTokens: state2.contextTokens, outputTokens: state2.outputTokens, sessionId: sid });
|
|
24781
24800
|
}
|
|
24782
24801
|
while (detached.pendingEvents.length > 0) {
|
|
24783
24802
|
const batch = detached.pendingEvents.splice(0, detached.pendingEvents.length);
|
|
@@ -24785,11 +24804,11 @@ ${msg.message}` : markers.join("\n");
|
|
|
24785
24804
|
sendJson(ws, evt);
|
|
24786
24805
|
}
|
|
24787
24806
|
detached.unsubscribe();
|
|
24788
|
-
|
|
24807
|
+
subscribeSession(client2, sm, sid);
|
|
24789
24808
|
if (state2 && running) {
|
|
24790
24809
|
console.debug(`[WS] Session still running \u2014 synthesizing in-progress state`);
|
|
24791
24810
|
const agentName = detachedSession?.agentName ?? (state2.streamingAgent || "default");
|
|
24792
|
-
const sessionId =
|
|
24811
|
+
const sessionId = sid;
|
|
24793
24812
|
sendJson(ws, { type: "chat:followup", agentName, replay: true, sessionId });
|
|
24794
24813
|
for (const block of state2.turnContentBlocks) {
|
|
24795
24814
|
if (block.type === "thinking") {
|
|
@@ -24812,36 +24831,28 @@ ${msg.message}` : markers.join("\n");
|
|
|
24812
24831
|
client2.sessionManager = getSessionManager(subProjectPath);
|
|
24813
24832
|
watchers.attach(ws, subProjectPath);
|
|
24814
24833
|
}
|
|
24815
|
-
if (msg.sessionId) {
|
|
24816
|
-
const pendingSave = client2.backgroundSaves.get(msg.sessionId);
|
|
24817
|
-
if (pendingSave) {
|
|
24818
|
-
pendingSave();
|
|
24819
|
-
client2.backgroundSaves.delete(msg.sessionId);
|
|
24820
|
-
}
|
|
24821
|
-
}
|
|
24822
24834
|
let agentId;
|
|
24823
24835
|
let accessLevel;
|
|
24824
|
-
if (
|
|
24825
|
-
const existingSession = client2.sessionManager.getSessionById(
|
|
24836
|
+
if (sid && client2.sessionManager) {
|
|
24837
|
+
const existingSession = client2.sessionManager.getSessionById(sid);
|
|
24826
24838
|
if (existingSession) {
|
|
24827
|
-
client2.
|
|
24828
|
-
client2.unsubscribeEvents = client2.sessionManager.registerEventListener(msg.sessionId, createEventListener(client2));
|
|
24839
|
+
subscribeSession(client2, client2.sessionManager, sid);
|
|
24829
24840
|
agentId = existingSession.agentId;
|
|
24830
24841
|
accessLevel = existingSession.accessLevel ?? null;
|
|
24831
24842
|
}
|
|
24832
24843
|
}
|
|
24833
24844
|
let maxContextTokens = config.model.maxContextTokens;
|
|
24834
|
-
if (
|
|
24835
|
-
const view = await client2.sessionManager.getSessionView(
|
|
24845
|
+
if (sid && client2.sessionManager) {
|
|
24846
|
+
const view = await client2.sessionManager.getSessionView(sid);
|
|
24836
24847
|
if (view) {
|
|
24837
24848
|
maxContextTokens = view.maxContextTokens;
|
|
24838
24849
|
}
|
|
24839
24850
|
}
|
|
24840
|
-
const state = getState(client2);
|
|
24851
|
+
const state = sid ? getState(client2, sid) : null;
|
|
24841
24852
|
const messages2 = state ? [...createSaveSnapshot(state)] : [];
|
|
24842
|
-
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages2, sessionId:
|
|
24853
|
+
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages2, sessionId: sid, maxContextTokens, agentId, archiveCount: sid ? archiveCountFor(client2, sid, agentId) : 0, accessLevel } });
|
|
24843
24854
|
if (state && state.contextTokens > 0) {
|
|
24844
|
-
sendJson(ws, { type: "chat:usage", contextTokens: state.contextTokens, outputTokens: state.outputTokens });
|
|
24855
|
+
sendJson(ws, { type: "chat:usage", contextTokens: state.contextTokens, outputTokens: state.outputTokens, sessionId: sid });
|
|
24845
24856
|
}
|
|
24846
24857
|
}
|
|
24847
24858
|
function handleTerminalStart(client2, msg) {
|
|
@@ -33015,7 +33026,7 @@ var init_dist2 = __esm({
|
|
|
33015
33026
|
init_git_ssh();
|
|
33016
33027
|
__dirname3 = path72.dirname(fileURLToPath3(import.meta.url));
|
|
33017
33028
|
PORT = config.server.port;
|
|
33018
|
-
HALO_VERSION = "1.5.
|
|
33029
|
+
HALO_VERSION = "1.5.3-alpha";
|
|
33019
33030
|
PROJECT_ROOT = findProjectRoot();
|
|
33020
33031
|
HALO_HOME4 = path72.join(homedir24(), ".halo");
|
|
33021
33032
|
GIT_SHA = readGitSha();
|
|
@@ -33946,7 +33957,7 @@ import path73 from "node:path";
|
|
|
33946
33957
|
import fs67 from "node:fs";
|
|
33947
33958
|
import { homedir as homedir25 } from "node:os";
|
|
33948
33959
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
33949
|
-
var VERSION = "1.5.
|
|
33960
|
+
var VERSION = "1.5.3-alpha";
|
|
33950
33961
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
33951
33962
|
|
|
33952
33963
|
Commands:
|