@keyou007/agent-bot 0.1.24-alpha.1 → 0.1.24-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/README.md +33 -7
- package/README.zh.md +32 -7
- package/dist/acp/AcpJsonRpcConnection.js +3 -1
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -1
- package/dist/cli/SelfUpdateRunner.js +2 -0
- package/dist/cli/SelfUpdateRunner.js.map +1 -1
- package/dist/cli/SelfUpdater.js +2 -0
- package/dist/cli/SelfUpdater.js.map +1 -1
- package/dist/codex/AppServerConnection.js +3 -1
- package/dist/codex/AppServerConnection.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +41 -3
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +95 -3
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +49 -23
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +2 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +70 -4
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +44 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +114 -37
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/RecentMessageContext.js +70 -0
- package/dist/feishu/RecentMessageContext.js.map +1 -0
- package/dist/index.js +62 -24
- package/dist/index.js.map +1 -1
- package/dist/local-files/DiagramPreview.js +171 -0
- package/dist/local-files/DiagramPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +141 -3
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/PreviewSyntaxHighlight.js +57 -0
- package/dist/local-files/PreviewSyntaxHighlight.js.map +1 -0
- package/dist/local-files/TurnPreviewPage.js +499 -60
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +18 -9
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +157 -8
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/presentation/turnReasoning.js +23 -0
- package/dist/presentation/turnReasoning.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +461 -49
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +43 -2
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +469 -0
- package/dist/state/TurnPreviewJournal.js.map +1 -0
- package/dist/state/migrations.js +10 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/updates/AutomaticUpdatePreparer.js +16 -7
- package/dist/updates/AutomaticUpdatePreparer.js.map +1 -1
- package/dist/updates/ManualUpdateController.js +102 -0
- package/dist/updates/ManualUpdateController.js.map +1 -0
- package/dist/updates/PublishedRelease.js +42 -4
- package/dist/updates/PublishedRelease.js.map +1 -1
- package/dist/utils/ProtocolLineWarning.js +15 -0
- package/dist/utils/ProtocolLineWarning.js.map +1 -0
- package/docs/technical-reference.md +21 -1
- package/docs/technical-reference.zh.md +21 -1
- package/npm-shrinkwrap.json +1401 -10
- package/package.json +3 -1
- package/skills/agent-bot/SKILL.md +9 -2
- package/src/acp/AcpJsonRpcConnection.ts +3 -1
- package/src/cli/SelfUpdateRunner.ts +3 -0
- package/src/cli/SelfUpdater.ts +2 -0
- package/src/codex/AppServerConnection.ts +3 -1
- package/src/codex/CodexEventMapper.ts +40 -3
- package/src/codex/CodexRuntime.ts +91 -4
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/feishu/CardRenderer.ts +47 -21
- package/src/feishu/FeishuConnector.ts +2 -0
- package/src/feishu/FeishuMarkdown.ts +69 -3
- package/src/feishu/FeishuMessageClient.ts +44 -0
- package/src/feishu/FeishuTurnPresenter.ts +107 -31
- package/src/feishu/RecentMessageContext.ts +90 -0
- package/src/feishu/types.ts +5 -0
- package/src/index.ts +62 -22
- package/src/local-files/DiagramPreview.ts +173 -0
- package/src/local-files/LocalFileViewerServer.ts +129 -2
- package/src/local-files/PreviewSyntaxHighlight.ts +58 -0
- package/src/local-files/TurnPreviewPage.ts +539 -57
- package/src/presentation/OutboundRouter.ts +25 -7
- package/src/presentation/TurnStateReducer.ts +153 -9
- package/src/presentation/turnReasoning.ts +21 -0
- package/src/presentation/turnViewTypes.ts +19 -1
- package/src/proxy/ProxySessionController.ts +453 -40
- package/src/runtime/types.ts +21 -0
- package/src/state/StateStore.ts +45 -2
- package/src/state/TurnPreviewJournal.ts +398 -0
- package/src/state/migrations.ts +10 -0
- package/src/updates/AutomaticUpdatePreparer.ts +21 -7
- package/src/updates/ManualUpdateController.ts +116 -0
- package/src/updates/PublishedRelease.ts +49 -3
- package/src/utils/ProtocolLineWarning.ts +13 -0
package/src/runtime/types.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface PlanStep {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export interface ToolState {
|
|
45
|
+
previewRevision?: number;
|
|
45
46
|
id: string;
|
|
46
47
|
title: string;
|
|
47
48
|
kind: string;
|
|
@@ -98,7 +99,10 @@ export type AgentEvent =
|
|
|
98
99
|
activityId?: string;
|
|
99
100
|
append?: boolean;
|
|
100
101
|
severity?: "warning";
|
|
102
|
+
reasoning?: { itemId: string; summaryIndex: number };
|
|
101
103
|
}
|
|
104
|
+
| { type: "reasoning_delta"; sessionId: string; turnId: string; itemId: string; contentIndex: number; text: string }
|
|
105
|
+
| { type: "reasoning_completed"; sessionId: string; turnId: string; itemId: string; summary: string[]; content: string[] }
|
|
102
106
|
| { type: "plan_updated"; sessionId: string; turnId: string; steps: PlanStep[] }
|
|
103
107
|
| { type: "tool_started"; sessionId: string; turnId: string; tool: ToolState }
|
|
104
108
|
| { type: "tool_updated"; sessionId: string; turnId: string; tool: ToolState }
|
|
@@ -135,6 +139,22 @@ export interface ConversationTurn {
|
|
|
135
139
|
messages: Array<{ role: "user" | "assistant"; text: string }>;
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
export interface RemoteTurnDetails {
|
|
143
|
+
turnId: string;
|
|
144
|
+
status: "completed" | "cancelled" | "failed";
|
|
145
|
+
startedAt?: number;
|
|
146
|
+
completedAt?: number;
|
|
147
|
+
durationMs?: number;
|
|
148
|
+
finalResponse: string;
|
|
149
|
+
error?: string;
|
|
150
|
+
items: Array<
|
|
151
|
+
| { kind: "message"; id: string; role: "user" | "assistant"; text: string; localImagePaths?: string[] }
|
|
152
|
+
| { kind: "reasoning"; id: string; summary: string[]; content: string[] }
|
|
153
|
+
| { kind: "tool"; tool: ToolState }
|
|
154
|
+
| { kind: "plan"; steps: PlanStep[] }
|
|
155
|
+
>;
|
|
156
|
+
}
|
|
157
|
+
|
|
138
158
|
export interface RemoteTurnPage {
|
|
139
159
|
turns: RemoteCompletedTurnSummary[];
|
|
140
160
|
nextCursor?: string;
|
|
@@ -273,6 +293,7 @@ export interface AgentRuntime {
|
|
|
273
293
|
readRemoteForkSource?(remoteSessionId: string): Promise<RemoteSessionSummary>;
|
|
274
294
|
listRemoteTurnSummaries?(remoteSessionId: string, input: { cursor?: string; limit: number }): Promise<RemoteTurnPage>;
|
|
275
295
|
readConversation?(remoteSessionId: string, throughTurnId?: string): AsyncIterable<ConversationTurn>;
|
|
296
|
+
readRemoteTurn?(remoteSessionId: string, turnId: string): Promise<RemoteTurnDetails>;
|
|
276
297
|
inspectRemoteSessionActivity?(remoteSessionId: string): Promise<RemoteSessionActivity>;
|
|
277
298
|
synchronizeSession(sessionId: string): Promise<RuntimeSession>;
|
|
278
299
|
startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string>;
|
package/src/state/StateStore.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { TurnPreviewJournal } from "./TurnPreviewJournal.js";
|
|
3
|
+
import { compactTurnView } from "../presentation/TurnStateReducer.js";
|
|
4
|
+
import type { TurnViewState } from "../presentation/turnViewTypes.js";
|
|
2
5
|
import path from "node:path";
|
|
3
6
|
import Database from "better-sqlite3";
|
|
4
7
|
import { migrations } from "./migrations.js";
|
|
@@ -260,8 +263,10 @@ interface TurnAttemptRow {
|
|
|
260
263
|
|
|
261
264
|
export class StateStore {
|
|
262
265
|
private readonly db: Database.Database;
|
|
266
|
+
readonly previews: TurnPreviewJournal;
|
|
263
267
|
|
|
264
|
-
constructor(sqlitePath: string) {
|
|
268
|
+
constructor(sqlitePath: string, onPreviewError?: (error: unknown) => void) {
|
|
269
|
+
this.previews = new TurnPreviewJournal(path.join(path.dirname(sqlitePath), "turn-previews"), onPreviewError);
|
|
265
270
|
fs.mkdirSync(path.dirname(sqlitePath), { recursive: true });
|
|
266
271
|
this.db = new Database(sqlitePath);
|
|
267
272
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -406,6 +411,7 @@ export class StateStore {
|
|
|
406
411
|
}
|
|
407
412
|
|
|
408
413
|
close(): void {
|
|
414
|
+
this.previews.close();
|
|
409
415
|
this.db.close();
|
|
410
416
|
}
|
|
411
417
|
|
|
@@ -1031,6 +1037,7 @@ export class StateStore {
|
|
|
1031
1037
|
}
|
|
1032
1038
|
|
|
1033
1039
|
saveTurnSnapshot(turnId: string, localSessionId: string, snapshot: unknown, contextKey?: string): void {
|
|
1040
|
+
if ((snapshot as TurnViewState)?.previewJournal) snapshot = compactTurnView(snapshot as TurnViewState);
|
|
1034
1041
|
const now = new Date().toISOString();
|
|
1035
1042
|
this.db
|
|
1036
1043
|
.prepare(
|
|
@@ -1042,6 +1049,9 @@ export class StateStore {
|
|
|
1042
1049
|
context_key = coalesce(excluded.context_key, turn_snapshots.context_key),
|
|
1043
1050
|
snapshot_json = excluded.snapshot_json,
|
|
1044
1051
|
updated_at = excluded.updated_at
|
|
1052
|
+
WHERE turn_snapshots.snapshot_json != excluded.snapshot_json
|
|
1053
|
+
OR turn_snapshots.local_session_id != excluded.local_session_id
|
|
1054
|
+
OR (excluded.context_key IS NOT NULL AND turn_snapshots.context_key IS NOT excluded.context_key)
|
|
1045
1055
|
`,
|
|
1046
1056
|
)
|
|
1047
1057
|
.run(turnId, localSessionId, contextKey ?? null, JSON.stringify(snapshot), now);
|
|
@@ -1108,7 +1118,9 @@ export class StateStore {
|
|
|
1108
1118
|
for (const id of branch.reverse()) {
|
|
1109
1119
|
const row = read.get(id) as { prompt: string | null; answer: string | null; followups: string };
|
|
1110
1120
|
const messages: ConversationTurn["messages"] = [];
|
|
1111
|
-
|
|
1121
|
+
const preview = this.previews.has(id) ? this.previews.load(id, false) : undefined;
|
|
1122
|
+
const followups = preview ? preview.activities.flatMap((a) => a.kind === "user" ? [a.text] : []) : JSON.parse(row.followups) as string[];
|
|
1123
|
+
for (const text of [row.prompt, ...followups]) {
|
|
1112
1124
|
if (text?.trim()) messages.push({ role: "user", text });
|
|
1113
1125
|
}
|
|
1114
1126
|
if (row.answer?.trim()) messages.push({ role: "assistant", text: row.answer });
|
|
@@ -1880,6 +1892,37 @@ export class StateStore {
|
|
|
1880
1892
|
.run(contextKey, eventType, JSON.stringify(payload), new Date().toISOString());
|
|
1881
1893
|
}
|
|
1882
1894
|
|
|
1895
|
+
recentSubmittedMessageIds(localSessionId: string, remoteSessionId: string, contextKey: string): Set<string> {
|
|
1896
|
+
const rows = this.db.prepare(`SELECT b.message_id
|
|
1897
|
+
FROM (SELECT turn_id FROM turn_runtime_origins
|
|
1898
|
+
WHERE local_session_id = ? AND remote_session_id = ? ORDER BY created_at DESC LIMIT 200) o
|
|
1899
|
+
JOIN message_turn_bindings b ON b.turn_id = o.turn_id
|
|
1900
|
+
JOIN turn_snapshots t ON t.turn_id = o.turn_id
|
|
1901
|
+
WHERE b.local_session_id = ? AND t.context_key = ?
|
|
1902
|
+
ORDER BY b.created_at DESC LIMIT 200`)
|
|
1903
|
+
.all(localSessionId, remoteSessionId, localSessionId, contextKey) as Array<{ message_id: string }>;
|
|
1904
|
+
return new Set(rows.map((row) => row.message_id));
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
recentContextMessageIds(localSessionId: string, remoteSessionId: string, contextKey: string, includeQueued = true): Set<string> {
|
|
1908
|
+
const rows = this.db.prepare(`SELECT message_id FROM recent_context_receipts r
|
|
1909
|
+
WHERE local_session_id = ? AND remote_session_id = ? AND context_key = ?
|
|
1910
|
+
AND (EXISTS (SELECT 1 FROM message_turn_bindings b WHERE b.message_id = r.prompt_message_id AND b.local_session_id = r.local_session_id)
|
|
1911
|
+
OR (? AND EXISTS (SELECT 1 FROM queued_prompts q WHERE q.message_id = r.prompt_message_id AND q.local_session_id = r.local_session_id)))`)
|
|
1912
|
+
.all(localSessionId, remoteSessionId, contextKey, includeQueued ? 1 : 0) as Array<{ message_id: string }>;
|
|
1913
|
+
return new Set(rows.map((row) => row.message_id));
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
rememberRecentContextMessages(localSessionId: string, remoteSessionId: string, contextKey: string, promptMessageId: string, messageIds: string[]): void {
|
|
1917
|
+
const insert = this.db.prepare("INSERT INTO recent_context_receipts (local_session_id, remote_session_id, context_key, prompt_message_id, message_id) VALUES (?, ?, ?, ?, ?) ON CONFLICT(local_session_id, remote_session_id, context_key, message_id) DO UPDATE SET prompt_message_id = excluded.prompt_message_id");
|
|
1918
|
+
this.db.transaction(() => {
|
|
1919
|
+
for (const id of new Set(messageIds)) insert.run(localSessionId, remoteSessionId, contextKey, promptMessageId, id);
|
|
1920
|
+
this.db.prepare(`DELETE FROM recent_context_receipts WHERE local_session_id = ? AND remote_session_id = ? AND context_key = ? AND rowid NOT IN (
|
|
1921
|
+
SELECT rowid FROM recent_context_receipts WHERE local_session_id = ? AND remote_session_id = ? AND context_key = ? ORDER BY rowid DESC LIMIT 200
|
|
1922
|
+
)`).run(localSessionId, remoteSessionId, contextKey, localSessionId, remoteSessionId, contextKey);
|
|
1923
|
+
})();
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1883
1926
|
hasInjectedTopicRoot(localSessionId: string, rootMessageId: string): boolean {
|
|
1884
1927
|
return Boolean(this.db.prepare(`
|
|
1885
1928
|
SELECT 1
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash, type Hash } from "node:crypto";
|
|
4
|
+
import type { AgentEvent, ToolState } from "../runtime/types.js";
|
|
5
|
+
import { reduceTurnEvent } from "../presentation/TurnStateReducer.js";
|
|
6
|
+
import type { TurnViewState } from "../presentation/turnViewTypes.js";
|
|
7
|
+
|
|
8
|
+
type TextChange = { key: string; field: string; append: boolean; text: string };
|
|
9
|
+
export type PreviewRecord =
|
|
10
|
+
| { kind: "seed"; state: TurnViewState }
|
|
11
|
+
| { kind: "state"; patch: Partial<TurnViewState> }
|
|
12
|
+
| { kind: "user"; id: string; text: string; images?: string[] }
|
|
13
|
+
| { kind: "event"; event: AgentEvent; texts: TextChange[] };
|
|
14
|
+
type Stream = { length: number; hash: Hash };
|
|
15
|
+
type Writer = { seeded?: boolean; pending: string[]; bytes: number; dirtyDisk?: boolean; streams: Map<string, Stream>; timings: Map<string, number>; timer?: NodeJS.Timeout; error?: unknown };
|
|
16
|
+
|
|
17
|
+
/** A presentation journal, not an agent/tool replay mechanism. All paths are local to this store. */
|
|
18
|
+
export class TurnPreviewJournal {
|
|
19
|
+
private readonly writers = new Map<string, Writer>();
|
|
20
|
+
private detailExpiry?: NodeJS.Timeout;
|
|
21
|
+
private readonly detailReaders = new Map<string, { projection: TurnPreviewProjection; offset: number; accessed: number }>();
|
|
22
|
+
constructor(readonly directory: string, private readonly onError: (error: unknown) => void = () => {}) {}
|
|
23
|
+
|
|
24
|
+
file(turnId: string): string {
|
|
25
|
+
const hash = createHash("sha256").update(turnId).digest("hex");
|
|
26
|
+
return path.join(this.directory, hash.slice(0, 2), hash + ".jsonl");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
has(turnId: string): boolean { return this.writers.has(turnId) || fs.existsSync(this.file(turnId)); }
|
|
30
|
+
|
|
31
|
+
seed(state: TurnViewState): void {
|
|
32
|
+
const writer = this.writer(state.turnId);
|
|
33
|
+
if (writer.seeded) return;
|
|
34
|
+
this.append(state.turnId, { kind: "seed", state: { ...state, previewJournal: true } });
|
|
35
|
+
writer.seeded = true;
|
|
36
|
+
for (const change of seedTexts(state)) this.track(writer, change);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
state(state: TurnViewState): void {
|
|
40
|
+
this.seed(state);
|
|
41
|
+
// Growing execution fields live only in events. Keep routing/final-delivery metadata here.
|
|
42
|
+
const { activities, reasoningItems, fullToolOutputs, fullToolErrors, toolStatuses,
|
|
43
|
+
completedTools, failedTools, activeTool, assistantText, progressText, plan, fileSummary,
|
|
44
|
+
activitiesTruncated, previewCursor, ...patch } = state;
|
|
45
|
+
this.append(state.turnId, { kind: "state", patch });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
user(turnId: string, id: string, text: string, images?: string[]): void {
|
|
49
|
+
this.append(turnId, { kind: "user", id, text, images });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
event(event: AgentEvent): void {
|
|
53
|
+
const writer = this.writer(event.turnId);
|
|
54
|
+
const texts: TextChange[] = [];
|
|
55
|
+
const copy = { ...event } as AgentEvent;
|
|
56
|
+
const capture = (key: string, field: string, text: string, append = false) => {
|
|
57
|
+
const old = writer.streams.get(key);
|
|
58
|
+
let suffix = text;
|
|
59
|
+
let incremental = append;
|
|
60
|
+
if (!append && old && text.length >= old.length
|
|
61
|
+
&& createHash("sha256").update(text.slice(0, old.length)).digest("hex") === old.hash.copy().digest("hex")) {
|
|
62
|
+
suffix = text.slice(old.length);
|
|
63
|
+
incremental = true;
|
|
64
|
+
}
|
|
65
|
+
this.track(writer, { key, field, append: incremental, text: suffix });
|
|
66
|
+
texts.push({ key, field, append: incremental, text: suffix });
|
|
67
|
+
};
|
|
68
|
+
if (copy.type === "tool_started" || copy.type === "tool_updated") {
|
|
69
|
+
const startedAt = copy.tool.startedAt ?? writer.timings.get(copy.tool.id) ?? Date.now();
|
|
70
|
+
writer.timings.set(copy.tool.id, startedAt);
|
|
71
|
+
copy.tool = { ...copy.tool, startedAt,
|
|
72
|
+
completedAt: copy.tool.status === "running" ? copy.tool.completedAt : copy.tool.completedAt ?? Date.now() };
|
|
73
|
+
for (const field of ["title", "command", "output", "error"] as const) {
|
|
74
|
+
const text = copy.tool[field];
|
|
75
|
+
if (text === undefined) continue;
|
|
76
|
+
capture(`tool:${copy.tool.id}:${field}`, field, text);
|
|
77
|
+
delete (copy.tool as Partial<ToolState>)[field];
|
|
78
|
+
}
|
|
79
|
+
} else if (copy.type === "tool_output_delta") {
|
|
80
|
+
capture(`tool:${copy.toolId}:output`, "delta", copy.delta, true);
|
|
81
|
+
copy.delta = "";
|
|
82
|
+
} else if (copy.type === "progress") {
|
|
83
|
+
capture(copy.reasoning ? `reasoning:${copy.reasoning.itemId}:summary:${copy.reasoning.summaryIndex}`
|
|
84
|
+
: `progress:${copy.activityId ?? "progress"}`, "text", copy.text, copy.append);
|
|
85
|
+
copy.text = "";
|
|
86
|
+
copy.append = false;
|
|
87
|
+
}
|
|
88
|
+
if (copy.type === "reasoning_delta") {
|
|
89
|
+
capture(`reasoning:${copy.itemId}:content:${copy.contentIndex}`, "text", copy.text, true);
|
|
90
|
+
copy.text = "";
|
|
91
|
+
} else if (copy.type === "reasoning_completed") {
|
|
92
|
+
for (const field of ["summary", "content"] as const) {
|
|
93
|
+
copy[field] = copy[field].map((value, index) => {
|
|
94
|
+
capture(`reasoning:${copy.itemId}:${field}:${index}`, `${field}:${index}`, value);
|
|
95
|
+
return "";
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.append(event.turnId, { kind: "event", event: copy, texts });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
flush(turnId?: string, durable = false): void {
|
|
103
|
+
if (!turnId) { for (const id of this.writers.keys()) this.flush(id, durable); return; }
|
|
104
|
+
const writer = this.writers.get(turnId);
|
|
105
|
+
if (!writer) return;
|
|
106
|
+
if (writer.timer) clearTimeout(writer.timer);
|
|
107
|
+
writer.timer = undefined;
|
|
108
|
+
if (writer.error) throw writer.error;
|
|
109
|
+
if (!writer.bytes && !(durable && writer.dirtyDisk)) return;
|
|
110
|
+
const file = this.file(turnId);
|
|
111
|
+
let fd: number | undefined;
|
|
112
|
+
let size = 0;
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
115
|
+
fd = fs.openSync(file, "a");
|
|
116
|
+
size = fs.fstatSync(fd).size;
|
|
117
|
+
if (writer.bytes) fs.writeFileSync(fd, writer.pending.join(""));
|
|
118
|
+
writer.dirtyDisk = true;
|
|
119
|
+
if (durable) { fs.fsyncSync(fd); writer.dirtyDisk = false; }
|
|
120
|
+
writer.pending = [];
|
|
121
|
+
writer.bytes = 0;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// Never continue after a partial append: the journal must remain a valid prefix.
|
|
124
|
+
if (fd !== undefined) { try { fs.ftruncateSync(fd, size); } catch { /* Fail closed below. */ } }
|
|
125
|
+
writer.pending = [];
|
|
126
|
+
writer.bytes = 0;
|
|
127
|
+
writer.error = error;
|
|
128
|
+
throw error;
|
|
129
|
+
} finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
release(turnId: string): void {
|
|
133
|
+
this.flush(turnId, true);
|
|
134
|
+
this.writers.delete(turnId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
close(): void {
|
|
138
|
+
try { this.flush(undefined, true); } finally {
|
|
139
|
+
for (const writer of this.writers.values()) if (writer.timer) clearTimeout(writer.timer);
|
|
140
|
+
this.writers.clear();
|
|
141
|
+
this.detailReaders.clear();
|
|
142
|
+
if (this.detailExpiry) clearTimeout(this.detailExpiry);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Bounded byte cursor; a partial final record is ignored until its newline is present. */
|
|
147
|
+
read(turnId: string, offset = 0, maxBytes = 256 * 1024, through = Infinity): { records: PreviewRecord[]; offset: number; end: boolean } {
|
|
148
|
+
const file = this.file(turnId);
|
|
149
|
+
if (!fs.existsSync(file)) return { records: [], offset, end: true };
|
|
150
|
+
const fd = fs.openSync(file, "r");
|
|
151
|
+
try {
|
|
152
|
+
const size = Math.min(fs.fstatSync(fd).size, through);
|
|
153
|
+
const records: PreviewRecord[] = [];
|
|
154
|
+
const start = offset;
|
|
155
|
+
let position = offset;
|
|
156
|
+
let parts: Buffer[] = [];
|
|
157
|
+
while (position < size) {
|
|
158
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, size - position));
|
|
159
|
+
const count = fs.readSync(fd, chunk, 0, chunk.length, position);
|
|
160
|
+
if (!count) break;
|
|
161
|
+
position += count;
|
|
162
|
+
let begin = 0;
|
|
163
|
+
let newline: number;
|
|
164
|
+
while ((newline = chunk.indexOf(10, begin)) !== -1 && newline < count) {
|
|
165
|
+
parts.push(chunk.subarray(begin, newline));
|
|
166
|
+
const line = Buffer.concat(parts);
|
|
167
|
+
records.push(JSON.parse(line.toString("utf8")) as PreviewRecord);
|
|
168
|
+
offset += line.length + 1;
|
|
169
|
+
parts = [];
|
|
170
|
+
begin = newline + 1;
|
|
171
|
+
}
|
|
172
|
+
if (begin < count) parts.push(chunk.subarray(begin, count));
|
|
173
|
+
if (records.length && position - start >= maxBytes) break;
|
|
174
|
+
}
|
|
175
|
+
return { records, offset, end: position >= size };
|
|
176
|
+
} finally { fs.closeSync(fd); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
load(turnId: string, details: boolean | string = true): TurnViewState | undefined {
|
|
180
|
+
this.flush(turnId);
|
|
181
|
+
const cacheKey = `${turnId}:${String(details)}`;
|
|
182
|
+
const cached = typeof details === "string" ? this.detailReaders.get(cacheKey) : undefined;
|
|
183
|
+
const projection = cached?.projection ?? new TurnPreviewProjection(details);
|
|
184
|
+
let offset = cached?.offset ?? 0;
|
|
185
|
+
for (;;) {
|
|
186
|
+
const batch = this.read(turnId, offset);
|
|
187
|
+
for (const record of batch.records) projection.apply(record, ++projection.revision);
|
|
188
|
+
if (batch.end || batch.offset === offset) {
|
|
189
|
+
if (typeof details === "string") {
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
for (const [key, value] of this.detailReaders) if (now - value.accessed > 60_000) this.detailReaders.delete(key);
|
|
192
|
+
this.detailReaders.delete(cacheKey);
|
|
193
|
+
if (projection.retainedTextBytes() < 16 * 1024 * 1024)
|
|
194
|
+
this.detailReaders.set(cacheKey, { projection, offset: batch.offset, accessed: now });
|
|
195
|
+
if (this.detailExpiry) clearTimeout(this.detailExpiry);
|
|
196
|
+
this.detailExpiry = setTimeout(() => this.detailReaders.clear(), 60_000);
|
|
197
|
+
this.detailExpiry.unref();
|
|
198
|
+
while (this.detailReaders.size > 4) this.detailReaders.delete(this.detailReaders.keys().next().value!);
|
|
199
|
+
}
|
|
200
|
+
return projection.state ? { ...projection.state, previewCursor: batch.offset } : undefined;
|
|
201
|
+
}
|
|
202
|
+
offset = batch.offset;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
outputAppend(turnId: string, toolId: string, after: number, through: number): string | undefined {
|
|
207
|
+
if (!Number.isSafeInteger(after) || after <= 0 || after > through) return undefined;
|
|
208
|
+
let offset = after;
|
|
209
|
+
const parts: string[] = [];
|
|
210
|
+
while (offset < through) {
|
|
211
|
+
const batch = this.read(turnId, offset, 256 * 1024, through);
|
|
212
|
+
if (batch.offset === offset) return undefined;
|
|
213
|
+
for (const record of batch.records) {
|
|
214
|
+
if (record.kind === "seed") return undefined;
|
|
215
|
+
if (record.kind !== "event") continue;
|
|
216
|
+
const event = record.event;
|
|
217
|
+
if ((event.type === "tool_started" || event.type === "tool_updated") && event.tool.id === toolId
|
|
218
|
+
&& (event.tool.files || event.tool.imagePath)) return undefined;
|
|
219
|
+
for (const change of record.texts) {
|
|
220
|
+
if (!change.key.startsWith(`tool:${toolId}:`)) continue;
|
|
221
|
+
if (change.key === `tool:${toolId}:output` && change.append) parts.push(change.text);
|
|
222
|
+
else if (change.text || !change.append) return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
offset = batch.offset;
|
|
226
|
+
}
|
|
227
|
+
return parts.join("");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private track(writer: Writer, change: TextChange): void {
|
|
231
|
+
const old = change.append ? writer.streams.get(change.key) : undefined;
|
|
232
|
+
const stream = old ?? { length: 0, hash: createHash("sha256") };
|
|
233
|
+
stream.hash.update(change.text);
|
|
234
|
+
stream.length += change.text.length;
|
|
235
|
+
writer.streams.set(change.key, stream);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private writer(turnId: string): Writer {
|
|
239
|
+
let writer = this.writers.get(turnId);
|
|
240
|
+
if (writer) return writer;
|
|
241
|
+
writer = { pending: [], bytes: 0, streams: new Map(), timings: new Map() };
|
|
242
|
+
// On first write after restart, rebuild only stream fingerprints, not the full execution.
|
|
243
|
+
let offset = 0;
|
|
244
|
+
if (fs.existsSync(this.file(turnId))) {
|
|
245
|
+
for (;;) {
|
|
246
|
+
const batch = this.read(turnId, offset);
|
|
247
|
+
for (const record of batch.records) {
|
|
248
|
+
if (record.kind === "event") {
|
|
249
|
+
for (const change of record.texts) this.track(writer, change);
|
|
250
|
+
const event = record.event;
|
|
251
|
+
if ((event.type === "tool_started" || event.type === "tool_updated") && event.tool.startedAt !== undefined)
|
|
252
|
+
writer.timings.set(event.tool.id, event.tool.startedAt);
|
|
253
|
+
}
|
|
254
|
+
if (record.kind === "seed") {
|
|
255
|
+
writer.seeded = true;
|
|
256
|
+
for (const change of seedTexts(record.state)) this.track(writer, change);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
offset = batch.offset;
|
|
260
|
+
if (batch.end || !batch.records.length) break;
|
|
261
|
+
}
|
|
262
|
+
const size = fs.statSync(this.file(turnId)).size;
|
|
263
|
+
if (offset < size) fs.truncateSync(this.file(turnId), offset);
|
|
264
|
+
}
|
|
265
|
+
this.writers.set(turnId, writer);
|
|
266
|
+
return writer;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private append(turnId: string, record: PreviewRecord): void {
|
|
270
|
+
const writer = this.writer(turnId);
|
|
271
|
+
if (writer.error) throw writer.error;
|
|
272
|
+
const line = JSON.stringify(record) + "\n";
|
|
273
|
+
writer.pending.push(line);
|
|
274
|
+
writer.bytes += Buffer.byteLength(line);
|
|
275
|
+
if (writer.bytes >= 256 * 1024) this.flush(turnId);
|
|
276
|
+
else if (!writer.timer) {
|
|
277
|
+
writer.timer = setTimeout(() => {
|
|
278
|
+
try { this.flush(turnId); } catch (error) { this.onError(error); }
|
|
279
|
+
}, 250);
|
|
280
|
+
writer.timer.unref();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Built only when a human opens history. SSE keeps summaries, not full tool output, in memory. */
|
|
286
|
+
export class TurnPreviewProjection {
|
|
287
|
+
state?: TurnViewState;
|
|
288
|
+
revision = 0;
|
|
289
|
+
readonly changed = new Set<string>();
|
|
290
|
+
readonly removed = new Set<string>();
|
|
291
|
+
private readonly text = new Map<string, string>();
|
|
292
|
+
constructor(private readonly details: boolean | string = false) {}
|
|
293
|
+
retainedTextBytes(): number {
|
|
294
|
+
let bytes = 0;
|
|
295
|
+
for (const value of this.text.values()) bytes += value.length * 2;
|
|
296
|
+
return bytes;
|
|
297
|
+
}
|
|
298
|
+
private wantsFull(key: string): boolean {
|
|
299
|
+
return this.details === true || (typeof this.details === "string" && key.startsWith(`${this.details}:`));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
apply(record: PreviewRecord, revision: number): void {
|
|
303
|
+
if (record.kind === "seed") {
|
|
304
|
+
this.state = { ...record.state, activities: [...record.state.activities], activitiesTruncated: false };
|
|
305
|
+
for (const change of seedTexts(record.state)) this.text.set(change.key, this.wantsFull(change.key) ? change.text : change.text.slice(0, 6000));
|
|
306
|
+
if (this.details !== true) {
|
|
307
|
+
this.state = { ...this.state, fullToolOutputs: {}, fullToolErrors: {}, activities: this.state.activities.map((a) => a.kind === "tool"
|
|
308
|
+
? { ...a, tool: { ...a.tool, output: a.tool.output?.slice(0, 6000), error: a.tool.error?.slice(0, 6000) } } : a) };
|
|
309
|
+
}
|
|
310
|
+
if (typeof this.details === "string" && this.details.startsWith("tool:")) {
|
|
311
|
+
const id = this.details.slice(5);
|
|
312
|
+
const tool = record.state.activities.find((a) => a.kind === "tool" && a.id === id);
|
|
313
|
+
if (tool?.kind === "tool") {
|
|
314
|
+
this.state.activities = this.state.activities.map((a) => a.id === id ? tool : a);
|
|
315
|
+
this.state.fullToolOutputs = { [id]: record.state.fullToolOutputs?.[id] ?? tool.tool.output ?? "" };
|
|
316
|
+
this.state.fullToolErrors = { [id]: record.state.fullToolErrors?.[id] ?? tool.tool.error ?? "" };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const a of record.state.activities) this.changed.add(a.id);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!this.state) return;
|
|
323
|
+
if (record.kind === "state") {
|
|
324
|
+
this.state = { ...this.state, ...record.patch, historyDetail: "full", previewJournal: true };
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (record.kind === "user") {
|
|
328
|
+
const user = { kind: "user" as const, id: record.id, text: record.text, localImagePaths: record.images };
|
|
329
|
+
const index = this.state.activities.findIndex((a) => a.id === record.id);
|
|
330
|
+
this.state.activities = [...this.state.activities];
|
|
331
|
+
if (index < 0) this.state.activities.push(user); else this.state.activities[index] = user;
|
|
332
|
+
this.changed.add(record.id);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
let event = { ...record.event } as AgentEvent;
|
|
336
|
+
if (event.type === "tool_started" || event.type === "tool_updated") event.tool = { ...event.tool };
|
|
337
|
+
if (event.type === "reasoning_completed") event = { ...event, summary: [...event.summary], content: [...event.content] };
|
|
338
|
+
for (const change of record.texts) {
|
|
339
|
+
let value = (change.append ? this.text.get(change.key) ?? "" : "") + change.text;
|
|
340
|
+
const full = this.wantsFull(change.key);
|
|
341
|
+
if (!full && (change.key.startsWith("tool:") || change.key.startsWith("reasoning:"))) value = value.slice(0, 6000);
|
|
342
|
+
this.text.set(change.key, value);
|
|
343
|
+
if (event.type === "tool_started" || event.type === "tool_updated") {
|
|
344
|
+
(event.tool as unknown as Record<string, unknown>)[change.field] = value;
|
|
345
|
+
} else if (event.type === "progress") event.text = value;
|
|
346
|
+
else if (event.type === "reasoning_delta") event.text = change.text;
|
|
347
|
+
else if (event.type === "reasoning_completed") {
|
|
348
|
+
const [field, index] = change.field.split(":");
|
|
349
|
+
event[field as "summary" | "content"][Number(index)] = value;
|
|
350
|
+
}
|
|
351
|
+
else if (event.type === "tool_output_delta") {
|
|
352
|
+
const toolId = event.toolId;
|
|
353
|
+
const activity = this.state.activities.find((a) => a.kind === "tool" && a.id === toolId);
|
|
354
|
+
if (activity?.kind === "tool") event = { type: "tool_updated", sessionId: event.sessionId, turnId: event.turnId,
|
|
355
|
+
tool: { ...activity.tool, output: value } };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (event.type === "tool_started" || event.type === "tool_updated") {
|
|
359
|
+
const toolId = event.tool.id;
|
|
360
|
+
const previous = this.state.activities.find((a) => a.kind === "tool" && a.id === toolId);
|
|
361
|
+
if (previous?.kind === "tool") event.tool = { ...previous.tool, ...event.tool };
|
|
362
|
+
event.tool.previewRevision = revision;
|
|
363
|
+
this.changed.add(event.tool.id);
|
|
364
|
+
} else if (event.type === "progress") {
|
|
365
|
+
this.changed.add(event.activityId ?? "progress");
|
|
366
|
+
if (event.reasoning) this.changed.add(`reasoning:${event.reasoning.itemId}`);
|
|
367
|
+
}
|
|
368
|
+
else if (event.type === "agent_text_delta" && event.replacesActivityId) this.removed.add(event.replacesActivityId);
|
|
369
|
+
if (event.type === "reasoning_delta" || event.type === "reasoning_completed") this.changed.add(`reasoning:${event.itemId}`);
|
|
370
|
+
const old = this.state;
|
|
371
|
+
this.state = reduceTurnEvent(old, event, true);
|
|
372
|
+
if (["completed", "failed", "cancelled"].includes(old.status) && !event.type.startsWith("turn_")) this.state.status = old.status;
|
|
373
|
+
const reasoningId = event.type === "reasoning_delta" || event.type === "reasoning_completed" ? event.itemId
|
|
374
|
+
: event.type === "progress" ? event.reasoning?.itemId : undefined;
|
|
375
|
+
if (reasoningId) this.state.reasoningItems = this.state.reasoningItems?.map((r) => r.itemId === reasoningId ? { ...r, previewRevision: revision } : r);
|
|
376
|
+
if (event.type === "context_compaction") {
|
|
377
|
+
for (const a of this.state.activities) if (!old.activities.includes(a)) this.changed.add(a.id);
|
|
378
|
+
}
|
|
379
|
+
if (this.details !== true && this.state.reasoningItems) this.state.reasoningItems = this.state.reasoningItems.map((r) => ({ ...r,
|
|
380
|
+
summary: r.summary.map((t) => this.wantsFull(`reasoning:${r.itemId}:`) ? t : t.slice(0, 6000)), content: r.content.map((t) => this.wantsFull(`reasoning:${r.itemId}:`) ? t : t.slice(0, 6000)) }));
|
|
381
|
+
this.state.activitiesTruncated = false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function seedTexts(state: TurnViewState): TextChange[] {
|
|
386
|
+
const result: TextChange[] = [];
|
|
387
|
+
for (const a of state.activities) {
|
|
388
|
+
if (a.kind === "tool") for (const field of ["title", "command", "output", "error"] as const) {
|
|
389
|
+
const text = (field === "output" ? state.fullToolOutputs?.[a.id] : field === "error" ? state.fullToolErrors?.[a.id] : undefined) ?? a.tool[field];
|
|
390
|
+
if (text !== undefined) result.push({ key: `tool:${a.id}:${field}`, field, append: false, text });
|
|
391
|
+
}
|
|
392
|
+
else if (a.kind !== "user") result.push({ key: `progress:${a.id}`, field: "text", append: false, text: a.text });
|
|
393
|
+
}
|
|
394
|
+
for (const item of state.reasoningItems ?? []) for (const field of ["summary", "content"] as const) {
|
|
395
|
+
item[field].forEach((text, i) => result.push({ key: `reasoning:${item.itemId}:${field}:${i}`, field: `${field}:${i}`, append: false, text }));
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
package/src/state/migrations.ts
CHANGED
|
@@ -207,4 +207,14 @@ export const migrations = [
|
|
|
207
207
|
);
|
|
208
208
|
CREATE INDEX IF NOT EXISTS idx_update_notices_status ON update_notices(status);
|
|
209
209
|
`,
|
|
210
|
+
`
|
|
211
|
+
CREATE TABLE IF NOT EXISTS recent_context_receipts (
|
|
212
|
+
prompt_message_id TEXT NOT NULL,
|
|
213
|
+
local_session_id TEXT NOT NULL,
|
|
214
|
+
remote_session_id TEXT NOT NULL,
|
|
215
|
+
context_key TEXT NOT NULL,
|
|
216
|
+
message_id TEXT NOT NULL,
|
|
217
|
+
PRIMARY KEY (local_session_id, remote_session_id, context_key, message_id)
|
|
218
|
+
);
|
|
219
|
+
`,
|
|
210
220
|
] as const;
|
|
@@ -5,11 +5,11 @@ import { z } from "zod";
|
|
|
5
5
|
import { loadConfig } from "../config/loadConfig.js";
|
|
6
6
|
import { controlEndpoint } from "../cli/controlProtocol.js";
|
|
7
7
|
import { finalizeSelfUpdatePlan, prepareSelfUpdate, releaseSelfUpdatePlan, requireNpmSelfUpdateInstallation } from "../cli/SelfUpdater.js";
|
|
8
|
-
import { stableVersionSchema } from "./PublishedRelease.js";
|
|
8
|
+
import { publishedVersionSchema, stableVersionSchema } from "./PublishedRelease.js";
|
|
9
9
|
|
|
10
10
|
const resultSchema = z.discriminatedUnion("status", [
|
|
11
11
|
z.object({ status: z.literal("current") }),
|
|
12
|
-
z.object({ status: z.literal("prepared"), planPath: z.string().min(1), targetVersion:
|
|
12
|
+
z.object({ status: z.literal("prepared"), planPath: z.string().min(1), targetVersion: publishedVersionSchema }),
|
|
13
13
|
]);
|
|
14
14
|
const runFile = promisify(execFile);
|
|
15
15
|
type RunPreparation = (file: string, args: string[], options: ExecFileOptionsWithStringEncoding) => Promise<{ stdout: string }>;
|
|
@@ -32,11 +32,24 @@ export async function prepareAutomaticUpdate(
|
|
|
32
32
|
return result;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async function
|
|
35
|
+
export async function prepareSelectedUpdate(
|
|
36
|
+
version: string,
|
|
37
|
+
profile: UpdateProfile,
|
|
38
|
+
run: RunPreparation = runFile,
|
|
39
|
+
): Promise<z.infer<typeof resultSchema>> {
|
|
40
|
+
publishedVersionSchema.parse(version);
|
|
41
|
+
const result = resultSchema.parse(await runPreparer("--manual", profile, run, version));
|
|
42
|
+
if (result.status === "prepared" && result.targetVersion !== version) {
|
|
43
|
+
throw new Error("The prepared update does not match the selected version.");
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runPreparer(argument: string, profile: UpdateProfile, run: RunPreparation, version?: string): Promise<unknown> {
|
|
36
49
|
const environment: NodeJS.ProcessEnv = { ...process.env, AGENT_BOT_HOME: profile.home, AGENT_BOT_CONFIG: profile.configPath };
|
|
37
50
|
delete environment.AGENT_BOT;
|
|
38
51
|
// npm preparation is synchronous and can take minutes; keep it off the live worker's event loop.
|
|
39
|
-
const { stdout } = await run(process.execPath, [fileURLToPath(import.meta.url), argument], {
|
|
52
|
+
const { stdout } = await run(process.execPath, [fileURLToPath(import.meta.url), argument, ...(version ? [version] : [])], {
|
|
40
53
|
env: environment, windowsHide: true, encoding: "utf8", maxBuffer: 1_000_000,
|
|
41
54
|
});
|
|
42
55
|
return JSON.parse(String(stdout)) as unknown;
|
|
@@ -48,9 +61,10 @@ async function main(): Promise<void> {
|
|
|
48
61
|
process.stdout.write(JSON.stringify({ status: "supported" }));
|
|
49
62
|
return;
|
|
50
63
|
}
|
|
51
|
-
const
|
|
64
|
+
const manual = process.argv[2] === "--manual";
|
|
65
|
+
const version = manual ? publishedVersionSchema.parse(process.argv[3]) : stableVersionSchema.parse(process.argv[2]);
|
|
52
66
|
const config = loadConfig();
|
|
53
|
-
const prepared = await prepareSelfUpdate({ version
|
|
67
|
+
const prepared = await prepareSelfUpdate({ version });
|
|
54
68
|
if (prepared.status === "prepared") {
|
|
55
69
|
try {
|
|
56
70
|
finalizeSelfUpdatePlan(prepared.planPath, {
|
|
@@ -58,7 +72,7 @@ async function main(): Promise<void> {
|
|
|
58
72
|
databasePath: config.storage.sqlitePath,
|
|
59
73
|
restartService: true,
|
|
60
74
|
workingDirectory: process.cwd(),
|
|
61
|
-
reason: `Agent Bot
|
|
75
|
+
reason: `Agent Bot ${manual ? "更新" : "自动更新"}到 ${version}`,
|
|
62
76
|
});
|
|
63
77
|
} catch (error) {
|
|
64
78
|
releaseSelfUpdatePlan(prepared.planPath);
|