@chatcode/chatcode-cli 3.0.4-rc.7 → 3.0.5-rc.1
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/README.i18n.yaml +6 -6
- package/README.md +10 -4
- package/README.zh.md +10 -4
- package/lib/bin.js +23 -9
- package/lib/{command-errors-Ca64Lgba.js → command-errors-CsRCh0gk.js} +12 -12
- package/lib/{command-trees-CVc4Xu_z.js → command-trees-CD_43rxX.js} +2 -2
- package/lib/{component-identity-6aEut5Yt.js → component-identity-DGA2-TeB.js} +3 -3
- package/lib/{dump-config-ZzrfvtnX.js → dump-config-D8kXfdTs.js} +1 -1
- package/lib/dump-config-DuCc-QB7.js +2 -0
- package/lib/{dump-config-schema-BlO-eZ-J.js → dump-config-schema-CtZbroi6.js} +2 -2
- package/lib/{extension-events-C7MUpqVE.js → extension-events-D0BjYRqT.js} +21 -21
- package/lib/{host-access-CLbyAGT5.js → host-access-DLJjuR07.js} +1 -1
- package/lib/{keymap-82JwLLHe.js → keymap-C4cs2IBN.js} +2 -2
- package/lib/{plugin-BlQlWIR5.js → plugin-CGtTByud.js} +801 -456
- package/lib/{plugin-BZdazFXS.js → plugin-CYw-MBRn.js} +1 -1
- package/lib/{profile-boot-B0aEEQIR.js → profile-boot-CffsubnV.js} +7 -7
- package/lib/profile-boot.js +1 -1
- package/lib/{scenes-BNS0ZFyQ.js → scenes-CbAGK5Uy.js} +3 -3
- package/lib/{settings-sections-CXH24cu5.js → settings-sections-DYKDa04e.js} +3 -3
- package/lib/{statusBarSettings-BAMzn8fX.js → statusBarSettings-T1m8xad4.js} +3 -3
- package/lib/{themes-DXUIvP7m.js → themes-BY2ui1Dv.js} +12 -12
- package/lib/tui-compat-command-trees.js +1 -1
- package/lib/tui-compat-extensions.js +3 -3
- package/lib/tui-compat-plugin-host.js +5 -5
- package/lib/tui-compat-scenes.js +1 -1
- package/lib/tui-compat-settings-sections.js +1 -1
- package/lib/tui-compat-working-activity.js +1 -1
- package/lib/tui-compat-workspaces.js +1 -1
- package/lib/tui-compat.js +7 -7
- package/lib/types/bin.d.ts +9 -0
- package/lib/types/tui-compat.d.ts +1 -1
- package/lib/types/web-instance-process.d.ts +20 -0
- package/lib/{web-instance-DbP7IHPv.js → web-instance-D_Ppi2Zo.js} +176 -14
- package/lib/{web-instance-B0jec7cq.js → web-instance-amvCJWnq.js} +1 -1
- package/lib/{workspaces-CUJXNU5W.js → workspaces-D5VKbESy.js} +2 -2
- package/package.json +28 -29
- package/runtime/manifest.json +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-agent-default-model/lib/index.js +7 -3
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/client.js +11 -3
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/contract/snapshot.d.ts +1 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/assistant-stream.d.ts +2 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/assistant-stream.js +4 -0
- package/runtime/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/session.js +10 -3
- package/runtime/node_modules/@deepseek-ai/dsh-app-boot/lib/index.js +32 -11
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/index.js +62 -11
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/darwin-lock.d.ts +7 -0
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/index.d.ts +7 -2
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/linux-lock.d.ts +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-locale/lib/client.js +7 -7
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-agent-preset/lib/client.js +62 -62
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-approval/lib/client.js +10 -10
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-attachment/lib/client.js +49 -49
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js +204 -204
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-commands/lib/client.js +16 -16
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js +159 -157
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/client/service.d.ts +4 -2
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-cordis/lib/client.js +74 -74
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/client.js +129 -123
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/index.js +19 -14
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/types/changes.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/types/client/Deliverables.d.ts +1 -2
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/types/client/locales.d.ts +2 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/types/client/present-open.d.ts +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-deliverables/package.json +2 -4
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-directory-picker-browse/lib/client.js +39 -39
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-goal/lib/client.js +16 -16
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-input-trigger/lib/client.js +23 -23
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-jobs/lib/client.js +37 -37
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-layout/lib/client.js +9 -9
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-message-feedback/lib/client.js +12 -12
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/client.js +119 -32
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/types/client/catalog.d.ts +13 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/types/client/directory.d.ts +2 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/types/client/effort-preferences.d.ts +20 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/types/client/slots.d.ts +2 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-open-in-app/lib/client.js +8 -8
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-permission-presets/lib/client.js +18 -18
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-plan/lib/client.js +22 -22
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-plugin-manager/lib/client.js +128 -128
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-schedule/lib/client.js +182 -182
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-account/lib/client.js +130 -130
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-general/lib/client.js +50 -50
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-models/lib/client.js +88 -88
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-plugin-inventory/lib/client.js +36 -36
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js +13 -13
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-subagent/lib/client.js +27 -27
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-shortcuts/lib/client.js +42 -42
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar/lib/client.js +44 -46
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar-browser/lib/client.js +25 -25
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/client.excel.js +9 -9
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/client.js +70 -70
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/client.pdf.js +9 -9
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar-files/lib/client.js +18 -18
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar-right/lib/client.js +26 -26
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar-terminal/lib/client.js +16 -16
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar-terminal/lib/client.terminal.js +11 -11
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-skill/lib/client.js +20 -20
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-subagent/lib/client.js +37 -37
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-theme/lib/client.js +27 -27
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-tool/lib/client.js +95 -95
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-trajectory/lib/client.js +197 -197
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-user-questions/lib/client.js +49 -49
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workflow-run/lib/client.js +24 -24
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/client.js +656 -152
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/ArchivedSessions.d.ts +9 -2
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/locales.d.ts +46 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/session-actions/DeleteSession.d.ts +9 -10
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/types/client/session-actions/deletion.d.ts +57 -0
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-workspace/package.json +2 -0
- package/runtime/node_modules/@deepseek-ai/dsh-experimental-client-ui-agent-team/lib/client.js +32 -32
- package/runtime/node_modules/@deepseek-ai/dsh-experimental-client-ui-voice-input/lib/client.js +24 -24
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/index.js +899 -453
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/adapter.d.ts +4 -20
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/chat-completions/adapter.d.ts +8 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/chat-completions/serialize.d.ts +10 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/chat-completions/sse.d.ts +24 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/chat-completions/translate.d.ts +37 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/chat-completions/types.d.ts +159 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/file-store.d.ts +3 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/files-api.d.ts +4 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/index.d.ts +1 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/protocol-adapter.d.ts +41 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/protocol.d.ts +35 -0
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/transport.d.ts +2 -1
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/types.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-mcp-client/lib/index.js +5 -4
- package/runtime/node_modules/@deepseek-ai/dsh-session-log-export/lib/client.js +4 -4
- package/runtime/node_modules/@deepseek-ai/dsh-session-log-export/lib/index.js +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-session-persistence-jsonl/lib/index.js +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-tool-cordis/lib/types/api-catalog.js +12 -10
- package/runtime/node_modules/@deepseek-ai/dsh-workspace-changes/lib/index.js +208 -39
- package/runtime/node_modules/@deepseek-ai/dsh-workspace-changes/lib/types/archive.d.ts +47 -0
- package/runtime/node_modules/@deepseek-ai/dsh-workspace-changes/lib/types/archive.js +129 -0
- package/runtime/node_modules/@deepseek-ai/dsh-workspace-changes/lib/types/index.d.ts +2 -0
- package/runtime/node_modules/@deepseek-ai/dsh-workspace-changes/lib/types/index.js +31 -8
- package/runtime/node_modules/@deepseek-ai/dsh-workspace-changes/lib/types/recorder.d.ts +7 -2
- package/runtime/node_modules/@deepseek-ai/dsh-workspace-changes/lib/types/recorder.js +35 -19
- package/runtime/node_modules/@deepseek-ai/dsh-workspace-changes/lib/types/types.d.ts +11 -10
- package/runtime/node_modules/@deepseek-ai/dsh-workspace-changes/package.json +1 -0
- package/scripts/link-embedded-runtime.mjs +1 -1
- package/lib/dump-config-CDixiJNt.js +0 -2
- package/lib/{home-migration-BjDmRwBB.js → home-migration-lRjtl2fP.js} +1 -1
- package/lib/{startup-diagnostics-C_hRCp-q.js → startup-diagnostics-4mLHYTFc.js} +1 -1
|
@@ -1,9 +1,146 @@
|
|
|
1
1
|
import { homedir, tmpdir } from "node:os";
|
|
2
|
-
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { copyFile, mkdir, mkdtemp, open, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
4
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
import { chatCodeCliHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
+
import { copyFile, mkdir, mkdtemp, open, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { gunzip, gzip } from "node:zlib";
|
|
6
9
|
import { structuredPatch } from "diff";
|
|
10
|
+
//#region lib/types/archive.js
|
|
11
|
+
/** Durable change summaries and compressed file comparisons, independent of temporary git snapshots. */
|
|
12
|
+
const compress = promisify(gzip);
|
|
13
|
+
const decompress = promisify(gunzip);
|
|
14
|
+
/** Stores completed turn records until their Session is permanently deleted. */
|
|
15
|
+
var ChangesArchive = class {
|
|
16
|
+
directory;
|
|
17
|
+
constructor(directory) {
|
|
18
|
+
this.directory = directory;
|
|
19
|
+
}
|
|
20
|
+
sessionDirectory(sessionId) {
|
|
21
|
+
return join(this.directory, createHash("sha256").update(sessionId).digest("hex"));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Save the summary and each listed comparison before publishing its event.
|
|
25
|
+
* @param sessionId - owning Session.
|
|
26
|
+
* @param summary - complete summary, including the Host working directory.
|
|
27
|
+
* @param diff - resolves one comparison from the turn's immutable snapshots.
|
|
28
|
+
* @param signal - cancels staging before the event is appended.
|
|
29
|
+
* @returns a staged record to commit with the event's sequence and then dispose.
|
|
30
|
+
*/
|
|
31
|
+
async stage(sessionId, summary, diff, signal) {
|
|
32
|
+
const parent = this.sessionDirectory(sessionId);
|
|
33
|
+
await mkdir(parent, {
|
|
34
|
+
recursive: true,
|
|
35
|
+
mode: 448
|
|
36
|
+
});
|
|
37
|
+
const staging = await mkdtemp(join(parent, ".pending-"));
|
|
38
|
+
try {
|
|
39
|
+
await writeFile(join(staging, "summary.json"), JSON.stringify({
|
|
40
|
+
version: 1,
|
|
41
|
+
summary
|
|
42
|
+
}), {
|
|
43
|
+
mode: 384,
|
|
44
|
+
signal
|
|
45
|
+
});
|
|
46
|
+
for (let index = 0; index < summary.files.length; index += 1) {
|
|
47
|
+
signal.throwIfAborted();
|
|
48
|
+
const bytes = await compress(JSON.stringify(await diff(index)));
|
|
49
|
+
await writeFile(join(staging, `${index}.json.gz`), bytes, {
|
|
50
|
+
mode: 384,
|
|
51
|
+
signal
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
signal.throwIfAborted();
|
|
55
|
+
return {
|
|
56
|
+
commit: (seq) => rename(staging, join(parent, String(seq))),
|
|
57
|
+
dispose: () => rm(staging, {
|
|
58
|
+
recursive: true,
|
|
59
|
+
force: true
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
await rm(staging, {
|
|
64
|
+
recursive: true,
|
|
65
|
+
force: true
|
|
66
|
+
});
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read an archived summary without opening a live Session or its repository.
|
|
72
|
+
* @param sessionId - owning Session.
|
|
73
|
+
* @param seq - announcing event sequence.
|
|
74
|
+
* @returns the validated summary, or undefined when no archive exists.
|
|
75
|
+
*/
|
|
76
|
+
async summary(sessionId, seq) {
|
|
77
|
+
const bytes = await readMissing(join(this.sessionDirectory(sessionId), String(seq), "summary.json"));
|
|
78
|
+
if (bytes === void 0) return void 0;
|
|
79
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
80
|
+
if (!record(value) || value.version !== 1 || !isSummary(value.summary)) throw new Error("Invalid workspace change archive summary");
|
|
81
|
+
return value.summary;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read a stored comparison independently of later workspace edits or git cleanup.
|
|
85
|
+
* @param sessionId - owning Session.
|
|
86
|
+
* @param seq - announcing event sequence.
|
|
87
|
+
* @param index - listed file index.
|
|
88
|
+
* @param signal - cancellation.
|
|
89
|
+
* @returns the validated comparison, or undefined for a missing record or index.
|
|
90
|
+
*/
|
|
91
|
+
async diff(sessionId, seq, index, signal) {
|
|
92
|
+
const bytes = await readMissing(join(this.sessionDirectory(sessionId), String(seq), `${index}.json.gz`), signal);
|
|
93
|
+
if (bytes === void 0) return void 0;
|
|
94
|
+
const value = JSON.parse((await decompress(bytes)).toString("utf8"));
|
|
95
|
+
signal.throwIfAborted();
|
|
96
|
+
if (!isDiff(value)) throw new Error("Invalid workspace change archive comparison");
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Remove secondary records when the Session is permanently deleted.
|
|
101
|
+
* @param sessionId - deleted Session.
|
|
102
|
+
* @returns after its archive directory has been removed.
|
|
103
|
+
*/
|
|
104
|
+
remove(sessionId) {
|
|
105
|
+
return rm(this.sessionDirectory(sessionId), {
|
|
106
|
+
recursive: true,
|
|
107
|
+
force: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
async function readMissing(file, signal) {
|
|
112
|
+
try {
|
|
113
|
+
return await readFile(file, signal === void 0 ? {} : { signal });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (record(error) && error.code === "ENOENT") return void 0;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function record(value) {
|
|
120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
+
}
|
|
122
|
+
function count(value) {
|
|
123
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
124
|
+
}
|
|
125
|
+
function pathFields(value) {
|
|
126
|
+
return typeof value.path === "string" && value.path.length > 0 && typeof value.display === "string" && value.display.length > 0;
|
|
127
|
+
}
|
|
128
|
+
function isSummary(value) {
|
|
129
|
+
if (!record(value)) return false;
|
|
130
|
+
const { turn, cwd, files, total, added, deleted, snapshot } = value;
|
|
131
|
+
return count(turn) && turn > 0 && typeof cwd === "string" && isAbsolute(cwd) && count(total) && count(added) && count(deleted) && Array.isArray(files) && files.length <= total && files.every((file) => record(file) && pathFields(file) && count(file.added) && count(file.deleted) && (file.binary === void 0 || file.binary === true) && (file.oversized === void 0 || file.oversized === true)) && (snapshot === void 0 || record(snapshot) && typeof snapshot.before === "string" && typeof snapshot.after === "string");
|
|
132
|
+
}
|
|
133
|
+
function isDiff(value) {
|
|
134
|
+
if (!record(value) || !pathFields(value)) return false;
|
|
135
|
+
if (value.kind === "binary" || value.kind === "oversized") return true;
|
|
136
|
+
return value.kind === "text" && typeof value.before === "boolean" && typeof value.after === "boolean" && typeof value.coarse === "boolean" && Array.isArray(value.hunks) && value.hunks.every((hunk) => record(hunk) && [
|
|
137
|
+
hunk.oldStart,
|
|
138
|
+
hunk.oldLines,
|
|
139
|
+
hunk.newStart,
|
|
140
|
+
hunk.newLines
|
|
141
|
+
].every(count) && Array.isArray(hunk.lines) && hunk.lines.every((line) => typeof line === "string" && /^[+ -]/.test(line)));
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
7
144
|
//#region lib/types/numstat.js
|
|
8
145
|
/** Parsing of `git diff-tree --numstat -z` output. */
|
|
9
146
|
/**
|
|
@@ -620,8 +757,9 @@ const OVERSIZED = Symbol("oversized");
|
|
|
620
757
|
* with its diff, and the appended `workspace/changes` event whose summary and
|
|
621
758
|
* comparisons this recorder keeps. Snapshot objects and captured copies live in
|
|
622
759
|
* a temporary directory owned by the recorder; disposal removes it together
|
|
623
|
-
* with the
|
|
624
|
-
*
|
|
760
|
+
* with the live records; the durable archive retains the summaries and comparisons.
|
|
761
|
+
* Tool execution waits for pending work so a snapshot or capture never races a mutation.
|
|
762
|
+
* A working directory outside any repository,
|
|
625
763
|
* or a Host without git, gets no snapshot; its summary lists the files the file
|
|
626
764
|
* tools changed.
|
|
627
765
|
*/
|
|
@@ -742,9 +880,17 @@ var TurnRecorder = class {
|
|
|
742
880
|
*/
|
|
743
881
|
async diff(seq, index, signal) {
|
|
744
882
|
const record = this.records.get(seq);
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
883
|
+
if (record === void 0 || record.summary.files[index] === void 0) return void 0;
|
|
884
|
+
try {
|
|
885
|
+
return await this.compare(record, index, signal);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if (this.lifetime.signal.aborted) return void 0;
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async compare(record, index, signal) {
|
|
892
|
+
const file = record.summary.files[index];
|
|
893
|
+
const sources = record.sources[index];
|
|
748
894
|
const { path, display } = file;
|
|
749
895
|
if (sources.refusal !== void 0) return {
|
|
750
896
|
kind: sources.refusal,
|
|
@@ -752,27 +898,22 @@ var TurnRecorder = class {
|
|
|
752
898
|
display
|
|
753
899
|
};
|
|
754
900
|
const combined = AbortSignal.any([signal, this.lifetime.signal]);
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
};
|
|
772
|
-
} catch (error) {
|
|
773
|
-
if (this.lifetime.signal.aborted) return void 0;
|
|
774
|
-
throw error;
|
|
775
|
-
}
|
|
901
|
+
const [before, after] = await Promise.all([this.readSide(sources.before, combined), this.readSide(sources.after, combined)]);
|
|
902
|
+
if (before === OVERSIZED || after === OVERSIZED) return {
|
|
903
|
+
kind: "oversized",
|
|
904
|
+
path,
|
|
905
|
+
display
|
|
906
|
+
};
|
|
907
|
+
const { hunks, coarse } = compareText(before, after, this.env.diffTimeoutMs);
|
|
908
|
+
return {
|
|
909
|
+
kind: "text",
|
|
910
|
+
path,
|
|
911
|
+
display,
|
|
912
|
+
before: before !== null,
|
|
913
|
+
after: after !== null,
|
|
914
|
+
hunks,
|
|
915
|
+
coarse
|
|
916
|
+
};
|
|
776
917
|
}
|
|
777
918
|
/**
|
|
778
919
|
* Abort queued work, forget every record, and remove the temporary directory.
|
|
@@ -815,6 +956,8 @@ var TurnRecorder = class {
|
|
|
815
956
|
if (git === null) return null;
|
|
816
957
|
const workspace = await locateGitWorkspace(git, cwd, () => this.scratchDir(), signal);
|
|
817
958
|
if (workspace === null) return null;
|
|
959
|
+
const archive = await canonicalPath(this.env.archive.directory);
|
|
960
|
+
if (isInside(workspace.root, archive)) workspace.excludes = [...workspace.excludes, toPosix(relative(workspace.root, archive))];
|
|
818
961
|
this.repository = {
|
|
819
962
|
git,
|
|
820
963
|
workspace
|
|
@@ -894,9 +1037,8 @@ var TurnRecorder = class {
|
|
|
894
1037
|
}
|
|
895
1038
|
const sorted = [...listed.values()].sort((a, b) => compareDisplay(a.file, b.file));
|
|
896
1039
|
if (sorted.length === 0 && state.recordedAfterSeq < 0) return;
|
|
897
|
-
const event = this.session.append("workspace/changes", { turn: state.turn });
|
|
898
1040
|
const kept = sorted.slice(0, this.env.maxFiles);
|
|
899
|
-
|
|
1041
|
+
const record = {
|
|
900
1042
|
summary: {
|
|
901
1043
|
turn: state.turn,
|
|
902
1044
|
cwd: this.cwd,
|
|
@@ -907,8 +1049,17 @@ var TurnRecorder = class {
|
|
|
907
1049
|
...snapshot === void 0 ? {} : { snapshot }
|
|
908
1050
|
},
|
|
909
1051
|
sources: kept.map((entry) => entry.sources)
|
|
910
|
-
}
|
|
911
|
-
|
|
1052
|
+
};
|
|
1053
|
+
const staged = await this.env.archive.stage(this.session.id, record.summary, (index) => this.compare(record, index, signal), signal);
|
|
1054
|
+
try {
|
|
1055
|
+
signal.throwIfAborted();
|
|
1056
|
+
const event = this.session.append("workspace/changes", { turn: state.turn });
|
|
1057
|
+
this.records.set(event.seq, record);
|
|
1058
|
+
state.recordedAfterSeq = event.seq;
|
|
1059
|
+
await staged.commit(event.seq);
|
|
1060
|
+
} finally {
|
|
1061
|
+
await staged.dispose();
|
|
1062
|
+
}
|
|
912
1063
|
}
|
|
913
1064
|
/**
|
|
914
1065
|
* The listing of a captured pair: an oversized side lists the file without
|
|
@@ -966,8 +1117,8 @@ function changedFile({ cwd, home }, root, absolute, counts) {
|
|
|
966
1117
|
* listed file's before-and-after comparison on demand. Each summary is
|
|
967
1118
|
* announced by a `workspace/changes` Session event that carries only the turn
|
|
968
1119
|
* number; summaries and comparisons are served through the `workspaceChanges`
|
|
969
|
-
* service until the Session is
|
|
970
|
-
* git, the summary lists file-tool edits only.
|
|
1120
|
+
* service across Host restarts until the Session is permanently deleted.
|
|
1121
|
+
* Outside a git repository, or without git, the summary lists file-tool edits only.
|
|
971
1122
|
*/
|
|
972
1123
|
/** Stable Loader identity. */
|
|
973
1124
|
const name = "workspace-changes";
|
|
@@ -975,6 +1126,7 @@ const name = "workspace-changes";
|
|
|
975
1126
|
const inject = ["subprocess"];
|
|
976
1127
|
/** Schemastery validation for {@link Config}. */
|
|
977
1128
|
const Config = z.object({
|
|
1129
|
+
directory: z.string().default(chatCodeCliHomePath("workspace-changes")),
|
|
978
1130
|
timeoutMs: z.number().default(3e4),
|
|
979
1131
|
outputMaxBytes: z.number().default(8 * 1024 * 1024),
|
|
980
1132
|
maxFiles: z.number().default(500),
|
|
@@ -1021,6 +1173,7 @@ async function resolveGit(ctx, signal) {
|
|
|
1021
1173
|
* @param config - validated bounds.
|
|
1022
1174
|
*/
|
|
1023
1175
|
function apply(ctx, config) {
|
|
1176
|
+
if (!isAbsolute(config.directory)) throw new Error("workspace-changes requires an absolute directory");
|
|
1024
1177
|
for (const [field, value] of [
|
|
1025
1178
|
["timeoutMs", config.timeoutMs],
|
|
1026
1179
|
["outputMaxBytes", config.outputMaxBytes],
|
|
@@ -1029,21 +1182,27 @@ function apply(ctx, config) {
|
|
|
1029
1182
|
["diffTimeoutMs", config.diffTimeoutMs]
|
|
1030
1183
|
]) if (!Number.isSafeInteger(value) || value < 1) throw new Error(`workspace-changes requires a positive integer ${field}`);
|
|
1031
1184
|
const lifetime = new AbortController();
|
|
1185
|
+
const archive = new ChangesArchive(config.directory);
|
|
1032
1186
|
const recorders = /* @__PURE__ */ new Map();
|
|
1033
1187
|
const byId = /* @__PURE__ */ new Map();
|
|
1188
|
+
const retiring = /* @__PURE__ */ new Set();
|
|
1034
1189
|
const forget = (session) => {
|
|
1035
1190
|
const recorder = recorders.get(session);
|
|
1036
1191
|
recorders.delete(session);
|
|
1037
1192
|
byId.delete(session.id);
|
|
1038
|
-
|
|
1193
|
+
if (recorder === void 0) return Promise.resolve();
|
|
1194
|
+
const disposal = recorder.dispose();
|
|
1195
|
+
retiring.add(disposal);
|
|
1196
|
+
disposal.then(() => retiring.delete(disposal), () => retiring.delete(disposal));
|
|
1197
|
+
return disposal;
|
|
1039
1198
|
};
|
|
1040
1199
|
ctx.effect(() => async () => {
|
|
1041
1200
|
lifetime.abort();
|
|
1042
|
-
await Promise.all([...recorders.keys()].map(forget));
|
|
1201
|
+
await Promise.all([...recorders.keys()].map(forget).concat([...retiring]));
|
|
1043
1202
|
});
|
|
1044
1203
|
ctx.provide("workspaceChanges", {
|
|
1045
|
-
summary: (sessionId, seq) => byId.get(sessionId)?.summary(seq),
|
|
1046
|
-
diff: (sessionId, seq, index, signal) =>
|
|
1204
|
+
summary: async (sessionId, seq) => byId.get(sessionId)?.summary(seq) ?? archive.summary(sessionId, seq),
|
|
1205
|
+
diff: async (sessionId, seq, index, signal) => await archive.diff(sessionId, seq, index, signal) ?? byId.get(sessionId)?.diff(seq, index, signal)
|
|
1047
1206
|
});
|
|
1048
1207
|
let runner;
|
|
1049
1208
|
const gitRunner = () => {
|
|
@@ -1063,6 +1222,7 @@ function apply(ctx, config) {
|
|
|
1063
1222
|
let recorder = recorders.get(session);
|
|
1064
1223
|
if (recorder === void 0) {
|
|
1065
1224
|
recorder = new TurnRecorder(session, cwd, {
|
|
1225
|
+
archive,
|
|
1066
1226
|
git: gitRunner(),
|
|
1067
1227
|
tempRoot: tmpdir(),
|
|
1068
1228
|
maxFiles: config.maxFiles,
|
|
@@ -1087,7 +1247,16 @@ function apply(ctx, config) {
|
|
|
1087
1247
|
else if (event.type === "turn/end") recorders.get(session)?.end(event.data.turn);
|
|
1088
1248
|
});
|
|
1089
1249
|
ctx.on("session/disposed", (session) => {
|
|
1090
|
-
forget(session)
|
|
1250
|
+
forget(session).catch((error) => {
|
|
1251
|
+
ctx.logger.warn(`workspace-changes: ${String(error)}`);
|
|
1252
|
+
});
|
|
1253
|
+
});
|
|
1254
|
+
ctx.on("session/deleting", async ({ sessionIds }) => {
|
|
1255
|
+
for (const sessionId of sessionIds) {
|
|
1256
|
+
const session = [...recorders.keys()].find((candidate) => candidate.id === sessionId);
|
|
1257
|
+
if (session !== void 0) await forget(session);
|
|
1258
|
+
await archive.remove(sessionId);
|
|
1259
|
+
}
|
|
1091
1260
|
});
|
|
1092
1261
|
ctx.on("agent/turn-stopping", async ({ agent, turn }) => {
|
|
1093
1262
|
await recorders.get(agent.session)?.stopping(turn);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
2
|
+
import type { WorkspaceChangesSummary, WorkspaceFileDiff } from './types.ts';
|
|
3
|
+
/** A complete comparison set awaiting its announcing event's sequence. */
|
|
4
|
+
export interface StagedChanges {
|
|
5
|
+
/** Publish the directory atomically under the announcing event's sequence. */
|
|
6
|
+
commit(seq: number): Promise<void>;
|
|
7
|
+
/** Remove any unpublished staging directory. */
|
|
8
|
+
dispose(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** Stores completed turn records until their Session is permanently deleted. */
|
|
11
|
+
export declare class ChangesArchive {
|
|
12
|
+
readonly directory: string;
|
|
13
|
+
constructor(directory: string);
|
|
14
|
+
private sessionDirectory;
|
|
15
|
+
/**
|
|
16
|
+
* Save the summary and each listed comparison before publishing its event.
|
|
17
|
+
* @param sessionId - owning Session.
|
|
18
|
+
* @param summary - complete summary, including the Host working directory.
|
|
19
|
+
* @param diff - resolves one comparison from the turn's immutable snapshots.
|
|
20
|
+
* @param signal - cancels staging before the event is appended.
|
|
21
|
+
* @returns a staged record to commit with the event's sequence and then dispose.
|
|
22
|
+
*/
|
|
23
|
+
stage(sessionId: SessionId, summary: WorkspaceChangesSummary, diff: (index: number) => Promise<WorkspaceFileDiff>, signal: AbortSignal): Promise<StagedChanges>;
|
|
24
|
+
/**
|
|
25
|
+
* Read an archived summary without opening a live Session or its repository.
|
|
26
|
+
* @param sessionId - owning Session.
|
|
27
|
+
* @param seq - announcing event sequence.
|
|
28
|
+
* @returns the validated summary, or undefined when no archive exists.
|
|
29
|
+
*/
|
|
30
|
+
summary(sessionId: SessionId, seq: number): Promise<WorkspaceChangesSummary | undefined>;
|
|
31
|
+
/**
|
|
32
|
+
* Read a stored comparison independently of later workspace edits or git cleanup.
|
|
33
|
+
* @param sessionId - owning Session.
|
|
34
|
+
* @param seq - announcing event sequence.
|
|
35
|
+
* @param index - listed file index.
|
|
36
|
+
* @param signal - cancellation.
|
|
37
|
+
* @returns the validated comparison, or undefined for a missing record or index.
|
|
38
|
+
*/
|
|
39
|
+
diff(sessionId: SessionId, seq: number, index: number, signal: AbortSignal): Promise<WorkspaceFileDiff | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Remove secondary records when the Session is permanently deleted.
|
|
42
|
+
* @param sessionId - deleted Session.
|
|
43
|
+
* @returns after its archive directory has been removed.
|
|
44
|
+
*/
|
|
45
|
+
remove(sessionId: SessionId): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Durable change summaries and compressed file comparisons, independent of temporary git snapshots. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { isAbsolute, join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { gzip, gunzip } from 'node:zlib';
|
|
7
|
+
const compress = promisify(gzip);
|
|
8
|
+
const decompress = promisify(gunzip);
|
|
9
|
+
/** Stores completed turn records until their Session is permanently deleted. */
|
|
10
|
+
export class ChangesArchive {
|
|
11
|
+
directory;
|
|
12
|
+
constructor(directory) {
|
|
13
|
+
this.directory = directory;
|
|
14
|
+
}
|
|
15
|
+
sessionDirectory(sessionId) {
|
|
16
|
+
return join(this.directory, createHash('sha256').update(sessionId).digest('hex'));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Save the summary and each listed comparison before publishing its event.
|
|
20
|
+
* @param sessionId - owning Session.
|
|
21
|
+
* @param summary - complete summary, including the Host working directory.
|
|
22
|
+
* @param diff - resolves one comparison from the turn's immutable snapshots.
|
|
23
|
+
* @param signal - cancels staging before the event is appended.
|
|
24
|
+
* @returns a staged record to commit with the event's sequence and then dispose.
|
|
25
|
+
*/
|
|
26
|
+
async stage(sessionId, summary, diff, signal) {
|
|
27
|
+
const parent = this.sessionDirectory(sessionId);
|
|
28
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
29
|
+
const staging = await mkdtemp(join(parent, '.pending-'));
|
|
30
|
+
try {
|
|
31
|
+
await writeFile(join(staging, 'summary.json'), JSON.stringify({ version: 1, summary }), { mode: 0o600, signal });
|
|
32
|
+
for (let index = 0; index < summary.files.length; index += 1) {
|
|
33
|
+
signal.throwIfAborted();
|
|
34
|
+
const bytes = await compress(JSON.stringify(await diff(index)));
|
|
35
|
+
await writeFile(join(staging, `${index}.json.gz`), bytes, { mode: 0o600, signal });
|
|
36
|
+
}
|
|
37
|
+
signal.throwIfAborted();
|
|
38
|
+
return {
|
|
39
|
+
commit: seq => rename(staging, join(parent, String(seq))),
|
|
40
|
+
dispose: () => rm(staging, { recursive: true, force: true }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
await rm(staging, { recursive: true, force: true });
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read an archived summary without opening a live Session or its repository.
|
|
50
|
+
* @param sessionId - owning Session.
|
|
51
|
+
* @param seq - announcing event sequence.
|
|
52
|
+
* @returns the validated summary, or undefined when no archive exists.
|
|
53
|
+
*/
|
|
54
|
+
async summary(sessionId, seq) {
|
|
55
|
+
const bytes = await readMissing(join(this.sessionDirectory(sessionId), String(seq), 'summary.json'));
|
|
56
|
+
if (bytes === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
const value = JSON.parse(bytes.toString('utf8'));
|
|
59
|
+
if (!record(value) || value.version !== 1 || !isSummary(value.summary))
|
|
60
|
+
throw new Error('Invalid workspace change archive summary');
|
|
61
|
+
return value.summary;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read a stored comparison independently of later workspace edits or git cleanup.
|
|
65
|
+
* @param sessionId - owning Session.
|
|
66
|
+
* @param seq - announcing event sequence.
|
|
67
|
+
* @param index - listed file index.
|
|
68
|
+
* @param signal - cancellation.
|
|
69
|
+
* @returns the validated comparison, or undefined for a missing record or index.
|
|
70
|
+
*/
|
|
71
|
+
async diff(sessionId, seq, index, signal) {
|
|
72
|
+
const bytes = await readMissing(join(this.sessionDirectory(sessionId), String(seq), `${index}.json.gz`), signal);
|
|
73
|
+
if (bytes === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const value = JSON.parse((await decompress(bytes)).toString('utf8'));
|
|
76
|
+
signal.throwIfAborted();
|
|
77
|
+
if (!isDiff(value))
|
|
78
|
+
throw new Error('Invalid workspace change archive comparison');
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Remove secondary records when the Session is permanently deleted.
|
|
83
|
+
* @param sessionId - deleted Session.
|
|
84
|
+
* @returns after its archive directory has been removed.
|
|
85
|
+
*/
|
|
86
|
+
remove(sessionId) {
|
|
87
|
+
return rm(this.sessionDirectory(sessionId), { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function readMissing(file, signal) {
|
|
91
|
+
try {
|
|
92
|
+
return await readFile(file, signal === undefined ? {} : { signal });
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (record(error) && error.code === 'ENOENT')
|
|
96
|
+
return undefined;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function record(value) {
|
|
101
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
102
|
+
}
|
|
103
|
+
function count(value) {
|
|
104
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
105
|
+
}
|
|
106
|
+
function pathFields(value) {
|
|
107
|
+
return typeof value.path === 'string' && value.path.length > 0 && typeof value.display === 'string' && value.display.length > 0;
|
|
108
|
+
}
|
|
109
|
+
function isSummary(value) {
|
|
110
|
+
if (!record(value))
|
|
111
|
+
return false;
|
|
112
|
+
const { turn, cwd, files, total, added, deleted, snapshot } = value;
|
|
113
|
+
return count(turn) && turn > 0 && typeof cwd === 'string' && isAbsolute(cwd) && count(total) && count(added) && count(deleted)
|
|
114
|
+
&& Array.isArray(files) && files.length <= total && files.every(file => record(file) && pathFields(file)
|
|
115
|
+
&& count(file.added) && count(file.deleted) && (file.binary === undefined || file.binary === true)
|
|
116
|
+
&& (file.oversized === undefined || file.oversized === true))
|
|
117
|
+
&& (snapshot === undefined || (record(snapshot) && typeof snapshot.before === 'string' && typeof snapshot.after === 'string'));
|
|
118
|
+
}
|
|
119
|
+
function isDiff(value) {
|
|
120
|
+
if (!record(value) || !pathFields(value))
|
|
121
|
+
return false;
|
|
122
|
+
if (value.kind === 'binary' || value.kind === 'oversized')
|
|
123
|
+
return true;
|
|
124
|
+
return value.kind === 'text' && typeof value.before === 'boolean' && typeof value.after === 'boolean'
|
|
125
|
+
&& typeof value.coarse === 'boolean' && Array.isArray(value.hunks) && value.hunks.every(hunk => record(hunk)
|
|
126
|
+
&& [hunk.oldStart, hunk.oldLines, hunk.newStart, hunk.newLines].every(count)
|
|
127
|
+
&& Array.isArray(hunk.lines) && hunk.lines.every(line => typeof line === 'string' && /^[+ -]/.test(line)));
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -7,6 +7,8 @@ export declare const name = "workspace-changes";
|
|
|
7
7
|
export declare const inject: string[];
|
|
8
8
|
/** Snapshot, capture, and comparison bounds. Invalid values fail plugin load. */
|
|
9
9
|
export interface Config {
|
|
10
|
+
/** Absolute directory retaining summaries and comparisons across Host restarts. */
|
|
11
|
+
directory: string;
|
|
10
12
|
/** Milliseconds one git command may run before the turn's record is abandoned. */
|
|
11
13
|
timeoutMs: number;
|
|
12
14
|
/** Bytes of git output retained per command; a larger diff listing abandons the record. */
|
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* listed file's before-and-after comparison on demand. Each summary is
|
|
6
6
|
* announced by a `workspace/changes` Session event that carries only the turn
|
|
7
7
|
* number; summaries and comparisons are served through the `workspaceChanges`
|
|
8
|
-
* service until the Session is
|
|
9
|
-
* git, the summary lists file-tool edits only.
|
|
8
|
+
* service across Host restarts until the Session is permanently deleted.
|
|
9
|
+
* Outside a git repository, or without git, the summary lists file-tool edits only.
|
|
10
10
|
*/
|
|
11
11
|
import { homedir, tmpdir } from 'node:os';
|
|
12
|
+
import { isAbsolute } from 'node:path';
|
|
12
13
|
import z from '@deepseek-ai/schemastery';
|
|
14
|
+
import { chatCodeCliHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
15
|
+
import { ChangesArchive } from "./archive.js";
|
|
13
16
|
import { GitRunner } from "./git.js";
|
|
14
17
|
import { TurnRecorder } from "./recorder.js";
|
|
15
18
|
/** Stable Loader identity. */
|
|
@@ -18,6 +21,7 @@ export const name = 'workspace-changes';
|
|
|
18
21
|
export const inject = ['subprocess'];
|
|
19
22
|
/** Schemastery validation for {@link Config}. */
|
|
20
23
|
export const Config = z.object({
|
|
24
|
+
directory: z.string().default(chatCodeCliHomePath('workspace-changes')),
|
|
21
25
|
timeoutMs: z.number().default(30_000),
|
|
22
26
|
outputMaxBytes: z.number().default(8 * 1024 * 1024),
|
|
23
27
|
maxFiles: z.number().default(500),
|
|
@@ -61,6 +65,8 @@ async function resolveGit(ctx, signal) {
|
|
|
61
65
|
* @param config - validated bounds.
|
|
62
66
|
*/
|
|
63
67
|
export function apply(ctx, config) {
|
|
68
|
+
if (!isAbsolute(config.directory))
|
|
69
|
+
throw new Error('workspace-changes requires an absolute directory');
|
|
64
70
|
for (const [field, value] of [
|
|
65
71
|
['timeoutMs', config.timeoutMs], ['outputMaxBytes', config.outputMaxBytes], ['maxFiles', config.maxFiles],
|
|
66
72
|
['maxFileBytes', config.maxFileBytes], ['diffTimeoutMs', config.diffTimeoutMs],
|
|
@@ -69,21 +75,28 @@ export function apply(ctx, config) {
|
|
|
69
75
|
throw new Error(`workspace-changes requires a positive integer ${field}`);
|
|
70
76
|
}
|
|
71
77
|
const lifetime = new AbortController();
|
|
78
|
+
const archive = new ChangesArchive(config.directory);
|
|
72
79
|
const recorders = new Map();
|
|
73
80
|
const byId = new Map();
|
|
81
|
+
const retiring = new Set();
|
|
74
82
|
const forget = (session) => {
|
|
75
83
|
const recorder = recorders.get(session);
|
|
76
84
|
recorders.delete(session);
|
|
77
85
|
byId.delete(session.id);
|
|
78
|
-
|
|
86
|
+
if (recorder === undefined)
|
|
87
|
+
return Promise.resolve();
|
|
88
|
+
const disposal = recorder.dispose();
|
|
89
|
+
retiring.add(disposal);
|
|
90
|
+
void disposal.then(() => retiring.delete(disposal), () => retiring.delete(disposal));
|
|
91
|
+
return disposal;
|
|
79
92
|
};
|
|
80
93
|
ctx.effect(() => async () => {
|
|
81
94
|
lifetime.abort();
|
|
82
|
-
await Promise.all([...recorders.keys()].map(forget));
|
|
95
|
+
await Promise.all([...recorders.keys()].map(forget).concat([...retiring]));
|
|
83
96
|
});
|
|
84
97
|
const service = {
|
|
85
|
-
summary: (sessionId, seq) => byId.get(sessionId)?.summary(seq),
|
|
86
|
-
diff: (sessionId, seq, index, signal) =>
|
|
98
|
+
summary: async (sessionId, seq) => byId.get(sessionId)?.summary(seq) ?? archive.summary(sessionId, seq),
|
|
99
|
+
diff: async (sessionId, seq, index, signal) => await archive.diff(sessionId, seq, index, signal) ?? byId.get(sessionId)?.diff(seq, index, signal),
|
|
87
100
|
};
|
|
88
101
|
ctx.provide('workspaceChanges', service);
|
|
89
102
|
let runner;
|
|
@@ -101,7 +114,7 @@ export function apply(ctx, config) {
|
|
|
101
114
|
let recorder = recorders.get(session);
|
|
102
115
|
if (recorder === undefined) {
|
|
103
116
|
recorder = new TurnRecorder(session, cwd, {
|
|
104
|
-
git: gitRunner(), tempRoot: tmpdir(), maxFiles: config.maxFiles,
|
|
117
|
+
archive, git: gitRunner(), tempRoot: tmpdir(), maxFiles: config.maxFiles,
|
|
105
118
|
maxFileBytes: config.maxFileBytes, diffTimeoutMs: config.diffTimeoutMs,
|
|
106
119
|
warn: (message) => { ctx.logger.warn(message); },
|
|
107
120
|
});
|
|
@@ -122,7 +135,17 @@ export function apply(ctx, config) {
|
|
|
122
135
|
else if (event.type === 'turn/end')
|
|
123
136
|
recorders.get(session)?.end(event.data.turn);
|
|
124
137
|
});
|
|
125
|
-
ctx.on('session/disposed', (session) => {
|
|
138
|
+
ctx.on('session/disposed', (session) => {
|
|
139
|
+
void forget(session).catch((error) => { ctx.logger.warn(`workspace-changes: ${String(error)}`); });
|
|
140
|
+
});
|
|
141
|
+
ctx.on('session/deleting', async ({ sessionIds }) => {
|
|
142
|
+
for (const sessionId of sessionIds) {
|
|
143
|
+
const session = [...recorders.keys()].find(candidate => candidate.id === sessionId);
|
|
144
|
+
if (session !== undefined)
|
|
145
|
+
await forget(session);
|
|
146
|
+
await archive.remove(sessionId);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
126
149
|
ctx.on('agent/turn-stopping', async ({ agent, turn }) => {
|
|
127
150
|
await recorders.get(agent.session)?.stopping(turn);
|
|
128
151
|
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
2
|
+
import type { ChangesArchive } from './archive.ts';
|
|
2
3
|
import { type GitRunner } from './git.ts';
|
|
3
4
|
import type { WorkspaceChangesSummary, WorkspaceFileDiff } from './types.ts';
|
|
4
5
|
/** Facts shared by every recorder of one plugin instance. */
|
|
5
6
|
export interface RecorderEnvironment {
|
|
7
|
+
/** Durable summaries and comparisons, retained after recorder disposal. */
|
|
8
|
+
archive: ChangesArchive;
|
|
6
9
|
/** Resolves to the runner, or null when git is unavailable and no snapshot is taken. */
|
|
7
10
|
git: Promise<GitRunner | null>;
|
|
8
11
|
/** Directory that receives each Session's temporary directory. */
|
|
@@ -22,8 +25,9 @@ export interface RecorderEnvironment {
|
|
|
22
25
|
* with its diff, and the appended `workspace/changes` event whose summary and
|
|
23
26
|
* comparisons this recorder keeps. Snapshot objects and captured copies live in
|
|
24
27
|
* a temporary directory owned by the recorder; disposal removes it together
|
|
25
|
-
* with the
|
|
26
|
-
*
|
|
28
|
+
* with the live records; the durable archive retains the summaries and comparisons.
|
|
29
|
+
* Tool execution waits for pending work so a snapshot or capture never races a mutation.
|
|
30
|
+
* A working directory outside any repository,
|
|
27
31
|
* or a Host without git, gets no snapshot; its summary lists the files the file
|
|
28
32
|
* tools changed.
|
|
29
33
|
*/
|
|
@@ -90,6 +94,7 @@ export declare class TurnRecorder {
|
|
|
90
94
|
* @throws when a read fails while the recorder lives.
|
|
91
95
|
*/
|
|
92
96
|
diff(seq: number, index: number, signal: AbortSignal): Promise<WorkspaceFileDiff | undefined>;
|
|
97
|
+
private compare;
|
|
93
98
|
/**
|
|
94
99
|
* Abort queued work, forget every record, and remove the temporary directory.
|
|
95
100
|
* @returns once the temporary directory is gone.
|