@turmind/halo 1.5.0-alpha → 1.5.2-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/app/page-fa5e935dbaa74165.js +1 -0
- package/admin-out/_next/static/css/b8ffb5202b5a6619.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +3 -3
- package/bundled-docs/design/agent.md +1 -1
- package/bundled-docs/design/canvas-extensions.md +3 -1
- package/bundled-docs/design/express-self.md +9 -7
- package/bundled-docs/design/relay.md +7 -5
- package/bundled-docs/design/session.md +26 -17
- package/bundled-docs/design/storage.md +2 -4
- package/bundled-docs/design/ws.md +4 -4
- package/bundled-docs/dev/deploy.md +39 -9
- package/bundled-docs/dev/desktop-packaging.md +18 -8
- package/bundled-docs/dev/env.md +1 -2
- package/bundled-docs/dev/previews.md +9 -1
- package/bundled-docs/dev/tools.md +2 -2
- package/bundled-docs/guide/chat.md +4 -2
- package/bundled-docs/requirements/chat.md +7 -5
- package/bundled-docs/requirements/editor.md +1 -0
- package/dist/index.js +365 -257
- package/package.json +1 -1
- package/templates/prompts/root/DELEGATION.md +7 -0
- package/admin-out/_next/static/chunks/app/page-c33392db67853747.js +0 -1
- package/admin-out/_next/static/css/7997e44c08e34b2b.css +0 -3
- /package/admin-out/_next/static/{Wn72NPclwda8BiGd0ChhY → DLRpRENVwZ-EHTRGc9AzB}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{Wn72NPclwda8BiGd0ChhY → DLRpRENVwZ-EHTRGc9AzB}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -512,7 +512,6 @@ function generalSection() {
|
|
|
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" },
|
|
515
|
-
{ key: "compact.summarize_timeout_sec", type: "int", globalOnly: true, description: "LLM summarization timeout (seconds)", description_zh: "LLM \u6458\u8981\u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09", default: "300" },
|
|
516
515
|
// sandbox (Linux bwrap / macOS sandbox-exec) — global-only: these define the security
|
|
517
516
|
// boundary agents run inside; a workspace overriding them could lift
|
|
518
517
|
// its own sandbox constraints.
|
|
@@ -1149,9 +1148,10 @@ var init_config = __esm({
|
|
|
1149
1148
|
// connection (server accepted but never sends bytes / sends no RST) hangs
|
|
1150
1149
|
// the await indefinitely. Bounds each callModel; on expiry the request
|
|
1151
1150
|
// aborts and the agent loop's retry takes over. Streaming providers reset
|
|
1152
|
-
// the timer on
|
|
1153
|
-
//
|
|
1154
|
-
|
|
1151
|
+
// the timer on any data the stream delivers (pings, tool-argument
|
|
1152
|
+
// fragments, SSE comments included), so for them it is an idle timeout — a
|
|
1153
|
+
// long but live stream is never cut off.
|
|
1154
|
+
modelRequest: systemInt("HALO_MODEL_TIMEOUT", "timeout.model_request", 10 * 6e4),
|
|
1155
1155
|
sessionGrace: systemInt("HALO_SESSION_GRACE", "timeout.session_grace", 5 * 6e4),
|
|
1156
1156
|
terminalGrace: systemInt("HALO_TERMINAL_GRACE", "timeout.terminal_grace", 5 * 6e4)
|
|
1157
1157
|
},
|
|
@@ -1188,8 +1188,7 @@ var init_config = __esm({
|
|
|
1188
1188
|
compact: {
|
|
1189
1189
|
keep_messages: settingsInt("general.compact.keep_messages", 5),
|
|
1190
1190
|
max_summary_input: settingsInt("general.compact.max_summary_input", 15e3),
|
|
1191
|
-
max_message_slice: settingsInt("general.compact.max_message_slice", 800)
|
|
1192
|
-
summarize_timeout_sec: settingsInt("general.compact.summarize_timeout_sec", 300)
|
|
1191
|
+
max_message_slice: settingsInt("general.compact.max_message_slice", 800)
|
|
1193
1192
|
},
|
|
1194
1193
|
auth: {
|
|
1195
1194
|
tokenMaxAge: 14 * 24 * 60 * 60,
|
|
@@ -1431,8 +1430,10 @@ var init_model_error = __esm({
|
|
|
1431
1430
|
});
|
|
1432
1431
|
|
|
1433
1432
|
// ../server/dist/agents/compact.js
|
|
1434
|
-
function
|
|
1433
|
+
function compactCut(messages2) {
|
|
1435
1434
|
const keepCount = config.compact.keep_messages;
|
|
1435
|
+
if (messages2.length <= keepCount)
|
|
1436
|
+
return 0;
|
|
1436
1437
|
let cut = Math.max(0, messages2.length - keepCount);
|
|
1437
1438
|
while (cut < messages2.length) {
|
|
1438
1439
|
const m = messages2[cut];
|
|
@@ -1443,7 +1444,7 @@ function splitForCompact(messages2) {
|
|
|
1443
1444
|
}
|
|
1444
1445
|
break;
|
|
1445
1446
|
}
|
|
1446
|
-
return
|
|
1447
|
+
return cut;
|
|
1447
1448
|
}
|
|
1448
1449
|
function messageText(m) {
|
|
1449
1450
|
const content = m.content;
|
|
@@ -1453,12 +1454,11 @@ function messageText(m) {
|
|
|
1453
1454
|
return String(content ?? "");
|
|
1454
1455
|
}
|
|
1455
1456
|
function localCompactMessages(messages2) {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1459
|
-
const { recentMsgs, olderMsgs } = splitForCompact(messages2);
|
|
1460
|
-
if (olderMsgs.length === 0)
|
|
1457
|
+
const cut = messages2 ? compactCut(messages2) : 0;
|
|
1458
|
+
if (cut === 0)
|
|
1461
1459
|
return { compacted: false, messages: messages2 };
|
|
1460
|
+
const recentMsgs = messages2.slice(cut);
|
|
1461
|
+
const olderMsgs = messages2.slice(0, cut);
|
|
1462
1462
|
const maxSlice = config.compact.max_message_slice;
|
|
1463
1463
|
const lines = [];
|
|
1464
1464
|
for (const m of olderMsgs) {
|
|
@@ -1621,7 +1621,7 @@ function toolResultImages(content) {
|
|
|
1621
1621
|
}
|
|
1622
1622
|
return images.length > 0 ? [{ type: "text", text: "[Images from the tool results above]" }, ...images] : [];
|
|
1623
1623
|
}
|
|
1624
|
-
var TOOL_ERROR_MARKER, TOOL_WARN_MARKER, MODEL_TIMEOUT_ERROR, INTERRUPTED_REPLY_MARKER, AgentLoop;
|
|
1624
|
+
var TOOL_ERROR_MARKER, TOOL_WARN_MARKER, MODEL_TIMEOUT_ERROR, INTERRUPTED_REPLY_MARKER, ACTIVITY_DELTA, AgentLoop;
|
|
1625
1625
|
var init_agent_loop = __esm({
|
|
1626
1626
|
"../server/dist/agents/agent-loop.js"() {
|
|
1627
1627
|
"use strict";
|
|
@@ -1630,6 +1630,7 @@ var init_agent_loop = __esm({
|
|
|
1630
1630
|
TOOL_WARN_MARKER = "__TOOL_WARN__";
|
|
1631
1631
|
MODEL_TIMEOUT_ERROR = "Model request timed out";
|
|
1632
1632
|
INTERRUPTED_REPLY_MARKER = "\n\n[reply interrupted by the user here \u2014 the text above is what was shown before the cut]";
|
|
1633
|
+
ACTIVITY_DELTA = { type: "activity", text: "" };
|
|
1633
1634
|
AgentLoop = class {
|
|
1634
1635
|
tools;
|
|
1635
1636
|
messages = [];
|
|
@@ -1676,8 +1677,10 @@ var init_agent_loop = __esm({
|
|
|
1676
1677
|
}
|
|
1677
1678
|
};
|
|
1678
1679
|
const call = this.callModel(callSignal, (d) => {
|
|
1679
|
-
deltas.push(d);
|
|
1680
1680
|
timer.refresh();
|
|
1681
|
+
if (d.type === "activity")
|
|
1682
|
+
return;
|
|
1683
|
+
deltas.push({ type: d.type, text: d.text });
|
|
1681
1684
|
notify();
|
|
1682
1685
|
});
|
|
1683
1686
|
call.then(() => {
|
|
@@ -2618,7 +2621,7 @@ var init_init = __esm({
|
|
|
2618
2621
|
path8.resolve(__dirname, "..", "bundled-docs"),
|
|
2619
2622
|
path8.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
2620
2623
|
];
|
|
2621
|
-
TEMPLATE_VERSION =
|
|
2624
|
+
TEMPLATE_VERSION = 76;
|
|
2622
2625
|
VERSION_FILE = ".template-version";
|
|
2623
2626
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
2624
2627
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -3587,7 +3590,8 @@ async function deliverGoalRound(host, worker) {
|
|
|
3587
3590
|
return;
|
|
3588
3591
|
let report = worker.finalOutput || worker.output || "(no output)";
|
|
3589
3592
|
if (worker.turnError) {
|
|
3590
|
-
|
|
3593
|
+
const next = worker.turnErrorKind === "account" ? `This is a model account / credential / balance / permission problem: retrying or re-dispatching will fail the same way. Fix the model configuration first (or tell the user to fix it), and do not resume the worker with query_session until then.` : `Re-dispatch with query_session to let the worker resume.`;
|
|
3594
|
+
report = `[WORKER ABORTED: this round was terminated by an unrecoverable error, NOT a normal wrap-up. Error: ${worker.turnError}. The text below is a partial trace of the aborted turn \u2014 judge this as an interrupted round, do not score it as a completed result. ${next}]
|
|
3591
3595
|
|
|
3592
3596
|
${report}`;
|
|
3593
3597
|
}
|
|
@@ -4907,6 +4911,10 @@ function execStop(ctx) {
|
|
|
4907
4911
|
const active = findActiveSessionId(ctx.sm, ctx.userId, ctx.sessionPrefix, ctx.activeOverrides, ctx.accessLevel);
|
|
4908
4912
|
if (!active)
|
|
4909
4913
|
return { text: t("stop.no_session", ctx.lang) };
|
|
4914
|
+
if (ctx.sm.isSessionCompacting(active) && !ctx.sm.isSessionRunning(active)) {
|
|
4915
|
+
ctx.sm.cancelCompact(active);
|
|
4916
|
+
return { text: t("stop.done", ctx.lang) };
|
|
4917
|
+
}
|
|
4910
4918
|
if (!ctx.sm.isSessionRunning(active))
|
|
4911
4919
|
return { text: t("stop.already_idle", ctx.lang) };
|
|
4912
4920
|
ctx.sm.stopSession(active).catch(() => {
|
|
@@ -4917,6 +4925,10 @@ function execInterrupt(ctx) {
|
|
|
4917
4925
|
const active = findActiveSessionId(ctx.sm, ctx.userId, ctx.sessionPrefix, ctx.activeOverrides, ctx.accessLevel);
|
|
4918
4926
|
if (!active)
|
|
4919
4927
|
return { text: t("interrupt.no_session", ctx.lang) };
|
|
4928
|
+
if (ctx.sm.isSessionCompacting(active) && !ctx.sm.isSessionRunning(active)) {
|
|
4929
|
+
ctx.sm.cancelCompact(active);
|
|
4930
|
+
return { text: t("interrupt.done", ctx.lang) };
|
|
4931
|
+
}
|
|
4920
4932
|
if (!ctx.sm.isSessionRunning(active))
|
|
4921
4933
|
return { text: t("interrupt.already_idle", ctx.lang) };
|
|
4922
4934
|
ctx.sm.interruptSession(active);
|
|
@@ -5617,6 +5629,14 @@ function writeReplyTo(db, sessionId, to) {
|
|
|
5617
5629
|
function clearReplyTo(db, sessionId) {
|
|
5618
5630
|
db.update(agentSessions).set({ replyTo: null }).where(eq3(agentSessions.id, sessionId)).run();
|
|
5619
5631
|
}
|
|
5632
|
+
function listActiveChildren(db, sessionId) {
|
|
5633
|
+
return db.select({
|
|
5634
|
+
agentName: agentSessions.agentName,
|
|
5635
|
+
description: agentSessions.description,
|
|
5636
|
+
createdAt: agentSessions.createdAt,
|
|
5637
|
+
updatedAt: agentSessions.updatedAt
|
|
5638
|
+
}).from(agentSessions).where(and3(eq3(agentSessions.parentId, sessionId), isNull2(agentSessions.stoppedAt), isNull2(agentSessions.archivedAt))).all();
|
|
5639
|
+
}
|
|
5620
5640
|
async function deliverRelayReport(host, session) {
|
|
5621
5641
|
if (session.parentId !== null)
|
|
5622
5642
|
return;
|
|
@@ -5624,8 +5644,7 @@ async function deliverRelayReport(host, session) {
|
|
|
5624
5644
|
const to = readReplyTo(db, session.id);
|
|
5625
5645
|
if (!to)
|
|
5626
5646
|
return;
|
|
5627
|
-
|
|
5628
|
-
if (activeChildren.length > 0 || session.messageQueue.length > 0)
|
|
5647
|
+
if (listActiveChildren(db, session.id).length > 0 || session.messageQueue.length > 0)
|
|
5629
5648
|
return;
|
|
5630
5649
|
const registry2 = getRelayRegistry();
|
|
5631
5650
|
if (!registry2) {
|
|
@@ -5634,7 +5653,8 @@ async function deliverRelayReport(host, session) {
|
|
|
5634
5653
|
}
|
|
5635
5654
|
let report = session.finalOutput || session.output || "(no output)";
|
|
5636
5655
|
if (session.turnError) {
|
|
5637
|
-
|
|
5656
|
+
const next = session.turnErrorKind === "account" ? "This is a model account / credential / balance / permission problem in the target workspace: retrying or re-sending will fail the same way. Fix its model configuration first (or tell the user to fix it), and do not resume it with relay_send until then." : "Re-send with relay_send to let it resume.";
|
|
5657
|
+
report = `[RELAY TARGET ABORTED: the last turn was terminated by an unrecoverable error, NOT completed. Error: ${session.turnError}. The text below is a partial trace \u2014 do not treat it as a finished result. ${next}]
|
|
5638
5658
|
|
|
5639
5659
|
${report}`;
|
|
5640
5660
|
}
|
|
@@ -6027,6 +6047,14 @@ function claimWorkspaceRuntime(workspaceRoot) {
|
|
|
6027
6047
|
return false;
|
|
6028
6048
|
}
|
|
6029
6049
|
}
|
|
6050
|
+
function releaseWorkspaceRuntime(workspaceRoot) {
|
|
6051
|
+
const lockPath = path21.join(workspaceRoot, ".halo", RUNTIME_LOCK_FILE);
|
|
6052
|
+
try {
|
|
6053
|
+
if (parseInt(fs19.readFileSync(lockPath, "utf-8").trim(), 10) === process.pid)
|
|
6054
|
+
fs19.unlinkSync(lockPath);
|
|
6055
|
+
} catch {
|
|
6056
|
+
}
|
|
6057
|
+
}
|
|
6030
6058
|
var RUNTIME_LOCK_FILE;
|
|
6031
6059
|
var init_workspace_runtime_lock = __esm({
|
|
6032
6060
|
"../server/dist/agents/workspace-runtime-lock.js"() {
|
|
@@ -6750,7 +6778,7 @@ function createSaveSnapshot(state) {
|
|
|
6750
6778
|
tempMsg.contentBlocks = [...state.turnContentBlocks];
|
|
6751
6779
|
return [...state.messageLog, tempMsg];
|
|
6752
6780
|
}
|
|
6753
|
-
function initSubSessionLog(state, taskId, agentId, agentName, text6) {
|
|
6781
|
+
function initSubSessionLog(state, taskId, agentId, agentName, text6, loadHistory2) {
|
|
6754
6782
|
const existing = state.subSessionLogs.get(taskId);
|
|
6755
6783
|
if (existing) {
|
|
6756
6784
|
existing.agentId = agentId;
|
|
@@ -6762,12 +6790,15 @@ function initSubSessionLog(state, taskId, agentId, agentName, text6) {
|
|
|
6762
6790
|
}
|
|
6763
6791
|
return;
|
|
6764
6792
|
}
|
|
6793
|
+
const messageLog = loadHistory2?.(taskId) ?? [];
|
|
6794
|
+
if (text6)
|
|
6795
|
+
messageLog.push({ id: genId(), type: "user", role: "user", content: text6, timestamp: Date.now(), agentName });
|
|
6765
6796
|
state.subSessionLogs.set(taskId, {
|
|
6766
6797
|
agentId,
|
|
6767
6798
|
agentName,
|
|
6768
6799
|
description: text6,
|
|
6769
6800
|
streamingAgent: agentName,
|
|
6770
|
-
messageLog
|
|
6801
|
+
messageLog,
|
|
6771
6802
|
streamBuffer: "",
|
|
6772
6803
|
turnToolCalls: [],
|
|
6773
6804
|
turnContentBlocks: [],
|
|
@@ -6800,12 +6831,12 @@ function buildUsageMsg(event, agentName, taskId, turnId) {
|
|
|
6800
6831
|
turnId
|
|
6801
6832
|
};
|
|
6802
6833
|
}
|
|
6803
|
-
function applyEvent(state, event) {
|
|
6834
|
+
function applyEvent(state, event, loadHistory2) {
|
|
6804
6835
|
const agentName = event.agentName ?? "default";
|
|
6805
6836
|
const taskId = event.taskId;
|
|
6806
|
-
const result = {
|
|
6837
|
+
const result = { taskId };
|
|
6807
6838
|
if (taskId && event.type !== "agent_start" && event.type !== "agent_done" && !state.subSessionLogs.has(taskId)) {
|
|
6808
|
-
initSubSessionLog(state, taskId, event.agentId ?? "", agentName, "");
|
|
6839
|
+
initSubSessionLog(state, taskId, event.agentId ?? "", agentName, "", loadHistory2);
|
|
6809
6840
|
}
|
|
6810
6841
|
switch (event.type) {
|
|
6811
6842
|
// Streaming chunks land in the turn blocks as they arrive; the whole
|
|
@@ -6841,7 +6872,7 @@ function applyEvent(state, event) {
|
|
|
6841
6872
|
});
|
|
6842
6873
|
}
|
|
6843
6874
|
if (taskId)
|
|
6844
|
-
initSubSessionLog(state, taskId, event.agentId ?? "", agentName, event.fullText ?? event.text ?? "");
|
|
6875
|
+
initSubSessionLog(state, taskId, event.agentId ?? "", agentName, event.fullText ?? event.text ?? "", loadHistory2);
|
|
6845
6876
|
break;
|
|
6846
6877
|
case "agent_done":
|
|
6847
6878
|
if (!taskId) {
|
|
@@ -6860,7 +6891,7 @@ function applyEvent(state, event) {
|
|
|
6860
6891
|
const sub = state.subSessionLogs.get(taskId);
|
|
6861
6892
|
if (sub)
|
|
6862
6893
|
flushAssistantMessage(sub);
|
|
6863
|
-
result.
|
|
6894
|
+
result.persist = "flush";
|
|
6864
6895
|
}
|
|
6865
6896
|
break;
|
|
6866
6897
|
case "tool_call": {
|
|
@@ -6878,7 +6909,7 @@ function applyEvent(state, event) {
|
|
|
6878
6909
|
toolInput: event.toolInput
|
|
6879
6910
|
};
|
|
6880
6911
|
addToolCall(getTarget(state, taskId), entry, msg);
|
|
6881
|
-
result.
|
|
6912
|
+
result.persist = "debounce";
|
|
6882
6913
|
break;
|
|
6883
6914
|
}
|
|
6884
6915
|
case "tool_result": {
|
|
@@ -6896,7 +6927,7 @@ function applyEvent(state, event) {
|
|
|
6896
6927
|
toolOutput: event.toolResult,
|
|
6897
6928
|
durationMs: event.durationMs
|
|
6898
6929
|
});
|
|
6899
|
-
result.
|
|
6930
|
+
result.persist = "debounce";
|
|
6900
6931
|
break;
|
|
6901
6932
|
}
|
|
6902
6933
|
case "followup_start":
|
|
@@ -6914,10 +6945,9 @@ function applyEvent(state, event) {
|
|
|
6914
6945
|
if (sub) {
|
|
6915
6946
|
sub.messageLog.push(buildUsageMsg(event, agentName, taskId, sub.currentTurnId));
|
|
6916
6947
|
sub.currentTurnId = randomUUID();
|
|
6917
|
-
result.subSessionSave = taskId;
|
|
6918
6948
|
}
|
|
6919
6949
|
}
|
|
6920
|
-
result.
|
|
6950
|
+
result.persist = "debounce";
|
|
6921
6951
|
break;
|
|
6922
6952
|
}
|
|
6923
6953
|
case "complete":
|
|
@@ -6925,8 +6955,7 @@ function applyEvent(state, event) {
|
|
|
6925
6955
|
state.turnToolCalls = [];
|
|
6926
6956
|
state.turnContentBlocks = [];
|
|
6927
6957
|
state.currentTurnId = randomUUID();
|
|
6928
|
-
result.
|
|
6929
|
-
result.isComplete = true;
|
|
6958
|
+
result.persist = "flush";
|
|
6930
6959
|
break;
|
|
6931
6960
|
case "context": {
|
|
6932
6961
|
const ctxMsg = {
|
|
@@ -6943,12 +6972,11 @@ function applyEvent(state, event) {
|
|
|
6943
6972
|
state.messageLog.push(ctxMsg);
|
|
6944
6973
|
} else {
|
|
6945
6974
|
if (!state.subSessionLogs.has(taskId)) {
|
|
6946
|
-
initSubSessionLog(state, taskId, event.agentId ?? "", agentName, "");
|
|
6975
|
+
initSubSessionLog(state, taskId, event.agentId ?? "", agentName, "", loadHistory2);
|
|
6947
6976
|
}
|
|
6948
6977
|
state.subSessionLogs.get(taskId).messageLog.push(ctxMsg);
|
|
6949
|
-
result.subSessionSave = taskId;
|
|
6950
6978
|
}
|
|
6951
|
-
result.
|
|
6979
|
+
result.persist = "debounce";
|
|
6952
6980
|
break;
|
|
6953
6981
|
}
|
|
6954
6982
|
case "queued_message":
|
|
@@ -6965,8 +6993,7 @@ function applyEvent(state, event) {
|
|
|
6965
6993
|
timestamp: Date.now(),
|
|
6966
6994
|
agentName: "System"
|
|
6967
6995
|
});
|
|
6968
|
-
|
|
6969
|
-
result.subSessionSave = taskId;
|
|
6996
|
+
result.persist = "debounce";
|
|
6970
6997
|
break;
|
|
6971
6998
|
}
|
|
6972
6999
|
case "error":
|
|
@@ -6979,6 +7006,8 @@ function applyEvent(state, event) {
|
|
|
6979
7006
|
agentName,
|
|
6980
7007
|
taskId
|
|
6981
7008
|
});
|
|
7009
|
+
result.taskId = void 0;
|
|
7010
|
+
result.persist = "debounce";
|
|
6982
7011
|
break;
|
|
6983
7012
|
case "compacted":
|
|
6984
7013
|
if (!taskId)
|
|
@@ -6994,9 +7023,7 @@ function applyEvent(state, event) {
|
|
|
6994
7023
|
content: event.text ?? "",
|
|
6995
7024
|
timestamp: Date.now()
|
|
6996
7025
|
});
|
|
6997
|
-
|
|
6998
|
-
result.subSessionSave = taskId;
|
|
6999
|
-
result.shouldSave = true;
|
|
7026
|
+
result.persist = "debounce";
|
|
7000
7027
|
break;
|
|
7001
7028
|
}
|
|
7002
7029
|
}
|
|
@@ -7042,14 +7069,16 @@ var init_session_ui_store = __esm({
|
|
|
7042
7069
|
uiStates = /* @__PURE__ */ new Map();
|
|
7043
7070
|
/** Project path for each root session, needed for disk persistence */
|
|
7044
7071
|
uiStateProjectPaths = /* @__PURE__ */ new Map();
|
|
7045
|
-
/** Debounced persist timers — prevents flooding disk with writes during rapid
|
|
7072
|
+
/** Debounced persist timers — prevents flooding disk with writes during rapid
|
|
7073
|
+
* tool loops. Keyed by root id for a root's log, by taskId (`root>…`) for a
|
|
7074
|
+
* sub-session's own file. */
|
|
7046
7075
|
persistTimers = /* @__PURE__ */ new Map();
|
|
7047
7076
|
/** Last activity (event reduced / view built) per uiStates key — the idle
|
|
7048
7077
|
* sweep's eviction clock. Invariant: every uiStates key has an entry. */
|
|
7049
7078
|
uiStateTouched = /* @__PURE__ */ new Map();
|
|
7050
7079
|
/** Roots whose in-memory UIState holds local mutations (event reduced in,
|
|
7051
7080
|
* notification appended, log replaced) **not yet persisted** — marked on
|
|
7052
|
-
* every mutation, cleared once
|
|
7081
|
+
* every mutation, cleared once persistLog lands the snapshot. A state
|
|
7053
7082
|
* not in this set adds nothing to what's on disk: either it's a pure disk
|
|
7054
7083
|
* seed (built to *view* a session another process may be driving — a cron
|
|
7055
7084
|
* `halo cli` child shares the workspace's session files and keeps appending
|
|
@@ -7147,51 +7176,58 @@ var init_session_ui_store = __esm({
|
|
|
7147
7176
|
this.eventHandler?.(event);
|
|
7148
7177
|
}
|
|
7149
7178
|
/**
|
|
7150
|
-
* Fold an event into the root session's UIState and persist
|
|
7151
|
-
*
|
|
7152
|
-
* needed.
|
|
7179
|
+
* Fold an event into the root session's UIState and persist the log it
|
|
7180
|
+
* changed — the root's or a sub-session's, same rule (see applyEvent).
|
|
7181
|
+
* Builds the state lazily from disk if needed.
|
|
7153
7182
|
*/
|
|
7154
7183
|
reduceIntoUIState(rootId, event) {
|
|
7155
7184
|
try {
|
|
7156
7185
|
const state = this.ensureUIState(rootId);
|
|
7157
|
-
this.
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
if (result.isComplete) {
|
|
7168
|
-
this.flushPersist(rootId, state);
|
|
7169
|
-
} else {
|
|
7170
|
-
this.debouncedPersist(rootId, state);
|
|
7171
|
-
}
|
|
7172
|
-
}
|
|
7186
|
+
const { taskId, persist } = applyEvent(state, event, this.loadSubHistory);
|
|
7187
|
+
if (!taskId)
|
|
7188
|
+
this.uiStateDirty.add(rootId);
|
|
7189
|
+
const id = taskId ?? rootId;
|
|
7190
|
+
if (persist === "flush")
|
|
7191
|
+
this.flushPersist(id, state);
|
|
7192
|
+
else if (persist === "debounce")
|
|
7193
|
+
this.debouncedPersist(id, state);
|
|
7194
|
+
if (taskId && event.type === "agent_done")
|
|
7195
|
+
state.subSessionLogs.delete(taskId);
|
|
7173
7196
|
} catch (err) {
|
|
7174
7197
|
console.error(`[SessionUIStore] reduceIntoUIState failed for ${rootId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7175
7198
|
}
|
|
7176
7199
|
}
|
|
7177
|
-
/**
|
|
7178
|
-
|
|
7179
|
-
|
|
7200
|
+
/** A new sub-session log starts from the sub's own file, the way
|
|
7201
|
+
* ensureUIState seeds a root — so persistLog can overwrite it whole. */
|
|
7202
|
+
loadSubHistory = (taskId) => {
|
|
7203
|
+
const projectPath = this.uiStateProjectPaths.get(this.findRootSessionId(taskId)) ?? this.host.workspaceRoot;
|
|
7204
|
+
return loadSessionMessages(taskId, projectPath, this.resolveAgentId(taskId));
|
|
7205
|
+
};
|
|
7206
|
+
/** Debounced persist — coalesces rapid tool_call/tool_result saves into one
|
|
7207
|
+
* write. `id` is the log's own session id (root, or a sub's `root>…`
|
|
7208
|
+
* taskId), which keys the timer and names the file. */
|
|
7209
|
+
debouncedPersist(id, state) {
|
|
7210
|
+
const existing = this.persistTimers.get(id);
|
|
7180
7211
|
if (existing)
|
|
7181
7212
|
clearTimeout(existing);
|
|
7182
|
-
this.persistTimers.set(
|
|
7183
|
-
this.persistTimers.delete(
|
|
7184
|
-
this.
|
|
7213
|
+
this.persistTimers.set(id, setTimeout(() => {
|
|
7214
|
+
this.persistTimers.delete(id);
|
|
7215
|
+
this.persistLog(id, state);
|
|
7185
7216
|
}, 500));
|
|
7186
7217
|
}
|
|
7187
|
-
/** Flush pending persist immediately (
|
|
7188
|
-
flushPersist(
|
|
7189
|
-
const existing = this.persistTimers.get(
|
|
7218
|
+
/** Flush pending persist immediately (turn end, interrupt, release, drop). */
|
|
7219
|
+
flushPersist(id, state) {
|
|
7220
|
+
const existing = this.persistTimers.get(id);
|
|
7190
7221
|
if (existing) {
|
|
7191
7222
|
clearTimeout(existing);
|
|
7192
|
-
this.persistTimers.delete(
|
|
7223
|
+
this.persistTimers.delete(id);
|
|
7193
7224
|
}
|
|
7194
|
-
this.
|
|
7225
|
+
this.persistLog(id, state);
|
|
7226
|
+
}
|
|
7227
|
+
/** Sub-sessions of `rootId` with a debounced write still pending. */
|
|
7228
|
+
pendingSubPersists(rootId) {
|
|
7229
|
+
const prefix = `${rootId}>`;
|
|
7230
|
+
return [...this.persistTimers.keys()].filter((key) => key.startsWith(prefix));
|
|
7195
7231
|
}
|
|
7196
7232
|
/**
|
|
7197
7233
|
* Ensure UIState for a root session. On first access for a session that
|
|
@@ -7224,17 +7260,40 @@ var init_session_ui_store = __esm({
|
|
|
7224
7260
|
this.uiStates.set(rootId, state);
|
|
7225
7261
|
return state;
|
|
7226
7262
|
}
|
|
7227
|
-
/** Persist UI
|
|
7228
|
-
* `
|
|
7229
|
-
*
|
|
7230
|
-
*
|
|
7231
|
-
*
|
|
7232
|
-
|
|
7263
|
+
/** Persist one UI log to its own file, overwriting it whole: the root's
|
|
7264
|
+
* (`id` = root id: messages + token counts) or a sub-session's (`id` =
|
|
7265
|
+
* taskId: `state.subSessionLogs[id]`, which holds the sub's full history —
|
|
7266
|
+
* see loadSubHistory — plus the in-flight turn as a temp assistant, the
|
|
7267
|
+
* same snapshot the root writes). `archive` is passed ONLY by
|
|
7268
|
+
* archiveOldMessages (the commit step of an archive write): `count` is the
|
|
7269
|
+
* new segment count and `userDelta` the main user turns leaving the active
|
|
7270
|
+
* file. Every other call leaves both on-disk values alone. */
|
|
7271
|
+
persistLog(id, state, archive) {
|
|
7272
|
+
const rootId = this.findRootSessionId(id);
|
|
7273
|
+
const sub = id === rootId ? void 0 : state.subSessionLogs.get(id);
|
|
7274
|
+
if (id !== rootId && (!sub || this.host.isSessionDeleted(rootId)))
|
|
7275
|
+
return;
|
|
7233
7276
|
try {
|
|
7234
7277
|
const projectPath = this.uiStateProjectPaths.get(rootId) ?? this.host.workspaceRoot;
|
|
7235
|
-
const snapshot2 = createSaveSnapshot(state);
|
|
7278
|
+
const snapshot2 = createSaveSnapshot(sub ?? state);
|
|
7236
7279
|
if (snapshot2.length === 0)
|
|
7237
7280
|
return;
|
|
7281
|
+
if (sub) {
|
|
7282
|
+
const parts = id.split(">");
|
|
7283
|
+
this.host.persistSessionFile({
|
|
7284
|
+
sessionId: id,
|
|
7285
|
+
projectPath,
|
|
7286
|
+
messages: snapshot2,
|
|
7287
|
+
contextTokens: 0,
|
|
7288
|
+
outputTokens: 0,
|
|
7289
|
+
agentId: this.resolveAgentId(id),
|
|
7290
|
+
agentName: sub.agentName,
|
|
7291
|
+
source: "delegated",
|
|
7292
|
+
description: sub.description,
|
|
7293
|
+
parentSessionId: parts.slice(0, -1).join(">")
|
|
7294
|
+
});
|
|
7295
|
+
return;
|
|
7296
|
+
}
|
|
7238
7297
|
const inMem = this.host.getSession(rootId);
|
|
7239
7298
|
const row = inMem ? null : this.db.select().from(agentSessions).where(eq6(agentSessions.id, rootId)).get();
|
|
7240
7299
|
this.host.persistSessionFile({
|
|
@@ -7253,7 +7312,7 @@ var init_session_ui_store = __esm({
|
|
|
7253
7312
|
});
|
|
7254
7313
|
this.uiStateDirty.delete(rootId);
|
|
7255
7314
|
} catch (err) {
|
|
7256
|
-
console.error(`[SessionUIStore]
|
|
7315
|
+
console.error(`[SessionUIStore] persistLog failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7257
7316
|
}
|
|
7258
7317
|
}
|
|
7259
7318
|
/**
|
|
@@ -7300,7 +7359,7 @@ var init_session_ui_store = __esm({
|
|
|
7300
7359
|
const n = readArchiveCount(dir, seg) + 1;
|
|
7301
7360
|
writeArchiveSegment(dir, seg, n, older);
|
|
7302
7361
|
state.messageLog = kept;
|
|
7303
|
-
this.
|
|
7362
|
+
this.persistLog(rootId, state, { count: n, userDelta: countMainUserMessages(older) });
|
|
7304
7363
|
if (readArchiveCount(dir, seg) < n) {
|
|
7305
7364
|
state.messageLog = [...older, ...kept];
|
|
7306
7365
|
console.warn(`[SessionUIStore] archive commit for ${rootId} segment ${n} did not land \u2014 rolled back`);
|
|
@@ -7313,37 +7372,6 @@ var init_session_ui_store = __esm({
|
|
|
7313
7372
|
return 0;
|
|
7314
7373
|
}
|
|
7315
7374
|
}
|
|
7316
|
-
/** Persist a sub-session's UI log to its own disk file */
|
|
7317
|
-
persistSubSession(state, rootId, taskId) {
|
|
7318
|
-
if (this.host.isSessionDeleted(rootId))
|
|
7319
|
-
return;
|
|
7320
|
-
const sub = state.subSessionLogs.get(taskId);
|
|
7321
|
-
if (!sub || sub.messageLog.length === 0)
|
|
7322
|
-
return;
|
|
7323
|
-
try {
|
|
7324
|
-
const projectPath = this.uiStateProjectPaths.get(rootId) ?? this.host.workspaceRoot;
|
|
7325
|
-
const agentId = this.resolveAgentId(taskId);
|
|
7326
|
-
const existing = loadSessionMessages(taskId, projectPath, agentId);
|
|
7327
|
-
const seen = new Set(existing.map((m) => m.id).filter(Boolean));
|
|
7328
|
-
const merged = [...existing, ...sub.messageLog.filter((m) => !m.id || !seen.has(m.id))];
|
|
7329
|
-
const parts = taskId.split(">");
|
|
7330
|
-
const directParentId = parts.length > 1 ? parts.slice(0, -1).join(">") : rootId;
|
|
7331
|
-
this.host.persistSessionFile({
|
|
7332
|
-
sessionId: taskId,
|
|
7333
|
-
projectPath,
|
|
7334
|
-
messages: merged,
|
|
7335
|
-
contextTokens: 0,
|
|
7336
|
-
outputTokens: 0,
|
|
7337
|
-
agentId,
|
|
7338
|
-
agentName: sub.agentName,
|
|
7339
|
-
source: "delegated",
|
|
7340
|
-
description: sub.description,
|
|
7341
|
-
parentSessionId: directParentId
|
|
7342
|
-
});
|
|
7343
|
-
} catch (err) {
|
|
7344
|
-
console.error(`[SessionUIStore] persistSubSession failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7345
|
-
}
|
|
7346
|
-
}
|
|
7347
7375
|
/** Process-local cache of taskId → authoritative agentId, so the hot persist
|
|
7348
7376
|
* path doesn't hit the db on every sub-session write. */
|
|
7349
7377
|
agentIdCache = /* @__PURE__ */ new Map();
|
|
@@ -7412,7 +7440,7 @@ var init_session_ui_store = __esm({
|
|
|
7412
7440
|
agentName
|
|
7413
7441
|
});
|
|
7414
7442
|
this.uiStateDirty.add(rootId);
|
|
7415
|
-
this.
|
|
7443
|
+
this.persistLog(rootId, state);
|
|
7416
7444
|
}
|
|
7417
7445
|
/**
|
|
7418
7446
|
* Replace the message log entirely (used for compact, which rewrites the
|
|
@@ -7426,7 +7454,7 @@ var init_session_ui_store = __esm({
|
|
|
7426
7454
|
state.turnToolCalls = [];
|
|
7427
7455
|
state.turnContentBlocks = [];
|
|
7428
7456
|
this.uiStateDirty.add(rootId);
|
|
7429
|
-
this.
|
|
7457
|
+
this.persistLog(rootId, state);
|
|
7430
7458
|
}
|
|
7431
7459
|
/**
|
|
7432
7460
|
* Drop the in-memory UIState for a session (e.g. after stopSession / clear).
|
|
@@ -7442,8 +7470,12 @@ var init_session_ui_store = __esm({
|
|
|
7442
7470
|
*/
|
|
7443
7471
|
dropUIState(sessionId) {
|
|
7444
7472
|
const state = this.uiStates.get(sessionId);
|
|
7445
|
-
if (state && this.
|
|
7446
|
-
this.
|
|
7473
|
+
if (state && sessionId === this.findRootSessionId(sessionId)) {
|
|
7474
|
+
if (this.persistTimers.has(sessionId))
|
|
7475
|
+
this.flushPersist(sessionId, state);
|
|
7476
|
+
for (const taskId of this.pendingSubPersists(sessionId))
|
|
7477
|
+
this.flushPersist(taskId, state);
|
|
7478
|
+
}
|
|
7447
7479
|
this.uiStates.delete(sessionId);
|
|
7448
7480
|
this.uiStateProjectPaths.delete(sessionId);
|
|
7449
7481
|
this.uiStateTouched.delete(sessionId);
|
|
@@ -7477,25 +7509,45 @@ var init_session_ui_store = __esm({
|
|
|
7477
7509
|
}
|
|
7478
7510
|
// ── Operations the manager's session-lifecycle methods delegate here ──
|
|
7479
7511
|
// These exist so the manager never reaches into the maps above directly.
|
|
7480
|
-
/** Flush
|
|
7481
|
-
*
|
|
7512
|
+
/** Flush the tree's unpersisted UI logs before release, so an interrupted
|
|
7513
|
+
* run's last UI batch lands on disk. No-op if not loaded. The root's file
|
|
7514
|
+
* is rewritten only when its log holds changes (dirty): a root turn end
|
|
7515
|
+
* already flushed at `complete`, and a sub's release must not rewrite the
|
|
7516
|
+
* root's (large) file for nothing. */
|
|
7482
7517
|
flushSession(sessionId) {
|
|
7483
7518
|
const rootId = this.findRootSessionId(sessionId);
|
|
7484
7519
|
const state = this.uiStates.get(rootId);
|
|
7485
|
-
if (state)
|
|
7520
|
+
if (!state)
|
|
7521
|
+
return;
|
|
7522
|
+
if (this.uiStateDirty.has(rootId))
|
|
7486
7523
|
this.flushPersist(rootId, state);
|
|
7524
|
+
for (const taskId of this.pendingSubPersists(rootId))
|
|
7525
|
+
this.flushPersist(taskId, state);
|
|
7487
7526
|
}
|
|
7488
|
-
/**
|
|
7489
|
-
*
|
|
7490
|
-
*
|
|
7491
|
-
*
|
|
7492
|
-
|
|
7493
|
-
|
|
7527
|
+
/** Server shutdown: land every loaded tree's unpersisted UI logs — a dirty
|
|
7528
|
+
* root plus every live sub log (pending timers would never fire, and a
|
|
7529
|
+
* sub's streamed text since its last write rides no timer at all). Same
|
|
7530
|
+
* writes flushSession makes, minus the "pending timer" filter on subs. */
|
|
7531
|
+
flushAll() {
|
|
7532
|
+
for (const [rootId, state] of this.uiStates) {
|
|
7533
|
+
if (rootId !== this.findRootSessionId(rootId))
|
|
7534
|
+
continue;
|
|
7535
|
+
if (this.uiStateDirty.has(rootId))
|
|
7536
|
+
this.flushPersist(rootId, state);
|
|
7537
|
+
for (const taskId of state.subSessionLogs.keys())
|
|
7538
|
+
this.flushPersist(taskId, state);
|
|
7539
|
+
}
|
|
7540
|
+
}
|
|
7541
|
+
/** Flush a sub-session's UI log to its own file now. Called after the
|
|
7542
|
+
* synthetic interrupted tool_results on interrupt/stop: those only arm the
|
|
7543
|
+
* 500ms debounce, and the interrupt is a turn boundary — the marker should
|
|
7544
|
+
* land immediately rather than ride a timer. No-op if the root state isn't
|
|
7545
|
+
* loaded. */
|
|
7494
7546
|
flushSubSession(taskId) {
|
|
7495
7547
|
const rootId = this.findRootSessionId(taskId);
|
|
7496
7548
|
const state = this.uiStates.get(rootId);
|
|
7497
7549
|
if (state)
|
|
7498
|
-
this.
|
|
7550
|
+
this.flushPersist(taskId, state);
|
|
7499
7551
|
}
|
|
7500
7552
|
/**
|
|
7501
7553
|
* Prepare a session's UIState for a view request. When the session isn't
|
|
@@ -7504,9 +7556,10 @@ var init_session_ui_store = __esm({
|
|
|
7504
7556
|
* session evolving elsewhere would freeze on the first snapshot.
|
|
7505
7557
|
*/
|
|
7506
7558
|
prepareForView(sessionId, selfDriven) {
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7559
|
+
const isRoot = sessionId === this.findRootSessionId(sessionId);
|
|
7560
|
+
const liveTree = isRoot && this.host.hasActiveWorkInTree(sessionId);
|
|
7561
|
+
if (!isRoot || !selfDriven && !liveTree) {
|
|
7562
|
+
this.dropUIState(sessionId);
|
|
7510
7563
|
}
|
|
7511
7564
|
this.uiStateProjectPaths.set(sessionId, this.host.workspaceRoot);
|
|
7512
7565
|
return this.ensureUIState(sessionId);
|
|
@@ -7518,10 +7571,12 @@ var init_session_ui_store = __esm({
|
|
|
7518
7571
|
this.uiStateProjectPaths.delete(id);
|
|
7519
7572
|
this.uiStateTouched.delete(id);
|
|
7520
7573
|
this.uiStateDirty.delete(id);
|
|
7521
|
-
const
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7574
|
+
for (const key of [id, ...this.pendingSubPersists(id)]) {
|
|
7575
|
+
const timer = this.persistTimers.get(key);
|
|
7576
|
+
if (timer) {
|
|
7577
|
+
clearTimeout(timer);
|
|
7578
|
+
this.persistTimers.delete(key);
|
|
7579
|
+
}
|
|
7525
7580
|
}
|
|
7526
7581
|
this.eventListeners.delete(id);
|
|
7527
7582
|
this.agentIdCache.delete(id);
|
|
@@ -7762,7 +7817,7 @@ var init_session_query_store = __esm({
|
|
|
7762
7817
|
});
|
|
7763
7818
|
|
|
7764
7819
|
// ../server/dist/agents/sse.js
|
|
7765
|
-
async function* readSseJson(body) {
|
|
7820
|
+
async function* readSseJson(body, onChunk) {
|
|
7766
7821
|
const reader = body.getReader();
|
|
7767
7822
|
const decoder = new TextDecoder();
|
|
7768
7823
|
let buffer = "";
|
|
@@ -7771,6 +7826,7 @@ async function* readSseJson(body) {
|
|
|
7771
7826
|
const { done, value } = await reader.read();
|
|
7772
7827
|
if (done)
|
|
7773
7828
|
break;
|
|
7829
|
+
onChunk?.();
|
|
7774
7830
|
buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
|
|
7775
7831
|
let sep2;
|
|
7776
7832
|
while ((sep2 = buffer.indexOf("\n\n")) !== -1) {
|
|
@@ -7828,7 +7884,7 @@ async function fetchAnthropicStream(opts) {
|
|
|
7828
7884
|
if (!res.body)
|
|
7829
7885
|
throw new Error(`[${tag}] empty response body`);
|
|
7830
7886
|
const acc = new AnthropicStreamAccumulator(startTime, opts.onDelta);
|
|
7831
|
-
for await (const ev of readSseJson(res.body)) {
|
|
7887
|
+
for await (const ev of readSseJson(res.body, () => opts.onDelta?.(ACTIVITY_DELTA))) {
|
|
7832
7888
|
if (ev.type === "error") {
|
|
7833
7889
|
throw new Error(`[${tag}] ${SSE_ERROR_STATUS[ev.error?.type ?? ""] ?? "?"} ${ev.error?.type ?? "?"}: ${ev.error?.message ?? ""}`);
|
|
7834
7890
|
}
|
|
@@ -7842,6 +7898,7 @@ var AnthropicStreamAccumulator, SSE_ERROR_STATUS;
|
|
|
7842
7898
|
var init_anthropic_stream = __esm({
|
|
7843
7899
|
"../server/dist/agents/anthropic-stream.js"() {
|
|
7844
7900
|
"use strict";
|
|
7901
|
+
init_agent_loop();
|
|
7845
7902
|
init_sse();
|
|
7846
7903
|
AnthropicStreamAccumulator = class {
|
|
7847
7904
|
startTime;
|
|
@@ -8041,6 +8098,7 @@ var init_bedrock_agent = __esm({
|
|
|
8041
8098
|
const acc = new AnthropicStreamAccumulator(startTime, onDelta);
|
|
8042
8099
|
const decoder = new TextDecoder();
|
|
8043
8100
|
for await (const chunk of response.body ?? []) {
|
|
8101
|
+
onDelta?.(ACTIVITY_DELTA);
|
|
8044
8102
|
if (chunk.chunk?.bytes)
|
|
8045
8103
|
acc.push(JSON.parse(decoder.decode(chunk.chunk.bytes)));
|
|
8046
8104
|
}
|
|
@@ -8129,7 +8187,7 @@ async function fetchChatCompletionStream(opts) {
|
|
|
8129
8187
|
if (!res.body)
|
|
8130
8188
|
throw new Error(`[${tag}] empty response body`);
|
|
8131
8189
|
const acc = new ChatCompletionAccumulator(startTime, opts.onDelta);
|
|
8132
|
-
for await (const chunk of readSseJson(res.body)) {
|
|
8190
|
+
for await (const chunk of readSseJson(res.body, () => opts.onDelta?.(ACTIVITY_DELTA))) {
|
|
8133
8191
|
if (chunk.error) {
|
|
8134
8192
|
throw new Error(`[${tag}] API error in stream: ${JSON.stringify(chunk.error).slice(0, 300)}`);
|
|
8135
8193
|
}
|
|
@@ -8143,6 +8201,7 @@ var ChatCompletionAccumulator;
|
|
|
8143
8201
|
var init_openai_chat_stream = __esm({
|
|
8144
8202
|
"../server/dist/agents/openai-chat-stream.js"() {
|
|
8145
8203
|
"use strict";
|
|
8204
|
+
init_agent_loop();
|
|
8146
8205
|
init_sse();
|
|
8147
8206
|
ChatCompletionAccumulator = class {
|
|
8148
8207
|
startTime;
|
|
@@ -9518,7 +9577,7 @@ async function readResponsesStream(res, onDelta, startTime, signal) {
|
|
|
9518
9577
|
throw new Error("[MantleAgent] empty response body");
|
|
9519
9578
|
let final;
|
|
9520
9579
|
let firstDeltaAt;
|
|
9521
|
-
for await (const ev of readSseJson(res.body)) {
|
|
9580
|
+
for await (const ev of readSseJson(res.body, () => onDelta?.(ACTIVITY_DELTA))) {
|
|
9522
9581
|
switch (ev.type) {
|
|
9523
9582
|
case "response.output_text.delta":
|
|
9524
9583
|
firstDeltaAt ??= Date.now();
|
|
@@ -12310,7 +12369,7 @@ async function initObservability() {
|
|
|
12310
12369
|
return true;
|
|
12311
12370
|
}
|
|
12312
12371
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
12313
|
-
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.5.
|
|
12372
|
+
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.5.2-alpha");
|
|
12314
12373
|
console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, capture_content=${config.observability.captureContent})`);
|
|
12315
12374
|
return true;
|
|
12316
12375
|
}
|
|
@@ -12676,6 +12735,11 @@ function generateSessionId() {
|
|
|
12676
12735
|
const rand2 = Math.random().toString(36).slice(2, 8);
|
|
12677
12736
|
return `sid_${ts}_${rand2}`;
|
|
12678
12737
|
}
|
|
12738
|
+
function interimDoor(parentId, batch) {
|
|
12739
|
+
if (parentId !== null)
|
|
12740
|
+
return batch.some((q) => q.sourceSessionId === parentId) ? "parent" : null;
|
|
12741
|
+
return batch.some((q) => q.sourceSessionId === void 0 && q.text.includes(RELAY_CHANNEL_PREFIX)) ? "relay" : null;
|
|
12742
|
+
}
|
|
12679
12743
|
var CONTINUE_TASK_KICK, SessionManager;
|
|
12680
12744
|
var init_session_manager = __esm({
|
|
12681
12745
|
"../server/dist/agents/session-manager.js"() {
|
|
@@ -12962,6 +13026,7 @@ var init_session_manager = __esm({
|
|
|
12962
13026
|
streamedThisCall: false,
|
|
12963
13027
|
finalOutput: "",
|
|
12964
13028
|
turnError: null,
|
|
13029
|
+
turnErrorKind: null,
|
|
12965
13030
|
promise: null,
|
|
12966
13031
|
abortController: null,
|
|
12967
13032
|
messageQueue: [],
|
|
@@ -12976,6 +13041,7 @@ var init_session_manager = __esm({
|
|
|
12976
13041
|
isCompacting: false,
|
|
12977
13042
|
compactAbortController: null,
|
|
12978
13043
|
compactedThisTurn: false,
|
|
13044
|
+
foldAfterCompact: null,
|
|
12979
13045
|
systemPrompt,
|
|
12980
13046
|
thinkingEffort,
|
|
12981
13047
|
workingDir,
|
|
@@ -13289,6 +13355,10 @@ var init_session_manager = __esm({
|
|
|
13289
13355
|
getUIState(rootSessionId) {
|
|
13290
13356
|
return this.uiStore.getUIState(rootSessionId);
|
|
13291
13357
|
}
|
|
13358
|
+
/** Server shutdown: land every loaded tree's unpersisted UI logs. */
|
|
13359
|
+
flushAll() {
|
|
13360
|
+
this.uiStore.flushAll();
|
|
13361
|
+
}
|
|
13292
13362
|
appendUserMessage(sessionId, text6, opts) {
|
|
13293
13363
|
this.uiStore.appendUserMessage(sessionId, text6, opts);
|
|
13294
13364
|
}
|
|
@@ -13437,6 +13507,7 @@ var init_session_manager = __esm({
|
|
|
13437
13507
|
session.lastActivityAt = null;
|
|
13438
13508
|
session.finalOutput = "";
|
|
13439
13509
|
session.turnError = null;
|
|
13510
|
+
session.turnErrorKind = null;
|
|
13440
13511
|
const stamp = `[${(/* @__PURE__ */ new Date()).toISOString()}]`;
|
|
13441
13512
|
const stamped = (text6) => text6 ? `${stamp} ${text6}` : stamp;
|
|
13442
13513
|
if (typeof message === "string") {
|
|
@@ -13508,6 +13579,7 @@ var init_session_manager = __esm({
|
|
|
13508
13579
|
console.error(`[SessionManager] Session ${session.id} account error: ${msg}`);
|
|
13509
13580
|
this.emitEvent(session.id, { type: "error", error: msg, agentName: session.agentName, taskId: session.parentId ? session.id : void 0 });
|
|
13510
13581
|
session.turnError = msg;
|
|
13582
|
+
session.turnErrorKind = kind;
|
|
13511
13583
|
resultText = `Error: ${msg}`;
|
|
13512
13584
|
break;
|
|
13513
13585
|
}
|
|
@@ -13583,6 +13655,7 @@ var init_session_manager = __esm({
|
|
|
13583
13655
|
console.error(`[SessionManager] Session ${session.id} error (attempt ${attempt + 1}/${maxRetries})${errDetail ? ` [${errDetail}]` : ""}: ${msg}`);
|
|
13584
13656
|
this.emitEvent(session.id, { type: "error", error: msg, agentName: session.agentName, taskId: session.parentId ? session.id : void 0 });
|
|
13585
13657
|
session.turnError = errName ? `${errName}: ${msg}` : msg;
|
|
13658
|
+
session.turnErrorKind = kind;
|
|
13586
13659
|
resultText = `Error: ${errName ? `${errName}: ` : ""}${msg}`;
|
|
13587
13660
|
break;
|
|
13588
13661
|
}
|
|
@@ -13605,6 +13678,7 @@ var init_session_manager = __esm({
|
|
|
13605
13678
|
throw new Error(`Session ${sessionId} not found`);
|
|
13606
13679
|
const hasFirst = typeof message === "string" ? message !== "" : message.length > 0;
|
|
13607
13680
|
console.debug(`[SessionManager] runSession ${sessionId} (agent: ${session.agentId}, parent: ${session.parentId ?? "none"}) \u2014 hasFirst: ${hasFirst}`);
|
|
13681
|
+
const lastTurn = { batch: [], waitingOnChildren: false };
|
|
13608
13682
|
const runFn = async () => {
|
|
13609
13683
|
let result = "";
|
|
13610
13684
|
if (hasFirst) {
|
|
@@ -13613,14 +13687,16 @@ var init_session_manager = __esm({
|
|
|
13613
13687
|
session.interruptRequested = false;
|
|
13614
13688
|
session.resumedAfterInterrupt = false;
|
|
13615
13689
|
session.selfKick = false;
|
|
13690
|
+
lastTurn.batch = [{ text: rawMessageText({ role: "user", content: message }) }];
|
|
13691
|
+
lastTurn.waitingOnChildren = interimDoor(session.parentId, lastTurn.batch) !== null && listActiveChildren(this.db, session.id).length > 0;
|
|
13616
13692
|
result = await this.runAgentTurn(session, message);
|
|
13617
13693
|
console.debug(`[SessionManager] runSession ${sessionId} first turn done \u2014 result: ${result.slice(0, 150)}`);
|
|
13618
13694
|
if (session.messageQueue.length > 0 && session.finalOutput !== "" && !session.turnError) {
|
|
13619
|
-
this.sendOpeningInterim(session);
|
|
13695
|
+
this.sendOpeningInterim(session, lastTurn.batch);
|
|
13620
13696
|
}
|
|
13621
13697
|
}
|
|
13622
13698
|
if (session.messageQueue.length > 0) {
|
|
13623
|
-
await this.drainQueue(session);
|
|
13699
|
+
await this.drainQueue(session, lastTurn);
|
|
13624
13700
|
result = session.output || result;
|
|
13625
13701
|
}
|
|
13626
13702
|
return result;
|
|
@@ -13635,6 +13711,7 @@ var init_session_manager = __esm({
|
|
|
13635
13711
|
if (session.parentId === null) {
|
|
13636
13712
|
this.emitEvent(session.id, { type: "complete" });
|
|
13637
13713
|
}
|
|
13714
|
+
this.sendDeferredInterim(session, lastTurn);
|
|
13638
13715
|
this.tryReportToParent(session);
|
|
13639
13716
|
deliverGoalRound(this, session).catch((err) => {
|
|
13640
13717
|
console.error(`[GoalMode] deliverGoalRound failed for ${session.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -13668,7 +13745,7 @@ var init_session_manager = __esm({
|
|
|
13668
13745
|
tryReportToParent(session) {
|
|
13669
13746
|
if (!session.parentId)
|
|
13670
13747
|
return;
|
|
13671
|
-
const activeChildren = this.db
|
|
13748
|
+
const activeChildren = listActiveChildren(this.db, session.id);
|
|
13672
13749
|
if (activeChildren.length > 0) {
|
|
13673
13750
|
console.debug(`[SessionManager] tryReportToParent: ${session.id} has ${activeChildren.length} active children, skipping`);
|
|
13674
13751
|
return;
|
|
@@ -13676,7 +13753,8 @@ var init_session_manager = __esm({
|
|
|
13676
13753
|
this.db.update(agentSessions).set({ stoppedAt: Date.now() }).where(eq9(agentSessions.id, session.id)).run();
|
|
13677
13754
|
let result = session.finalOutput || session.output || "(no output)";
|
|
13678
13755
|
if (session.turnError) {
|
|
13679
|
-
|
|
13756
|
+
const next = session.turnErrorKind === "account" ? `This is a model account / credential / balance / permission problem: retrying or re-dispatching will fail the same way. Fix the model configuration first (or tell the user to fix it), and do not resume it with query_session("${session.id}", ...) until then.` : `Re-dispatch with query_session("${session.id}", ...) to let it resume.`;
|
|
13757
|
+
result = `[SUB-AGENT ABORTED: the last turn was terminated by an unrecoverable error, NOT completed. Error: ${session.turnError}. The text below is a partial trace of the aborted turn \u2014 do not treat it as a finished result. ${next}]
|
|
13680
13758
|
|
|
13681
13759
|
${result}`;
|
|
13682
13760
|
}
|
|
@@ -13727,12 +13805,7 @@ ${result}`;
|
|
|
13727
13805
|
return { llm: "", ui: "" };
|
|
13728
13806
|
const now = Date.now();
|
|
13729
13807
|
const nowIso = new Date(now).toISOString();
|
|
13730
|
-
const stillRunning = this.db.
|
|
13731
|
-
agentName: agentSessions.agentName,
|
|
13732
|
-
description: agentSessions.description,
|
|
13733
|
-
createdAt: agentSessions.createdAt,
|
|
13734
|
-
updatedAt: agentSessions.updatedAt
|
|
13735
|
-
}).from(agentSessions).where(and6(eq9(agentSessions.parentId, session.id), isNull4(agentSessions.stoppedAt), isNull4(agentSessions.archivedAt))).all();
|
|
13808
|
+
const stillRunning = listActiveChildren(this.db, session.id);
|
|
13736
13809
|
if (stillRunning.length === 0 && session.messageQueue.length === 0) {
|
|
13737
13810
|
const done = "All sub-agents you dispatched have completed. This is the final report \u2014 you may now consolidate and wrap up.";
|
|
13738
13811
|
return { llm: `
|
|
@@ -13762,7 +13835,7 @@ Still running:
|
|
|
13762
13835
|
* entries were traced by the channel before sendUserMessage — so drain does
|
|
13763
13836
|
* NOT re-emit a `user` event. It DOES emit one `queued_message` per merged
|
|
13764
13837
|
* batch (root only) to split the assistant bubble for the new turn. */
|
|
13765
|
-
async drainQueue(session) {
|
|
13838
|
+
async drainQueue(session, lastTurn) {
|
|
13766
13839
|
while (session.messageQueue.length > 0) {
|
|
13767
13840
|
const batch = session.messageQueue.splice(0);
|
|
13768
13841
|
session.resumedAfterInterrupt = session.interruptRequested;
|
|
@@ -13784,6 +13857,8 @@ ${q.text}` : q.text).join("\n\n");
|
|
|
13784
13857
|
}
|
|
13785
13858
|
const kickNote = kickFlagReset ? "\n\n[System] The continue_task flag you set last turn was reset by this interrupt. If the interrupted task is still unfinished after you answer, call continue_task again." : "";
|
|
13786
13859
|
const input = this.buildInput(merged + suffix.llm + kickNote, images.length > 0 ? images : void 0, session.supportsImage);
|
|
13860
|
+
lastTurn.batch = batch;
|
|
13861
|
+
lastTurn.waitingOnChildren = interimDoor(session.parentId, batch) !== null && listActiveChildren(this.db, session.id).length > 0;
|
|
13787
13862
|
try {
|
|
13788
13863
|
await this.runAgentTurn(session, input);
|
|
13789
13864
|
} catch (err) {
|
|
@@ -13809,7 +13884,8 @@ ${q.text}` : q.text).join("\n\n");
|
|
|
13809
13884
|
}
|
|
13810
13885
|
}
|
|
13811
13886
|
}
|
|
13812
|
-
/** Interim report for a drained turn
|
|
13887
|
+
/** Interim report for a drained turn (sendDeferredInterim sends through here
|
|
13888
|
+
* too). A turn followed by another turn (kick /
|
|
13813
13889
|
* queued message) is otherwise lost to whoever asked: the end-of-run reports
|
|
13814
13890
|
* (tryReportToParent / deliverRelayReport) read only the LAST turn's output,
|
|
13815
13891
|
* and the next runAgentTurn resets it. So when the batch just answered came
|
|
@@ -13821,25 +13897,44 @@ ${q.text}` : q.text).join("\n\n");
|
|
|
13821
13897
|
* an interim would wake G mid-round, and a fresh query_session from G would
|
|
13822
13898
|
* interrupt W again. */
|
|
13823
13899
|
sendInterimReport(session, batch) {
|
|
13824
|
-
const
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
this.deliverInterim(session, "parent");
|
|
13828
|
-
else if (fromRelay)
|
|
13829
|
-
this.deliverInterim(session, "relay");
|
|
13900
|
+
const to = interimDoor(session.parentId, batch);
|
|
13901
|
+
if (to)
|
|
13902
|
+
this.deliverInterim(session, to);
|
|
13830
13903
|
}
|
|
13831
13904
|
/** Interim report for the OPENING turn (runSession's `hasFirst` turn): when it
|
|
13832
13905
|
* ends naturally while the queue already holds a follow-up, the drain resets
|
|
13833
|
-
* output and the wrap-up is lost the same way.
|
|
13834
|
-
*
|
|
13835
|
-
*
|
|
13836
|
-
*
|
|
13837
|
-
|
|
13906
|
+
* output and the wrap-up is lost the same way. A sub-agent's opening message
|
|
13907
|
+
* is its parent's dispatch, so it always owes the parent. A root owes its
|
|
13908
|
+
* relay caller only when the opening message itself is relay-prefixed (same
|
|
13909
|
+
* door as the drain path): a pending reply_to alone is not enough — a local
|
|
13910
|
+
* user chatting into a root that still owes a relay report would otherwise
|
|
13911
|
+
* have their answer forwarded to the caller. Goal workers have neither. */
|
|
13912
|
+
sendOpeningInterim(session, batch) {
|
|
13838
13913
|
if (session.parentId !== null)
|
|
13839
13914
|
this.deliverInterim(session, "parent");
|
|
13840
|
-
else if (readReplyTo(this.db, session.id) !== null)
|
|
13915
|
+
else if (interimDoor(null, batch) === "relay" && readReplyTo(this.db, session.id) !== null)
|
|
13841
13916
|
this.deliverInterim(session, "relay");
|
|
13842
13917
|
}
|
|
13918
|
+
/** Interim report for the run's LAST turn while a still-running child holds
|
|
13919
|
+
* the end-of-run report back. The loss it fixes: tryReportToParent /
|
|
13920
|
+
* deliverRelayReport fire only once the subtree is quiet, and read only the
|
|
13921
|
+
* last turn. A session waiting on a sub-agent is asked by its caller
|
|
13922
|
+
* (relay_send / the parent's query_session) and answers; the run ends with the
|
|
13923
|
+
* child still running, so both skip, and the child's report later wakes a new
|
|
13924
|
+
* run whose first turn resets output — the answer reached no one. Sends only
|
|
13925
|
+
* when that turn answered the owed party while already waiting
|
|
13926
|
+
* (waitingOnChildren — door first, db lookup only past it) and the subtree is
|
|
13927
|
+
* STILL busy. Runs in runSession's finally right before those two, which read
|
|
13928
|
+
* the same db gate synchronously: children running → they skip and this sends;
|
|
13929
|
+
* none → they report this turn as the final and this doesn't. Never overlaps
|
|
13930
|
+
* sendInterimReport's drain call: a turn another turn follows is never the last. */
|
|
13931
|
+
sendDeferredInterim(session, lastTurn) {
|
|
13932
|
+
if (!lastTurn.waitingOnChildren || session.finalOutput === "" || session.turnError)
|
|
13933
|
+
return;
|
|
13934
|
+
if (listActiveChildren(this.db, session.id).length === 0)
|
|
13935
|
+
return;
|
|
13936
|
+
this.sendInterimReport(session, lastTurn.batch);
|
|
13937
|
+
}
|
|
13843
13938
|
/** Body = the WHOLE turn's text (`output`, not `finalOutput`): in a turn that
|
|
13844
13939
|
* answered and then called continue_task the answer rides the tool_use
|
|
13845
13940
|
* response, whose text is `final: false` and never reaches finalOutput. */
|
|
@@ -13959,6 +14054,12 @@ ${body}`;
|
|
|
13959
14054
|
* isn't a non-empty array, so a string-content turn would be silently
|
|
13960
14055
|
* deleted by the repair that runs right after this. */
|
|
13961
14056
|
foldIntoAgentMessages(session, text6) {
|
|
14057
|
+
if (session.isCompacting) {
|
|
14058
|
+
session.foldAfterCompact = session.foldAfterCompact ? `${session.foldAfterCompact}
|
|
14059
|
+
|
|
14060
|
+
${text6}` : text6;
|
|
14061
|
+
return;
|
|
14062
|
+
}
|
|
13962
14063
|
const msgs = session.agent.messages;
|
|
13963
14064
|
const last = msgs[msgs.length - 1];
|
|
13964
14065
|
if (last?.role === "user" && Array.isArray(last.content)) {
|
|
@@ -13997,8 +14098,10 @@ ${q.text}` : q.text);
|
|
|
13997
14098
|
} catch {
|
|
13998
14099
|
}
|
|
13999
14100
|
}
|
|
14000
|
-
|
|
14001
|
-
|
|
14101
|
+
if (!session.isCompacting) {
|
|
14102
|
+
session.agent.messages = repairConversationMessages(session.agent.messages, `[Session:${id}]`);
|
|
14103
|
+
this.releaseSession(id);
|
|
14104
|
+
}
|
|
14002
14105
|
}
|
|
14003
14106
|
this.db.update(agentSessions).set({ stoppedAt: now }).where(eq9(agentSessions.id, id)).run();
|
|
14004
14107
|
}
|
|
@@ -14110,7 +14213,8 @@ ${cleaned}` : cleaned;
|
|
|
14110
14213
|
* Send a user message to a session (WS handler → agent).
|
|
14111
14214
|
* If idle: runs the message directly.
|
|
14112
14215
|
* If busy: enqueues and requests graceful interrupt.
|
|
14113
|
-
* If compacting: enqueues for post-compact processing
|
|
14216
|
+
* If compacting: enqueues for post-compact processing (plus a graceful
|
|
14217
|
+
* interrupt when the compact runs inside a live turn).
|
|
14114
14218
|
* Returns 'running' | 'queued' to inform the caller.
|
|
14115
14219
|
*/
|
|
14116
14220
|
async sendUserMessage(sessionId, message, images, accessLevel) {
|
|
@@ -14137,6 +14241,8 @@ ${cleaned}` : cleaned;
|
|
|
14137
14241
|
}
|
|
14138
14242
|
if (session.isCompacting) {
|
|
14139
14243
|
session.messageQueue.push({ text: message, images });
|
|
14244
|
+
if (session.abortController !== null)
|
|
14245
|
+
session.interruptRequested = true;
|
|
14140
14246
|
console.debug(`[SessionManager] sendUserMessage: ${sessionId} compacting \u2014 message queued`);
|
|
14141
14247
|
return "queued";
|
|
14142
14248
|
}
|
|
@@ -14246,7 +14352,8 @@ ${cleaned}` : cleaned;
|
|
|
14246
14352
|
const overThreshold = session.lastContextTokens > session.contextConfig.maxTokens * session.contextConfig.compressAt;
|
|
14247
14353
|
if (!overThreshold)
|
|
14248
14354
|
return;
|
|
14249
|
-
|
|
14355
|
+
const cut = compactCut(session.agent.messages);
|
|
14356
|
+
if (cut === 0)
|
|
14250
14357
|
return;
|
|
14251
14358
|
const taskId = session.parentId ? session.id : void 0;
|
|
14252
14359
|
try {
|
|
@@ -14262,14 +14369,43 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14262
14369
|
this.emitEvent(session.id, { type: "compacted", totalTokens: result.estimatedTokens, taskId });
|
|
14263
14370
|
console.debug(`[SessionManager] Auto-compact ${session.id}: ${result.olderCount} messages compacted`);
|
|
14264
14371
|
} else {
|
|
14265
|
-
this.
|
|
14372
|
+
this.localCompactFallback(session, cut, "no summary produced", taskId);
|
|
14266
14373
|
}
|
|
14267
14374
|
} catch (err) {
|
|
14268
|
-
|
|
14269
|
-
|
|
14375
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14376
|
+
console.debug(`[SessionManager] Auto-compact ${session.id} failed: ${msg}`);
|
|
14377
|
+
this.localCompactFallback(session, cut, msg, taskId);
|
|
14270
14378
|
} finally {
|
|
14271
|
-
session
|
|
14272
|
-
}
|
|
14379
|
+
this.clearCompacting(session);
|
|
14380
|
+
}
|
|
14381
|
+
}
|
|
14382
|
+
/** Auto-compact fallback when the LLM summary failed (threw / idle-timed out
|
|
14383
|
+
* / came back empty). selfCompactSession has already rolled the history
|
|
14384
|
+
* back; leaving it as-is kept the session over threshold, so every later
|
|
14385
|
+
* model call re-ran the same failing summarize. Do the no-LLM compact
|
|
14386
|
+
* instead, with the LLM path's tail micro-compact + token estimate. */
|
|
14387
|
+
localCompactFallback(session, cut, reason, taskId) {
|
|
14388
|
+
const local = localCompactMessages(session.agent.messages);
|
|
14389
|
+
if (local.compacted) {
|
|
14390
|
+
const [summaryMsg, ...tail] = local.messages;
|
|
14391
|
+
const tailMicro = microCompactMessages(tail, 1);
|
|
14392
|
+
session.agent.messages = [summaryMsg, ...tailMicro.compacted ? tailMicro.messages : tail];
|
|
14393
|
+
}
|
|
14394
|
+
session.lastContextTokens = estimateMessageTokens(session.agent.messages) + estimateMessageTokens([{ role: "user", content: session.systemPrompt }]);
|
|
14395
|
+
const why = reason.length > 80 ? `${reason.slice(0, 80)}\u2026` : reason;
|
|
14396
|
+
this.emitEvent(session.id, { type: "system", text: `Auto-compacted ${cut} older messages (local fallback \u2014 LLM summary failed: ${why})`, taskId, compactEnd: true });
|
|
14397
|
+
}
|
|
14398
|
+
/** Leave the compact-in-flight state (auto: maybeAutoCompact's finally;
|
|
14399
|
+
* manual: endCompact), then land a queue fold a Stop parked meanwhile
|
|
14400
|
+
* (see foldIntoAgentMessages) on the post-compact history. */
|
|
14401
|
+
clearCompacting(session) {
|
|
14402
|
+
session.isCompacting = false;
|
|
14403
|
+
session.compactAbortController = null;
|
|
14404
|
+
const parked = session.foldAfterCompact;
|
|
14405
|
+
if (!parked)
|
|
14406
|
+
return;
|
|
14407
|
+
session.foldAfterCompact = null;
|
|
14408
|
+
this.foldIntoAgentMessages(session, parked);
|
|
14273
14409
|
}
|
|
14274
14410
|
/** Check if a session is compacting */
|
|
14275
14411
|
isSessionCompacting(sessionId) {
|
|
@@ -14351,8 +14487,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14351
14487
|
const session = this.sessions.get(sessionId);
|
|
14352
14488
|
if (!session)
|
|
14353
14489
|
return;
|
|
14354
|
-
session
|
|
14355
|
-
session.compactAbortController = null;
|
|
14490
|
+
this.clearCompacting(session);
|
|
14356
14491
|
if (session.messageQueue.length > 0 && session.promise === null) {
|
|
14357
14492
|
console.debug(`[SessionManager] Compact ${sessionId} ended \u2014 draining ${session.messageQueue.length} queued message(s)`);
|
|
14358
14493
|
this.runSession(sessionId, "").catch((err) => {
|
|
@@ -14370,38 +14505,6 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14370
14505
|
session.compactAbortController = null;
|
|
14371
14506
|
}
|
|
14372
14507
|
}
|
|
14373
|
-
/**
|
|
14374
|
-
* Compute the compaction cut point: messages[0..cut) get summarized,
|
|
14375
|
-
* messages[cut..] are kept. Returns 0 when there is nothing to compact
|
|
14376
|
-
* (too few messages) — the "compaction is feasible" predicate.
|
|
14377
|
-
*
|
|
14378
|
-
* Single source of truth shared by selfCompactSession AND the
|
|
14379
|
-
* pre-preflight feasibility gates in maybeAutoCompact / compactSession:
|
|
14380
|
-
* gate and actual compact must run the same computation on the same
|
|
14381
|
-
* `session.agent.messages` reference or they drift ("gate said yes,
|
|
14382
|
-
* compact said no" re-creates the orphan-preflight bug).
|
|
14383
|
-
*
|
|
14384
|
-
* Note the tail loop only ever moves `cut` UP (a tail of user tool_result
|
|
14385
|
-
* messages extends the summarized region — they're protocol continuations
|
|
14386
|
-
* of an assistant turn, not standalone turns), so cut === 0 is exactly
|
|
14387
|
-
* the `messages.length <= keepCount` case.
|
|
14388
|
-
*/
|
|
14389
|
-
compactCut(messages2) {
|
|
14390
|
-
const keepCount = config.compact.keep_messages;
|
|
14391
|
-
if (messages2.length <= keepCount)
|
|
14392
|
-
return 0;
|
|
14393
|
-
let cut = Math.max(0, messages2.length - keepCount);
|
|
14394
|
-
while (cut < messages2.length) {
|
|
14395
|
-
const m = messages2[cut];
|
|
14396
|
-
const firstBlock = Array.isArray(m.content) ? m.content[0] : void 0;
|
|
14397
|
-
if (m.role === "user" && firstBlock?.type === "tool_result") {
|
|
14398
|
-
cut++;
|
|
14399
|
-
continue;
|
|
14400
|
-
}
|
|
14401
|
-
break;
|
|
14402
|
-
}
|
|
14403
|
-
return cut;
|
|
14404
|
-
}
|
|
14405
14508
|
/**
|
|
14406
14509
|
* Self-compact: let the current agent summarize its own conversation.
|
|
14407
14510
|
* The agent already has full context cached, so this is fast and lossless.
|
|
@@ -14412,7 +14515,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14412
14515
|
if (!session)
|
|
14413
14516
|
return null;
|
|
14414
14517
|
const messages2 = session.agent.messages;
|
|
14415
|
-
const cut =
|
|
14518
|
+
const cut = compactCut(messages2);
|
|
14416
14519
|
if (cut === 0)
|
|
14417
14520
|
return null;
|
|
14418
14521
|
session.compactedThisTurn = true;
|
|
@@ -14427,29 +14530,24 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14427
14530
|
"Write in the SAME LANGUAGE as the conversation.",
|
|
14428
14531
|
"Output ONLY the summary, no preamble."
|
|
14429
14532
|
].join("\n");
|
|
14430
|
-
const timeoutMs = config.compact.summarize_timeout_sec * 1e3;
|
|
14431
|
-
const timeoutCtrl = new AbortController();
|
|
14432
|
-
const timer = setTimeout(() => timeoutCtrl.abort(abortReason("compact-timeout")), timeoutMs);
|
|
14433
14533
|
const cleanRecent = messages2.slice(cut).map((m) => structuredClone(m));
|
|
14434
14534
|
const preRunLen = messages2.length;
|
|
14435
14535
|
const preRunTail = structuredClone(messages2[preRunLen - 1]);
|
|
14436
14536
|
let rebuilt = false;
|
|
14437
14537
|
try {
|
|
14438
14538
|
let summaryText = "";
|
|
14439
|
-
|
|
14440
|
-
signal.addEventListener("abort", () => timeoutCtrl.abort(abortReason("compact-cancelled")), { once: true });
|
|
14441
|
-
const iter = session.agent.run(compactInstruction, {
|
|
14442
|
-
cancelSignal: timeoutCtrl.signal
|
|
14443
|
-
});
|
|
14539
|
+
const iter = session.agent.run(compactInstruction, { cancelSignal: signal });
|
|
14444
14540
|
for await (const event of iter) {
|
|
14445
|
-
if (signal?.aborted
|
|
14541
|
+
if (signal?.aborted)
|
|
14542
|
+
break;
|
|
14543
|
+
if (event.type === "tool_call") {
|
|
14544
|
+
summaryText = "";
|
|
14446
14545
|
break;
|
|
14546
|
+
}
|
|
14447
14547
|
if (event.type === "text") {
|
|
14448
14548
|
summaryText += event.text ?? "";
|
|
14449
14549
|
}
|
|
14450
14550
|
}
|
|
14451
|
-
if (timeoutCtrl.signal.aborted)
|
|
14452
|
-
throw new Error(`Self-compact timed out after ${timeoutMs / 1e3}s`);
|
|
14453
14551
|
if (signal?.aborted)
|
|
14454
14552
|
throw new Error("Self-compact cancelled");
|
|
14455
14553
|
if (!summaryText.trim())
|
|
@@ -14468,7 +14566,6 @@ ${summaryText}` }]
|
|
|
14468
14566
|
const estimatedTokens = estimateMessageTokens(session.agent.messages) + estimateMessageTokens([{ role: "user", content: session.systemPrompt }]);
|
|
14469
14567
|
return { summary: summaryText, olderCount, estimatedTokens };
|
|
14470
14568
|
} finally {
|
|
14471
|
-
clearTimeout(timer);
|
|
14472
14569
|
if (!rebuilt) {
|
|
14473
14570
|
session.agent.messages.length = preRunLen;
|
|
14474
14571
|
session.agent.messages[preRunLen - 1] = preRunTail;
|
|
@@ -14510,7 +14607,7 @@ ${summaryText}` }]
|
|
|
14510
14607
|
if (session.isCompacting)
|
|
14511
14608
|
return "already";
|
|
14512
14609
|
const rawCount = session.agent.messages.length;
|
|
14513
|
-
if (
|
|
14610
|
+
if (compactCut(session.agent.messages) === 0)
|
|
14514
14611
|
return "nothing";
|
|
14515
14612
|
const ac = await this.beginCompact(sessionId);
|
|
14516
14613
|
if (!ac)
|
|
@@ -14537,9 +14634,9 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14537
14634
|
return "compacted";
|
|
14538
14635
|
} catch (err) {
|
|
14539
14636
|
const msg = err instanceof Error ? err.message : String(err);
|
|
14540
|
-
if (
|
|
14637
|
+
if (ac.signal.aborted) {
|
|
14541
14638
|
this.emitEvent(sessionId, { type: "system", text: "Compact cancelled" });
|
|
14542
|
-
return "
|
|
14639
|
+
return "cancelled";
|
|
14543
14640
|
}
|
|
14544
14641
|
this.emitEvent(sessionId, { type: "system", text: "Compaction failed \u2014 context unchanged", taskId });
|
|
14545
14642
|
throw err;
|
|
@@ -14843,10 +14940,14 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14843
14940
|
* message), and requests a SOFT interrupt so a busy turn yields after its
|
|
14844
14941
|
* current tool — same effect as sendUserMessage's busy branch, minus the
|
|
14845
14942
|
* idle/run path (the caller already established the session is busy or
|
|
14846
|
-
* compacting).
|
|
14847
|
-
*
|
|
14848
|
-
*
|
|
14849
|
-
*
|
|
14943
|
+
* compacting). A compact outside a live model loop (manual /compact, or the
|
|
14944
|
+
* turn-end auto-compact — abortController already null) has nothing to
|
|
14945
|
+
* interrupt, so the flag is NOT set there (mirrors sendUserMessage's
|
|
14946
|
+
* compacting branch): the drain would snapshot it into resumedAfterInterrupt
|
|
14947
|
+
* and let continue_task arm a kick in a turn that interrupted nothing. A
|
|
14948
|
+
* mid-turn auto-compact (beforeCallModel) does interrupt a live turn — the
|
|
14949
|
+
* flag is set, so the turn yields after its next tool instead of running on
|
|
14950
|
+
* to its natural end with the message still queued. */
|
|
14850
14951
|
async enqueueUserMessage(sessionId, text6, images) {
|
|
14851
14952
|
const session = this.sessions.get(sessionId);
|
|
14852
14953
|
if (!session)
|
|
@@ -14856,7 +14957,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14856
14957
|
this.emitEvent(sessionId, { type: "system", text: `\u26A0 ${w}` });
|
|
14857
14958
|
}
|
|
14858
14959
|
session.messageQueue.push({ text: scoped.text, images });
|
|
14859
|
-
if (!session.isCompacting)
|
|
14960
|
+
if (!session.isCompacting || session.abortController !== null)
|
|
14860
14961
|
session.interruptRequested = true;
|
|
14861
14962
|
console.debug(`[SessionManager] User message enqueued for ${sessionId} (${session.messageQueue.length} in queue)`);
|
|
14862
14963
|
}
|
|
@@ -14885,7 +14986,9 @@ ${q.text}` : q.text);
|
|
|
14885
14986
|
this.markPendingToolCallsInterrupted(sessionId);
|
|
14886
14987
|
}
|
|
14887
14988
|
this.cancelCompact(sessionId);
|
|
14888
|
-
|
|
14989
|
+
if (!session.isCompacting) {
|
|
14990
|
+
session.agent.messages = repairConversationMessages(session.agent.messages, `[Session:${sessionId}]`);
|
|
14991
|
+
}
|
|
14889
14992
|
}
|
|
14890
14993
|
};
|
|
14891
14994
|
}
|
|
@@ -15153,6 +15256,7 @@ async function createHarness(opts) {
|
|
|
15153
15256
|
}
|
|
15154
15257
|
}
|
|
15155
15258
|
activeOverrides2.set(USER_ID, sessionId);
|
|
15259
|
+
const sessionAgentId = sm.getSessionById(sessionId)?.agentId ?? agentId;
|
|
15156
15260
|
const disabledSet = getDisabledSet(sm.getDb(), "agent");
|
|
15157
15261
|
const agents = await scanAvailableAgents(workspace, disabledSet);
|
|
15158
15262
|
const agentEntry = agents.find((a) => a.id === agentId);
|
|
@@ -15160,7 +15264,7 @@ async function createHarness(opts) {
|
|
|
15160
15264
|
const unsubPersistent = sm.registerEventListener(sessionId, (event) => {
|
|
15161
15265
|
for (const h of eventHandlers) h(event);
|
|
15162
15266
|
});
|
|
15163
|
-
return { sm, workspace, sessionId, accessLevel, supportsImage, unsubPersistent };
|
|
15267
|
+
return { sm, workspace, sessionId, agentId: sessionAgentId, accessLevel, supportsImage, unsubPersistent };
|
|
15164
15268
|
}
|
|
15165
15269
|
let state = await bindWorkspace(initialPath, opts.sessionId, opts.newSession ?? false);
|
|
15166
15270
|
async function send(message, images) {
|
|
@@ -15227,6 +15331,7 @@ async function createHarness(opts) {
|
|
|
15227
15331
|
state = {
|
|
15228
15332
|
...state,
|
|
15229
15333
|
sessionId: result.switchTo,
|
|
15334
|
+
agentId: state.sm.getSessionById(result.switchTo)?.agentId ?? agentId,
|
|
15230
15335
|
unsubPersistent: state.sm.registerEventListener(result.switchTo, (event) => {
|
|
15231
15336
|
for (const h of eventHandlers) h(event);
|
|
15232
15337
|
})
|
|
@@ -15307,6 +15412,9 @@ async function createHarness(opts) {
|
|
|
15307
15412
|
get workspace() {
|
|
15308
15413
|
return state.workspace;
|
|
15309
15414
|
},
|
|
15415
|
+
get agentId() {
|
|
15416
|
+
return state.agentId;
|
|
15417
|
+
},
|
|
15310
15418
|
get supportsImage() {
|
|
15311
15419
|
return state.supportsImage;
|
|
15312
15420
|
},
|
|
@@ -16265,8 +16373,8 @@ function formatUsageLine(event) {
|
|
|
16265
16373
|
parts.push(`${ANSI.dim}think off${ANSI.reset}`);
|
|
16266
16374
|
}
|
|
16267
16375
|
if (event.modelId) {
|
|
16268
|
-
const
|
|
16269
|
-
parts.push(`${ANSI.blue}${
|
|
16376
|
+
const shortModel2 = event.modelId.replace(/^global\.anthropic\./, "");
|
|
16377
|
+
parts.push(`${ANSI.blue}${shortModel2}${ANSI.reset}`);
|
|
16270
16378
|
}
|
|
16271
16379
|
return `${ANSI.dim}[${ANSI.reset} ${parts.join(" ")} ${ANSI.dim}]${ANSI.reset}`;
|
|
16272
16380
|
}
|
|
@@ -16506,22 +16614,15 @@ var init_history = __esm({
|
|
|
16506
16614
|
// src/tui/components/status-bar.tsx
|
|
16507
16615
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
16508
16616
|
import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
16509
|
-
function shortModel(modelId) {
|
|
16510
|
-
return modelId.replace(/^global\.anthropic\./, "").replace(/^anthropic\./, "").replace(/-\d{8}$/, "");
|
|
16511
|
-
}
|
|
16512
16617
|
function shortPath(p) {
|
|
16513
16618
|
const home = process.env.HOME ?? "";
|
|
16514
16619
|
if (home && p.startsWith(home)) return "~" + p.slice(home.length);
|
|
16515
16620
|
return p;
|
|
16516
16621
|
}
|
|
16517
|
-
function StatusBar({
|
|
16622
|
+
function StatusBar({ agentId, contextPercent, workspace, sessionId, verbose }) {
|
|
16518
16623
|
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", justifyContent: "space-between", paddingX: 1, children: [
|
|
16519
16624
|
/* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", gap: 1, children: [
|
|
16520
|
-
/* @__PURE__ */ jsx4(Text4, { color: "green", children:
|
|
16521
|
-
modelId ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
16522
|
-
/* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "\xB7" }),
|
|
16523
|
-
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: shortModel(modelId) })
|
|
16524
|
-
] }) : null,
|
|
16625
|
+
/* @__PURE__ */ jsx4(Text4, { color: "green", children: agentId }),
|
|
16525
16626
|
contextPercent != null ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
16526
16627
|
/* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "\xB7" }),
|
|
16527
16628
|
/* @__PURE__ */ jsx4(Text4, { color: contextPercent > 80 ? "red" : contextPercent > 50 ? "yellow" : "green", children: `\u25EF ${contextPercent}%` })
|
|
@@ -17228,10 +17329,8 @@ function initialState(verbose) {
|
|
|
17228
17329
|
liveThinking: null,
|
|
17229
17330
|
spinnerLabel: null,
|
|
17230
17331
|
running: false,
|
|
17231
|
-
modelId: null,
|
|
17232
17332
|
contextTokens: null,
|
|
17233
17333
|
agentNameByTaskId: /* @__PURE__ */ new Map(),
|
|
17234
|
-
rootAgentName: "agent",
|
|
17235
17334
|
subAgents: /* @__PURE__ */ new Map(),
|
|
17236
17335
|
verbose,
|
|
17237
17336
|
pendingToolInput: null,
|
|
@@ -17325,7 +17424,7 @@ function messagesToLogLines(messages2) {
|
|
|
17325
17424
|
if (u.thinkingEffort && u.thinkingEffort !== "off") {
|
|
17326
17425
|
parts.push(`${ANSI2.magenta}think ${u.thinkingEffort}${ANSI2.reset}`);
|
|
17327
17426
|
}
|
|
17328
|
-
if (m.modelId) parts.push(`${ANSI2.blue}${
|
|
17427
|
+
if (m.modelId) parts.push(`${ANSI2.blue}${shortModel(m.modelId)}${ANSI2.reset}`);
|
|
17329
17428
|
lines.push({ text: `${tsStr} ${ANSI2.dim}\xB7${ANSI2.reset} ${parts.join(" ")}` });
|
|
17330
17429
|
continue;
|
|
17331
17430
|
}
|
|
@@ -17396,7 +17495,7 @@ function fmtK2(n) {
|
|
|
17396
17495
|
function fmtMs2(ms) {
|
|
17397
17496
|
return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${ms}ms`;
|
|
17398
17497
|
}
|
|
17399
|
-
function
|
|
17498
|
+
function shortModel(id) {
|
|
17400
17499
|
return id.replace(/^global\.anthropic\./, "").replace(/^anthropic\./, "").replace(/-\d{8}$/, "");
|
|
17401
17500
|
}
|
|
17402
17501
|
function formatTs(ms) {
|
|
@@ -17433,10 +17532,8 @@ function reducer(state, action) {
|
|
|
17433
17532
|
liveThinking: null,
|
|
17434
17533
|
spinnerLabel: null,
|
|
17435
17534
|
running: false,
|
|
17436
|
-
modelId: null,
|
|
17437
17535
|
contextTokens: null,
|
|
17438
17536
|
agentNameByTaskId: /* @__PURE__ */ new Map(),
|
|
17439
|
-
rootAgentName: "agent",
|
|
17440
17537
|
subAgents: /* @__PURE__ */ new Map(),
|
|
17441
17538
|
pendingToolInput: null,
|
|
17442
17539
|
pendingToolArg: null,
|
|
@@ -17476,9 +17573,6 @@ function reducer(state, action) {
|
|
|
17476
17573
|
}]
|
|
17477
17574
|
};
|
|
17478
17575
|
}
|
|
17479
|
-
if (!e.taskId && e.agentName) {
|
|
17480
|
-
next = { ...next, rootAgentName: e.agentName };
|
|
17481
|
-
}
|
|
17482
17576
|
return next;
|
|
17483
17577
|
}
|
|
17484
17578
|
case "agent_done": {
|
|
@@ -17590,7 +17684,6 @@ function reducer(state, action) {
|
|
|
17590
17684
|
blocks,
|
|
17591
17685
|
liveText: "",
|
|
17592
17686
|
liveThinking: null,
|
|
17593
|
-
modelId: e.modelId ?? state.modelId,
|
|
17594
17687
|
contextTokens: ctx > 0 ? ctx : state.contextTokens
|
|
17595
17688
|
};
|
|
17596
17689
|
}
|
|
@@ -17898,8 +17991,7 @@ ${lines.join("\n")}` });
|
|
|
17898
17991
|
/* @__PURE__ */ jsx11(
|
|
17899
17992
|
StatusBar,
|
|
17900
17993
|
{
|
|
17901
|
-
|
|
17902
|
-
modelId: state.modelId,
|
|
17994
|
+
agentId: harness.agentId,
|
|
17903
17995
|
contextPercent: ctxPercent,
|
|
17904
17996
|
workspace: harness.workspace,
|
|
17905
17997
|
sessionId: harness.sessionId,
|
|
@@ -22275,6 +22367,10 @@ function createWebChannel(deps) {
|
|
|
22275
22367
|
const sessionId = opts?.sessionId ?? getActiveSessionId(sm, account.accountId);
|
|
22276
22368
|
if (!sessionId)
|
|
22277
22369
|
return false;
|
|
22370
|
+
if (sm.isSessionCompacting(sessionId) && !sm.isSessionRunning(sessionId)) {
|
|
22371
|
+
sm.cancelCompact(sessionId);
|
|
22372
|
+
return true;
|
|
22373
|
+
}
|
|
22278
22374
|
if (!sm.isSessionRunning(sessionId))
|
|
22279
22375
|
return false;
|
|
22280
22376
|
await sm.stopSession(sessionId);
|
|
@@ -23860,6 +23956,9 @@ function sendWsNotification(event, state, turnId, ctx) {
|
|
|
23860
23956
|
sendJson(ctx.ws, { type: "compact:started" });
|
|
23861
23957
|
}
|
|
23862
23958
|
sendJson(ctx.ws, { type: "chat:system", text: event.text ?? "", taskId, agentName, sessionId });
|
|
23959
|
+
if (event.compactEnd && !taskId) {
|
|
23960
|
+
sendJson(ctx.ws, { type: "compact:done" });
|
|
23961
|
+
}
|
|
23863
23962
|
break;
|
|
23864
23963
|
case "error":
|
|
23865
23964
|
sendJson(ctx.ws, { type: "error", error: event.error, agentName, taskId, sessionId });
|
|
@@ -24599,7 +24698,8 @@ ${msg.message}` : markers.join("\n");
|
|
|
24599
24698
|
rememberChat(msg);
|
|
24600
24699
|
await sm.enqueueUserMessage(sid, msg.message, msg.images);
|
|
24601
24700
|
ackChat(msg);
|
|
24602
|
-
|
|
24701
|
+
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 });
|
|
24603
24703
|
return;
|
|
24604
24704
|
}
|
|
24605
24705
|
if (sm.isSessionRunning(sid)) {
|
|
@@ -24627,7 +24727,7 @@ ${msg.message}` : markers.join("\n");
|
|
|
24627
24727
|
const sid = client2.sessionId;
|
|
24628
24728
|
if (!sm || !sid)
|
|
24629
24729
|
return;
|
|
24630
|
-
if (sm.isSessionCompacting(sid)) {
|
|
24730
|
+
if (sm.isSessionCompacting(sid) && !sm.isSessionRunning(sid)) {
|
|
24631
24731
|
sm.cancelCompact(sid);
|
|
24632
24732
|
sendJson(ws, { type: "chat:stopped", sessionId: client2.sessionId });
|
|
24633
24733
|
return;
|
|
@@ -24642,7 +24742,7 @@ ${msg.message}` : markers.join("\n");
|
|
|
24642
24742
|
const sid = client2.sessionId;
|
|
24643
24743
|
if (!sm || !sid)
|
|
24644
24744
|
return;
|
|
24645
|
-
if (sm.isSessionCompacting(sid)) {
|
|
24745
|
+
if (sm.isSessionCompacting(sid) && !sm.isSessionRunning(sid)) {
|
|
24646
24746
|
sm.cancelCompact(sid);
|
|
24647
24747
|
sendJson(ws, { type: "chat:stopped", sessionId: client2.sessionId });
|
|
24648
24748
|
return;
|
|
@@ -32857,6 +32957,14 @@ async function gracefulShutdown(signal) {
|
|
|
32857
32957
|
if (server && typeof server.close === "function") {
|
|
32858
32958
|
server.close();
|
|
32859
32959
|
}
|
|
32960
|
+
for (const { workspacePath, sm } of registry.list()) {
|
|
32961
|
+
try {
|
|
32962
|
+
sm.flushAll();
|
|
32963
|
+
} catch (err) {
|
|
32964
|
+
console.error(`[Server] flushAll failed for ${workspacePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
32965
|
+
}
|
|
32966
|
+
releaseWorkspaceRuntime(workspacePath);
|
|
32967
|
+
}
|
|
32860
32968
|
console.log("[Server] Shutdown complete");
|
|
32861
32969
|
await shutdownObservability();
|
|
32862
32970
|
process.exit(0);
|
|
@@ -32907,7 +33015,7 @@ var init_dist2 = __esm({
|
|
|
32907
33015
|
init_git_ssh();
|
|
32908
33016
|
__dirname3 = path72.dirname(fileURLToPath3(import.meta.url));
|
|
32909
33017
|
PORT = config.server.port;
|
|
32910
|
-
HALO_VERSION = "1.5.
|
|
33018
|
+
HALO_VERSION = "1.5.2-alpha";
|
|
32911
33019
|
PROJECT_ROOT = findProjectRoot();
|
|
32912
33020
|
HALO_HOME4 = path72.join(homedir24(), ".halo");
|
|
32913
33021
|
GIT_SHA = readGitSha();
|
|
@@ -33838,7 +33946,7 @@ import path73 from "node:path";
|
|
|
33838
33946
|
import fs67 from "node:fs";
|
|
33839
33947
|
import { homedir as homedir25 } from "node:os";
|
|
33840
33948
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
33841
|
-
var VERSION = "1.5.
|
|
33949
|
+
var VERSION = "1.5.2-alpha";
|
|
33842
33950
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
33843
33951
|
|
|
33844
33952
|
Commands:
|