personal-bot 0.2.4 → 0.2.5
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/CONTEXT.md +9 -7
- package/README.md +13 -6
- package/README.zh-CN.md +13 -6
- package/docs/README.md +3 -1
- package/docs/agent-collaboration.md +26 -12
- package/docs/architecture-guidelines.md +2 -2
- package/docs/capability-design.md +6 -4
- package/docs/dsh-implementation-plan.md +158 -0
- package/docs/dsh-support-proposal.md +314 -0
- package/docs/dsh.md +103 -0
- package/docs/file-storage.md +14 -9
- package/docs/history.md +29 -18
- package/docs/installation-design.md +3 -1
- package/docs/interaction-design.md +13 -0
- package/docs/release.md +3 -3
- package/docs/technical-design.md +29 -15
- package/docs/validation.md +38 -2
- package/lib/client.js +1071 -0
- package/lib/common/files.d.ts +2 -0
- package/lib/common/files.js +44 -0
- package/lib/common/json.d.ts +6 -0
- package/lib/common/json.js +16 -0
- package/lib/common/markdown.d.ts +5 -0
- package/lib/common/markdown.js +26 -0
- package/lib/common/paths.d.ts +1 -0
- package/lib/common/paths.js +2 -0
- package/lib/common/validation.d.ts +3 -0
- package/lib/common/validation.js +14 -0
- package/lib/common/version.d.ts +3 -0
- package/lib/common/version.js +13 -0
- package/lib/dsh.d.ts +4 -0
- package/lib/dsh.js +22 -0
- package/lib/extension/dsh/agent-events.d.ts +24 -0
- package/lib/extension/dsh/agent-events.js +242 -0
- package/lib/extension/dsh/agent-runtime.d.ts +49 -0
- package/lib/extension/dsh/agent-runtime.js +348 -0
- package/lib/extension/dsh/command.d.ts +4 -0
- package/lib/extension/dsh/command.js +83 -0
- package/lib/extension/dsh/files.d.ts +5 -0
- package/lib/extension/dsh/files.js +67 -0
- package/lib/extension/dsh/history.d.ts +15 -0
- package/lib/extension/dsh/history.js +88 -0
- package/lib/extension/dsh/management.d.ts +21 -0
- package/lib/extension/dsh/management.js +217 -0
- package/lib/extension/dsh/workspace.d.ts +48 -0
- package/lib/extension/dsh/workspace.js +290 -0
- package/lib/integrations/agent-resources.d.ts +1 -0
- package/lib/integrations/agent-resources.js +18 -0
- package/lib/integrations/dsh/dsh-agents.d.ts +30 -0
- package/lib/integrations/dsh/dsh-agents.js +104 -0
- package/lib/integrations/dsh/history-summary.d.ts +11 -0
- package/lib/integrations/dsh/history-summary.js +40 -0
- package/lib/integrations/dsh/history.d.ts +16 -0
- package/lib/integrations/dsh/history.js +156 -0
- package/lib/integrations/dsh/input.d.ts +21 -0
- package/lib/integrations/dsh/input.js +62 -0
- package/lib/integrations/dsh/messages.d.ts +36 -0
- package/lib/integrations/dsh/messages.js +11 -0
- package/lib/integrations/dsh/notifications.d.ts +8 -0
- package/lib/integrations/dsh/notifications.js +55 -0
- package/lib/integrations/dsh/resources.d.ts +10 -0
- package/lib/integrations/dsh/resources.js +41 -0
- package/lib/integrations/dsh/schema.d.ts +7 -0
- package/lib/integrations/dsh/schema.js +28 -0
- package/lib/integrations/dsh/session.d.ts +14 -0
- package/lib/integrations/dsh/session.js +59 -0
- package/lib/integrations/history-summary.d.ts +2 -0
- package/lib/integrations/history-summary.js +21 -0
- package/lib/integrations/process.d.ts +14 -0
- package/lib/integrations/process.js +48 -0
- package/lib/integrations/python-activation.d.ts +2 -0
- package/lib/integrations/python-activation.js +29 -0
- package/lib/integrations/team-context.d.ts +18 -0
- package/lib/integrations/team-context.js +57 -0
- package/lib/model/agent.d.ts +92 -0
- package/lib/model/agent.js +1 -0
- package/lib/model/history.d.ts +43 -0
- package/lib/model/history.js +1 -0
- package/lib/model/team.d.ts +85 -0
- package/lib/model/team.js +1 -0
- package/lib/services/agents/agent-notices.d.ts +13 -0
- package/lib/services/agents/agent-notices.js +25 -0
- package/lib/services/agents/agent-run.d.ts +39 -0
- package/lib/services/agents/agent-run.js +138 -0
- package/lib/services/agents/agent-service.d.ts +166 -0
- package/lib/services/agents/agent-service.js +325 -0
- package/lib/services/agents/team-service.d.ts +168 -0
- package/lib/services/agents/team-service.js +343 -0
- package/lib/services/history/history-service.d.ts +103 -0
- package/lib/services/history/history-service.js +269 -0
- package/lib/services/workspace/python-environment-service.d.ts +15 -0
- package/lib/services/workspace/python-environment-service.js +50 -0
- package/lib/services/workspace/workspace-files.d.ts +2 -0
- package/lib/services/workspace/workspace-files.js +17 -0
- package/lib/services/workspace/workspace-preparation.d.ts +2 -0
- package/lib/services/workspace/workspace-preparation.js +18 -0
- package/lib/storage/agent-files.d.ts +30 -0
- package/lib/storage/agent-files.js +166 -0
- package/lib/storage/atomic-json.d.ts +11 -0
- package/lib/storage/atomic-json.js +101 -0
- package/lib/storage/file-access.d.ts +2 -0
- package/lib/storage/file-access.js +26 -0
- package/lib/storage/history-files.d.ts +10 -0
- package/lib/storage/history-files.js +63 -0
- package/lib/storage/team-files.d.ts +16 -0
- package/lib/storage/team-files.js +111 -0
- package/lib/tools/agents/actions.d.ts +52 -0
- package/lib/tools/agents/actions.js +49 -0
- package/lib/tools/agents/contract.d.ts +163 -0
- package/lib/tools/agents/contract.js +56 -0
- package/lib/tools/agents/dsh-contract.d.ts +52 -0
- package/lib/tools/agents/dsh-contract.js +15 -0
- package/lib/tools/agents/dsh-tool.d.ts +5 -0
- package/lib/tools/agents/dsh-tool.js +38 -0
- package/lib/tools/history/actions.d.ts +70 -0
- package/lib/tools/history/actions.js +7 -0
- package/lib/tools/history/contract.d.ts +81 -0
- package/lib/tools/history/contract.js +32 -0
- package/lib/tools/history/dsh-tool.d.ts +4 -0
- package/lib/tools/history/dsh-tool.js +22 -0
- package/lib/tools/result.d.ts +18 -0
- package/lib/tools/result.js +10 -0
- package/lib/tools/teams/actions.d.ts +24 -0
- package/lib/tools/teams/actions.js +38 -0
- package/lib/tools/teams/contract.d.ts +232 -0
- package/lib/tools/teams/contract.js +67 -0
- package/lib/tools/teams/dsh-tool.d.ts +8 -0
- package/lib/tools/teams/dsh-tool.js +49 -0
- package/lib/ui/dsh/attachments.d.ts +5 -0
- package/lib/ui/dsh/attachments.js +39 -0
- package/lib/ui/dsh/elements.d.ts +17 -0
- package/lib/ui/dsh/elements.js +79 -0
- package/lib/ui/dsh/index.d.ts +4 -0
- package/lib/ui/dsh/index.js +13 -0
- package/lib/ui/dsh/management.d.ts +4 -0
- package/lib/ui/dsh/management.js +711 -0
- package/lib/ui/dsh/remote.d.ts +15 -0
- package/lib/ui/dsh/remote.js +11 -0
- package/lib/ui/dsh/styles.d.ts +1 -0
- package/lib/ui/dsh/styles.js +132 -0
- package/lib/ui/presentation.d.ts +3 -0
- package/lib/ui/presentation.js +12 -0
- package/npm-shrinkwrap.json +11140 -830
- package/package.json +94 -12
- package/python/README.md +6 -6
- package/resources/agents/coordinator.md +1 -1
- package/resources/dsh/cordis.patch.yml +3 -0
- package/resources/prompts/workspace.md +1 -1
- package/resources/references/compute.md +1 -1
- package/resources/references/task-conductor.md +6 -4
- package/resources/skills/provider-connections/SKILL.md +10 -0
- package/scripts/README.md +9 -1
- package/src/dsh.ts +22 -0
- package/src/extension/agent-events.ts +51 -46
- package/src/extension/agents-command.ts +48 -34
- package/src/extension/command.ts +42 -23
- package/src/extension/dsh/agent-events.ts +148 -0
- package/src/extension/dsh/agent-runtime.ts +251 -0
- package/src/extension/dsh/command.ts +78 -0
- package/src/extension/dsh/files.ts +61 -0
- package/src/extension/dsh/history.ts +69 -0
- package/src/extension/dsh/management.ts +108 -0
- package/src/extension/dsh/workspace.ts +206 -0
- package/src/extension/events.ts +6 -4
- package/src/extension/history.ts +15 -12
- package/src/extension/subagent-entry.ts +34 -63
- package/src/extension/teams-command.ts +53 -46
- package/src/integrations/{pi/agent-resources.ts → agent-resources.ts} +2 -2
- package/src/integrations/dsh/dsh-agents.ts +97 -0
- package/src/integrations/dsh/history-summary.ts +43 -0
- package/src/integrations/dsh/history.ts +76 -0
- package/src/integrations/dsh/input.ts +66 -0
- package/src/integrations/dsh/messages.ts +22 -0
- package/src/integrations/dsh/notifications.ts +46 -0
- package/src/integrations/dsh/resources.ts +44 -0
- package/src/integrations/dsh/schema.ts +26 -0
- package/src/integrations/dsh/session.ts +67 -0
- package/src/integrations/history-summary.ts +20 -0
- package/src/integrations/pi/history.ts +4 -14
- package/src/integrations/pi/pi-agents.ts +17 -6
- package/src/integrations/pi/team-context.ts +6 -58
- package/src/integrations/team-context.ts +60 -0
- package/src/model/agent.ts +2 -0
- package/src/model/history.ts +3 -1
- package/src/pbot.ts +1 -1
- package/src/services/agents/agent-notices.ts +28 -0
- package/src/services/agents/agent-run.ts +114 -0
- package/src/services/agents/agent-service.ts +127 -64
- package/src/services/history/history-service.ts +120 -59
- package/src/storage/agent-files.ts +17 -4
- package/src/storage/history-files.ts +30 -5
- package/src/tools/agents/actions.ts +47 -0
- package/src/tools/agents/contract.ts +8 -5
- package/src/tools/agents/dsh-contract.ts +16 -0
- package/src/tools/agents/dsh-tool.ts +41 -0
- package/src/tools/agents/tool.ts +7 -43
- package/src/tools/history/actions.ts +9 -0
- package/src/tools/history/contract.ts +34 -0
- package/src/tools/history/dsh-tool.ts +22 -0
- package/src/tools/history/tool.ts +7 -30
- package/src/tools/teams/actions.ts +38 -0
- package/src/tools/teams/contract.ts +7 -3
- package/src/tools/teams/dsh-tool.ts +55 -0
- package/src/tools/teams/tool.ts +6 -31
- package/src/ui/agent-display.ts +7 -4
- package/src/ui/dsh/attachments.ts +42 -0
- package/src/ui/dsh/elements.ts +50 -0
- package/src/ui/dsh/index.ts +18 -0
- package/src/ui/dsh/management.ts +412 -0
- package/src/ui/dsh/remote.ts +20 -0
- package/src/ui/dsh/styles.ts +132 -0
- package/src/ui/pi/menu.ts +37 -0
- package/src/ui/pi/reader.ts +46 -0
- package/src/ui/presentation.ts +12 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { requireThat } from "../../common/validation.js";
|
|
3
|
+
/** Shared prefixes reuse nodes; only consecutive exchanges on one native branch can merge. */
|
|
4
|
+
export function indexHistory(session, previous, host = "pi") {
|
|
5
|
+
const id = previous?.id ?? (host === "dsh" ? "dsh-" : "") + randomUUID(), nodes = new Map();
|
|
6
|
+
const old = new Map(previous?.nodes.map(node => [node.id, node])), changed = new Set();
|
|
7
|
+
const add = (value, level, children = []) => {
|
|
8
|
+
const key = `${id}:${level}:${value.entryIds.at(-1)}`, found = nodes.get(key);
|
|
9
|
+
if (found)
|
|
10
|
+
return found;
|
|
11
|
+
const before = old.get(key), same = before && JSON.stringify(before.children) === JSON.stringify(children) &&
|
|
12
|
+
(level ? children.every(child => !changed.has(child)) : before.text === value.text && before.completed === value.completed);
|
|
13
|
+
const node = { ...value, id: key, kind: level ? "summary" : "exchange", level, children, parents: [],
|
|
14
|
+
text: level ? same ? before.text : "" : value.text, ready: level ? Boolean(same && before.ready) : true };
|
|
15
|
+
if (!same)
|
|
16
|
+
changed.add(key);
|
|
17
|
+
nodes.set(key, node);
|
|
18
|
+
return node;
|
|
19
|
+
};
|
|
20
|
+
const roots = new Set();
|
|
21
|
+
for (const branch of session.branches) {
|
|
22
|
+
let layer = branch.map(exchange => add(exchange, 0)), level = 1;
|
|
23
|
+
while (layer.length >= 4) {
|
|
24
|
+
const count = Math.floor(layer.length / 4) * 4, next = [];
|
|
25
|
+
layer.slice(count).forEach(node => roots.add(node.id));
|
|
26
|
+
for (let i = 0; i < count; i += 4) {
|
|
27
|
+
const children = layer.slice(i, i + 4), first = children[0], last = children.at(-1);
|
|
28
|
+
next.push(add({ entryIds: [first.entryIds[0], last.entryIds.at(-1)], text: "", from: first.from, to: last.to,
|
|
29
|
+
completed: children.every(node => node.completed) }, level, children.map(node => node.id)));
|
|
30
|
+
}
|
|
31
|
+
layer = next;
|
|
32
|
+
level++;
|
|
33
|
+
}
|
|
34
|
+
layer.forEach(node => roots.add(node.id));
|
|
35
|
+
}
|
|
36
|
+
const dates = [...nodes.values()].flatMap(node => [node.from, node.to]).sort();
|
|
37
|
+
nodes.set(id, { id, kind: "session", level: -1, entryIds: [], text: session.name, ready: true, completed: [...nodes.values()].every(node => node.completed),
|
|
38
|
+
from: dates[0] ?? "", to: dates.at(-1) ?? "", parents: [],
|
|
39
|
+
children: [...roots].sort((a, b) => nodes.get(a).from.localeCompare(nodes.get(b).from)) });
|
|
40
|
+
for (const node of nodes.values())
|
|
41
|
+
for (const child of node.children)
|
|
42
|
+
nodes.get(child).parents.push(node.id);
|
|
43
|
+
return { id, sessionId: session.sessionId, name: session.name, nodes: [...nodes.values()] };
|
|
44
|
+
}
|
|
45
|
+
const page = (values, offset, limit) => ({ values: values.slice(offset, offset + limit),
|
|
46
|
+
nextOffset: offset + limit < values.length ? offset + limit : null });
|
|
47
|
+
const brief = ({ host, record }, node) => ({ host, id: node.id, kind: node.kind, sessionId: record.sessionId,
|
|
48
|
+
name: record.name, preview: node.text.replace(/\s+/g, " ").slice(0, 100), from: node.from, to: node.to, ready: node.ready, completed: node.completed });
|
|
49
|
+
const rawSource = (record) => record && JSON.stringify({ name: record.name,
|
|
50
|
+
exchanges: record.nodes.filter(node => node.kind === "exchange").map(node => [node.id, node.entryIds, node.text, node.from, node.to, node.completed]) });
|
|
51
|
+
const summarySource = (node) => [node.id, node.entryIds, node.children, node.text, node.from, node.to, node.completed, node.ready];
|
|
52
|
+
/** Validate the exact inputs again under the source lock; another writer's ready summary wins. */
|
|
53
|
+
export function completeHistorySummary(record, id, children, text) {
|
|
54
|
+
const node = record.nodes.find(value => value.id === id);
|
|
55
|
+
if (!node || node.kind !== "summary" || node.ready || JSON.stringify(node.children) !== JSON.stringify(children.map(child => child.id)))
|
|
56
|
+
return false;
|
|
57
|
+
const current = node.children.map(id => record.nodes.find(value => value.id === id));
|
|
58
|
+
if (current.some(value => !value) || JSON.stringify(current.map(value => summarySource(value))) !== JSON.stringify(children.map(summarySource)))
|
|
59
|
+
return false;
|
|
60
|
+
requireThat(text.trim(), "历史摘要为空。");
|
|
61
|
+
node.text = text.trim();
|
|
62
|
+
node.ready = true;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
export class HistoryService {
|
|
66
|
+
root;
|
|
67
|
+
files;
|
|
68
|
+
source;
|
|
69
|
+
previousFiles;
|
|
70
|
+
otherFiles;
|
|
71
|
+
#records = [];
|
|
72
|
+
#closed = false;
|
|
73
|
+
#loaded = false;
|
|
74
|
+
#refresh;
|
|
75
|
+
#work;
|
|
76
|
+
#sourceWarnings = [];
|
|
77
|
+
#summaryError;
|
|
78
|
+
constructor(root, files, source, previousFiles, otherFiles) {
|
|
79
|
+
this.root = root;
|
|
80
|
+
this.files = files;
|
|
81
|
+
this.source = source;
|
|
82
|
+
this.previousFiles = previousFiles;
|
|
83
|
+
this.otherFiles = otherFiles;
|
|
84
|
+
requireThat(source.host === files.host && (!otherFiles || otherFiles.host !== files.host), "历史来源与归档目录不匹配。");
|
|
85
|
+
requireThat(files.host === "pi" || !previousFiles, "dsh 不导入 Pi 旧归档。");
|
|
86
|
+
}
|
|
87
|
+
check() { requireThat(!this.#closed, "历史服务已关闭。"); }
|
|
88
|
+
get warnings() { return [...this.#sourceWarnings, ...(this.#summaryError ? [this.files.host + " 摘要:" + this.#summaryError] : [])]; }
|
|
89
|
+
get status() {
|
|
90
|
+
return { loaded: this.#loaded, organizing: Boolean(this.#work), pending: this.#loaded ? this.#records.filter(value => value.host === this.files.host)
|
|
91
|
+
.reduce((total, value) => total + value.record.nodes.filter(node => node.kind === "summary" && !node.ready).length, 0) : null, warnings: this.warnings };
|
|
92
|
+
}
|
|
93
|
+
refresh(location, signal) {
|
|
94
|
+
this.check();
|
|
95
|
+
// Each caller keeps its own cancellation. A closed page cannot cancel a server-owned refresh.
|
|
96
|
+
const work = (this.#refresh ?? Promise.resolve()).catch(() => { }).then(() => this.load(location, signal));
|
|
97
|
+
this.#refresh = work;
|
|
98
|
+
return work.finally(() => { if (this.#refresh === work)
|
|
99
|
+
this.#refresh = undefined; });
|
|
100
|
+
}
|
|
101
|
+
async load(location, signal) {
|
|
102
|
+
this.check();
|
|
103
|
+
signal?.throwIfAborted();
|
|
104
|
+
const warnings = [], records = [];
|
|
105
|
+
const warning = (host, error) => { this.check(); signal?.throwIfAborted(); warnings.push(host + ": " + String(error)); };
|
|
106
|
+
let known = [];
|
|
107
|
+
try {
|
|
108
|
+
const before = this.files.list();
|
|
109
|
+
known = before;
|
|
110
|
+
let imports = [], importedPrevious = !this.previousFiles;
|
|
111
|
+
if (this.previousFiles) {
|
|
112
|
+
try {
|
|
113
|
+
imports = this.previousFiles.list(this.root);
|
|
114
|
+
importedPrevious = true;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
warning("pi", error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let sessions = [];
|
|
121
|
+
try {
|
|
122
|
+
const scanned = await this.source.scan(location, signal);
|
|
123
|
+
sessions = scanned.sessions;
|
|
124
|
+
warnings.push(...scanned.warnings.map(value => this.files.host + ": " + value));
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
warning(this.files.host, error);
|
|
128
|
+
}
|
|
129
|
+
this.check();
|
|
130
|
+
signal?.throwIfAborted();
|
|
131
|
+
if (sessions.length || imports.length || (this.files.host === "pi" && before.some(value => "file" in value))) {
|
|
132
|
+
known = this.files.control(() => {
|
|
133
|
+
this.check();
|
|
134
|
+
signal?.throwIfAborted();
|
|
135
|
+
const latest = this.files.list(), current = new Map(latest.map(record => [record.sessionId, record])), imported = new Set();
|
|
136
|
+
if (this.files.host === "pi") {
|
|
137
|
+
for (const value of latest)
|
|
138
|
+
if ("file" in value) {
|
|
139
|
+
const { id, sessionId, name, nodes } = value, clean = { id, sessionId, name, nodes };
|
|
140
|
+
this.files.save(clean);
|
|
141
|
+
current.set(sessionId, clean);
|
|
142
|
+
}
|
|
143
|
+
for (const { id, sessionId, name, nodes } of imports)
|
|
144
|
+
if (!current.has(sessionId)) {
|
|
145
|
+
const value = { id, sessionId, name, nodes };
|
|
146
|
+
this.files.save(value);
|
|
147
|
+
current.set(sessionId, value);
|
|
148
|
+
imported.add(sessionId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const session of sessions) {
|
|
152
|
+
const previous = current.get(session.sessionId), captured = before.find(record => record.sessionId === session.sessionId);
|
|
153
|
+
// A concurrent source update must not be overwritten by this older scan. Summary-only updates remain reusable.
|
|
154
|
+
if (captured && rawSource(captured) !== rawSource(previous))
|
|
155
|
+
continue;
|
|
156
|
+
if (!captured && previous && !imported.has(session.sessionId))
|
|
157
|
+
continue;
|
|
158
|
+
const value = indexHistory(session, previous, this.files.host);
|
|
159
|
+
if (JSON.stringify(previous) !== JSON.stringify(value))
|
|
160
|
+
this.files.save(value);
|
|
161
|
+
current.set(session.sessionId, value);
|
|
162
|
+
}
|
|
163
|
+
return [...current.values()];
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (importedPrevious)
|
|
167
|
+
this.previousFiles = undefined;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
warning(this.files.host, error);
|
|
171
|
+
}
|
|
172
|
+
records.push(...known.map(record => ({ host: this.files.host, record })));
|
|
173
|
+
if (this.otherFiles) {
|
|
174
|
+
try {
|
|
175
|
+
records.push(...this.otherFiles.list().map(record => ({ host: this.otherFiles.host, record })));
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
warning(this.otherFiles.host, error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
this.check();
|
|
182
|
+
signal?.throwIfAborted();
|
|
183
|
+
this.#records = records;
|
|
184
|
+
this.#sourceWarnings = warnings;
|
|
185
|
+
this.#loaded = true;
|
|
186
|
+
}
|
|
187
|
+
search({ host, query = "", sessionId, from, to, offset = 0, limit = 20 } = {}) {
|
|
188
|
+
this.check();
|
|
189
|
+
const needle = query.trim().toLowerCase(), after = from === undefined ? -Infinity : Date.parse(from), before = to === undefined ? Infinity : Date.parse(to);
|
|
190
|
+
requireThat(!Number.isNaN(after) && !Number.isNaN(before) && after <= before, "时间范围无效,请使用 ISO 日期或时间。");
|
|
191
|
+
const matches = this.#records.filter(value => (!host || value.host === host) && (!sessionId || value.record.sessionId === sessionId)).flatMap(value => value.record.nodes
|
|
192
|
+
.filter(node => (needle ? node.kind !== "session" && node.ready && node.text.toLowerCase().includes(needle) : node.kind === "session") &&
|
|
193
|
+
Date.parse(node.to) >= after && Date.parse(node.from) <= before)
|
|
194
|
+
.map(node => {
|
|
195
|
+
const start = Math.max(0, node.text.toLowerCase().indexOf(needle) - 80);
|
|
196
|
+
return { ...brief(value, node), parents: node.parents, snippet: node.text.slice(start, start + 400) };
|
|
197
|
+
})).sort((a, b) => b.to.localeCompare(a.to) || a.id.localeCompare(b.id));
|
|
198
|
+
const result = page(matches, offset, limit);
|
|
199
|
+
return { matches: result.values, total: matches.length, nextOffset: result.nextOffset, warnings: this.warnings };
|
|
200
|
+
}
|
|
201
|
+
find(id, host) {
|
|
202
|
+
const values = this.#records.filter(value => (!host || value.host === host) && value.record.nodes.some(node => node.id === id));
|
|
203
|
+
requireThat(values.length, "历史节点不存在,请重新搜索。");
|
|
204
|
+
requireThat(values.length === 1, "历史节点来源不明确,请指定 host。");
|
|
205
|
+
const value = values[0], node = value.record.nodes.find(node => node.id === id);
|
|
206
|
+
return { value, node };
|
|
207
|
+
}
|
|
208
|
+
read(id, offset = 0, limit = 12000, childrenOffset = 0, host) {
|
|
209
|
+
this.check();
|
|
210
|
+
const { value, node } = this.find(id, host), lookup = new Map(value.record.nodes.map(node => [node.id, node]));
|
|
211
|
+
requireThat(offset <= node.text.length, "读取位置超出正文。");
|
|
212
|
+
if (offset && /[\uDC00-\uDFFF]/.test(node.text[offset] ?? ""))
|
|
213
|
+
offset--;
|
|
214
|
+
let end = Math.min(node.text.length, offset + limit);
|
|
215
|
+
if (end < node.text.length && /[\uD800-\uDBFF]/.test(node.text[end - 1] ?? ""))
|
|
216
|
+
end += end - offset === 1 ? 1 : -1;
|
|
217
|
+
const children = page(node.children, childrenOffset, 20);
|
|
218
|
+
return { ...brief(value, node), text: node.text.slice(offset, end), totalLength: node.text.length,
|
|
219
|
+
nextOffset: end < node.text.length ? end : null,
|
|
220
|
+
parents: node.parents.map(id => brief(value, lookup.get(id))), children: children.values.map(id => brief(value, lookup.get(id))),
|
|
221
|
+
nextChildrenOffset: children.nextOffset, source: { host: value.host, sessionId: value.record.sessionId, entryIds: node.entryIds }, warnings: this.warnings };
|
|
222
|
+
}
|
|
223
|
+
/** Only this source is organized. Model calls run outside the write lock. */
|
|
224
|
+
organize(summarize, signal) {
|
|
225
|
+
this.check();
|
|
226
|
+
const run = async () => {
|
|
227
|
+
for (;;) {
|
|
228
|
+
this.check();
|
|
229
|
+
signal.throwIfAborted();
|
|
230
|
+
let pending;
|
|
231
|
+
for (const record of this.files.list()) {
|
|
232
|
+
const lookup = new Map(record.nodes.map(node => [node.id, node]));
|
|
233
|
+
const node = [...record.nodes].sort((a, b) => a.level - b.level).find(node => node.kind === "summary" && !node.ready && node.children.every(id => lookup.get(id).ready));
|
|
234
|
+
if (node) {
|
|
235
|
+
pending = { record, node, children: node.children.map(id => lookup.get(id)) };
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (!pending)
|
|
240
|
+
return;
|
|
241
|
+
const { record, node, children } = pending;
|
|
242
|
+
const text = await summarize(children.map(child => `[${child.from} — ${child.to}${child.completed ? "" : ";未完成"}]\n${child.text}`).join("\n\n"), signal);
|
|
243
|
+
this.check();
|
|
244
|
+
signal.throwIfAborted();
|
|
245
|
+
const committed = this.files.control(() => {
|
|
246
|
+
this.check();
|
|
247
|
+
signal.throwIfAborted();
|
|
248
|
+
const current = this.files.list().find(value => value.id === record.id);
|
|
249
|
+
if (!current || !completeHistorySummary(current, node.id, children, text))
|
|
250
|
+
return;
|
|
251
|
+
this.files.save(current);
|
|
252
|
+
return current;
|
|
253
|
+
});
|
|
254
|
+
if (!committed)
|
|
255
|
+
return;
|
|
256
|
+
this.#records = this.#records.map(value => value.host === this.files.host && value.record.id === committed.id ? { host: value.host, record: committed } : value);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const work = (this.#work ?? Promise.resolve()).catch(() => { }).then(run).then(() => { this.#summaryError = undefined; }).catch(error => {
|
|
260
|
+
if (!signal.aborted && !this.#closed)
|
|
261
|
+
this.#summaryError = String(error);
|
|
262
|
+
throw error;
|
|
263
|
+
});
|
|
264
|
+
this.#work = work;
|
|
265
|
+
return work.finally(() => { if (this.#work === work)
|
|
266
|
+
this.#work = undefined; });
|
|
267
|
+
}
|
|
268
|
+
async close() { this.#closed = true; await Promise.allSettled([this.#work, this.#refresh]); this.#records = []; this.#loaded = false; }
|
|
269
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class PythonEnvironmentService {
|
|
2
|
+
#private;
|
|
3
|
+
private readonly root;
|
|
4
|
+
constructor(root: string);
|
|
5
|
+
get status(): {
|
|
6
|
+
state: "unprepared" | "preparing" | "ready" | "error";
|
|
7
|
+
directory?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
};
|
|
10
|
+
setup(signal?: AbortSignal): Promise<{
|
|
11
|
+
directory: string;
|
|
12
|
+
python: string;
|
|
13
|
+
}>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { object } from "../../common/json.js";
|
|
3
|
+
import { runCommand } from "../../integrations/process.js";
|
|
4
|
+
import { requireThat } from "../../common/validation.js";
|
|
5
|
+
import { resourceRoot } from "../../common/paths.js";
|
|
6
|
+
export class PythonEnvironmentService {
|
|
7
|
+
root;
|
|
8
|
+
#shutdown = new AbortController();
|
|
9
|
+
#pending;
|
|
10
|
+
#status = { state: "unprepared" };
|
|
11
|
+
constructor(root) {
|
|
12
|
+
this.root = root;
|
|
13
|
+
}
|
|
14
|
+
get status() { return { ...this.#status }; }
|
|
15
|
+
setup(signal) {
|
|
16
|
+
const cancel = signal ? AbortSignal.any([this.#shutdown.signal, signal]) : this.#shutdown.signal;
|
|
17
|
+
if (cancel.aborted)
|
|
18
|
+
return Promise.reject(cancel.reason);
|
|
19
|
+
if (this.#pending)
|
|
20
|
+
return this.#pending;
|
|
21
|
+
this.#status = { state: "preparing" };
|
|
22
|
+
this.#pending = (async () => {
|
|
23
|
+
try {
|
|
24
|
+
const environment = {};
|
|
25
|
+
for (const [key, value] of Object.entries(process.env))
|
|
26
|
+
if (value !== undefined)
|
|
27
|
+
environment[key] = value;
|
|
28
|
+
const result = await runCommand("/bin/sh", ["-c", 'exec "$(command -v python3 || command -v python)" "$@"',
|
|
29
|
+
"pbot-python", "-I", "-B", join(resourceRoot, "../scripts/setup-python.py")], undefined, this.root, environment, cancel, { timeoutMs: 15 * 60_000 });
|
|
30
|
+
cancel.throwIfAborted();
|
|
31
|
+
requireThat(result.success && object(result.data) && typeof result.data.directory === "string" && typeof result.data.python === "string", "共享 Python 环境未就绪;修复后执行 /pbot python setup。\n" + JSON.stringify(result.data).slice(-6000));
|
|
32
|
+
const ready = { directory: result.data.directory, python: result.data.python };
|
|
33
|
+
this.#status = { state: "ready", directory: ready.directory };
|
|
34
|
+
return ready;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
this.#status = { state: "error", error: error instanceof Error ? error.message : String(error) };
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
this.#pending = undefined;
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
return this.#pending;
|
|
45
|
+
}
|
|
46
|
+
async close() {
|
|
47
|
+
this.#shutdown.abort();
|
|
48
|
+
await this.#pending?.catch(() => undefined);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { relative, isAbsolute } from "node:path";
|
|
2
|
+
import { mutationTarget } from "../../common/files.js";
|
|
3
|
+
import { requireThat } from "../../common/validation.js";
|
|
4
|
+
export function mutationScope(root, target) {
|
|
5
|
+
const rel = relative(root, target);
|
|
6
|
+
if (rel === ".." || rel.startsWith("../") || isAbsolute(rel))
|
|
7
|
+
return "outside";
|
|
8
|
+
return "inside";
|
|
9
|
+
}
|
|
10
|
+
export async function authorizeMutation(root, path, change, confirm, signal) {
|
|
11
|
+
signal.throwIfAborted();
|
|
12
|
+
const target = await mutationTarget(path), scope = mutationScope(root, target);
|
|
13
|
+
if (scope === "outside")
|
|
14
|
+
requireThat(await confirm({ path: target, change }), "用户未确认文件修改。");
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
requireThat(await mutationTarget(path) === target, "确认期间文件路径已变化。");
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { requireThat } from "../../common/validation.js";
|
|
4
|
+
import { resourceRoot } from "../../common/paths.js";
|
|
5
|
+
export const directories = ["areas", "projects", "knowledge", "providers", "scratch", "agents"];
|
|
6
|
+
export async function prepareWorkspace(root) {
|
|
7
|
+
for (const part of directories) {
|
|
8
|
+
const path = join(root, part);
|
|
9
|
+
await mkdir(path, { mode: 0o700 }).catch(error => { if (error.code !== "EEXIST")
|
|
10
|
+
throw error; });
|
|
11
|
+
const entry = await lstat(path);
|
|
12
|
+
requireThat(entry.isDirectory() && !entry.isSymbolicLink(), "工作区目录不能使用符号链接:" + path);
|
|
13
|
+
}
|
|
14
|
+
const agents = await readFile(join(resourceRoot, "templates/workspace-agents.md"), "utf8");
|
|
15
|
+
await writeFile(join(root, "AGENTS.md"), agents, { flag: "wx" })
|
|
16
|
+
.catch(error => { if (error.code !== "EEXIST")
|
|
17
|
+
throw error; });
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentDescriptor, AgentLaunch, AgentMessage, AgentStatus, AgentCycle, AgentSessionRef, AgentSessionReference } from "../model/agent.js";
|
|
2
|
+
export declare function descriptor(v: unknown): AgentDescriptor;
|
|
3
|
+
export declare function sessionRef(v: unknown): AgentSessionRef;
|
|
4
|
+
export declare function launch(v: unknown): AgentLaunch;
|
|
5
|
+
export declare function message(v: unknown): AgentMessage;
|
|
6
|
+
export declare function cycle(v: unknown): AgentCycle;
|
|
7
|
+
export declare function status(v: unknown): AgentStatus;
|
|
8
|
+
export declare class AgentFiles {
|
|
9
|
+
#private;
|
|
10
|
+
readonly root: string;
|
|
11
|
+
readonly host: AgentSessionReference["host"];
|
|
12
|
+
constructor(root: string, host?: AgentSessionReference["host"]);
|
|
13
|
+
check(): void;
|
|
14
|
+
fail(error: unknown): void;
|
|
15
|
+
directory(id: string): string;
|
|
16
|
+
read<T>(id: string, path: string, parse: (v: unknown) => T): T;
|
|
17
|
+
optional<T>(id: string, path: string, parse: (v: unknown) => T): T | undefined;
|
|
18
|
+
write<T>(id: string, path: string, value: T, parse: (v: unknown) => T, create?: boolean): void;
|
|
19
|
+
create(value: AgentDescriptor, run: AgentLaunch): void;
|
|
20
|
+
list(): AgentDescriptor[];
|
|
21
|
+
messages(id: string): AgentMessage[];
|
|
22
|
+
completions(id: string): AgentCycle[];
|
|
23
|
+
reference(id: string): AgentSessionReference;
|
|
24
|
+
session(id: string): string | null;
|
|
25
|
+
/** The child fixes the native path before accepting input; never reset it to null. */
|
|
26
|
+
bindSession(id: string, file: string): void;
|
|
27
|
+
/** Short controller edits are serialized; an abandoned lock fails closed. */
|
|
28
|
+
control<T>(work: () => T): T;
|
|
29
|
+
enqueue(id: string, text: string, delivery: AgentMessage["delivery"], files?: string[], requestId?: string): AgentMessage;
|
|
30
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { canonical, object } from "../common/json.js";
|
|
5
|
+
import { requireThat } from "../common/validation.js";
|
|
6
|
+
import { readJsonFile, writeJsonFile } from "./atomic-json.js";
|
|
7
|
+
import { fileId, privateDirectory } from "./file-access.js";
|
|
8
|
+
const text = (v) => typeof v === "string" && v.length > 0;
|
|
9
|
+
const number = (v) => typeof v === "number" && Number.isFinite(v);
|
|
10
|
+
export function descriptor(v) {
|
|
11
|
+
requireThat(object(v) && text(v.agentId) && text(v.release) && text(v.workspace) && text(v.cwd) && text(v.parentSessionId) &&
|
|
12
|
+
(v.parentLeaf === null || text(v.parentLeaf)) && number(v.createdAt) && object(v.options), "子会话身份无效。");
|
|
13
|
+
fileId(v.agentId);
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
export function sessionRef(v) {
|
|
17
|
+
requireThat(object(v) && (v.file === null || (text(v.file) && isAbsolute(v.file) && !v.file.includes("\0"))), "子会话引用无效。");
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
20
|
+
function dshSessionRef(v) {
|
|
21
|
+
requireThat(object(v) && v.host === "dsh" && text(v.sessionId), "dsh 原生会话引用无效。");
|
|
22
|
+
fileId(v.sessionId);
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
export function launch(v) {
|
|
26
|
+
requireThat(object(v) && text(v.generation) && text(v.name) && Number.isSafeInteger(v.sequence) && typeof v.stop === "boolean" && number(v.startedAt), "子会话启动状态无效。");
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
export function message(v) {
|
|
30
|
+
requireThat(object(v) && text(v.requestId) && text(v.generation) && Number.isSafeInteger(v.sequence) && typeof v.message === "string" &&
|
|
31
|
+
["steer", "followUp"].includes(String(v.delivery)) && Array.isArray(v.files) && v.files.every(text), "子会话消息无效。");
|
|
32
|
+
return v;
|
|
33
|
+
}
|
|
34
|
+
export function cycle(v) {
|
|
35
|
+
requireThat(object(v) && text(v.cycleId) && text(v.generation) && number(v.startedAt) &&
|
|
36
|
+
["running", "completed", "failed", "cancelled", "interrupted"].includes(String(v.state)) &&
|
|
37
|
+
(v.leaf === null || text(v.leaf)) && Array.isArray(v.requestIds) && v.requestIds.every(text) &&
|
|
38
|
+
["parent", "interactive", "mixed"].includes(String(v.source)) && (v.finishedAt === undefined || number(v.finishedAt)), "子会话周期无效。");
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
export function status(v) {
|
|
42
|
+
requireThat(object(v) && text(v.generation) && ["starting", "busy", "idle", "stopping", "stopped", "error"].includes(String(v.state)) &&
|
|
43
|
+
typeof v.activity === "string" && number(v.updatedAt), "子会话状态无效。");
|
|
44
|
+
if (v.cycle)
|
|
45
|
+
cycle(v.cycle);
|
|
46
|
+
return v;
|
|
47
|
+
}
|
|
48
|
+
export class AgentFiles {
|
|
49
|
+
root;
|
|
50
|
+
host;
|
|
51
|
+
#failure;
|
|
52
|
+
constructor(root, host = "pi") {
|
|
53
|
+
this.root = root;
|
|
54
|
+
this.host = host;
|
|
55
|
+
}
|
|
56
|
+
check() { if (this.#failure)
|
|
57
|
+
throw this.#failure; }
|
|
58
|
+
fail(error) { this.#failure = error; }
|
|
59
|
+
directory(id) { return join(this.root, fileId(id)); }
|
|
60
|
+
read(id, path, parse) { return readJsonFile(this.directory(id), path, parse); }
|
|
61
|
+
optional(id, path, parse) {
|
|
62
|
+
try {
|
|
63
|
+
return this.read(id, path, parse);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error.code === "ENOENT")
|
|
67
|
+
return undefined;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
write(id, path, value, parse, create = false) {
|
|
72
|
+
this.check();
|
|
73
|
+
parse(value);
|
|
74
|
+
let published;
|
|
75
|
+
try {
|
|
76
|
+
published = writeJsonFile(this.directory(id), path, value, parse, create);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
this.fail(error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
requireThat(published, "子会话文件已存在。");
|
|
83
|
+
}
|
|
84
|
+
create(value, run) {
|
|
85
|
+
this.check();
|
|
86
|
+
mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
87
|
+
mkdirSync(this.directory(value.agentId), { mode: 0o700 });
|
|
88
|
+
for (const dir of ["inbox", "completions"])
|
|
89
|
+
privateDirectory(this.directory(value.agentId), dir);
|
|
90
|
+
this.write(value.agentId, "descriptor.json", value, descriptor, true);
|
|
91
|
+
this.write(value.agentId, "launch.json", run, launch, true);
|
|
92
|
+
if (this.host === "pi")
|
|
93
|
+
this.write(value.agentId, "session-ref.json", { file: null }, sessionRef, true);
|
|
94
|
+
else
|
|
95
|
+
this.write(value.agentId, "native-session.json", { host: "dsh", sessionId: value.agentId }, dshSessionRef, true);
|
|
96
|
+
}
|
|
97
|
+
list() {
|
|
98
|
+
if (!existsSync(this.root))
|
|
99
|
+
return [];
|
|
100
|
+
return readdirSync(this.root, { withFileTypes: true }).filter(e => e.isDirectory() && !e.name.startsWith(".")).flatMap(e => {
|
|
101
|
+
const d = this.optional(e.name, "descriptor.json", descriptor);
|
|
102
|
+
return d ? [d] : [];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
messages(id) {
|
|
106
|
+
return readdirSync(join(this.directory(id), "inbox")).filter(n => n.endsWith(".json"))
|
|
107
|
+
.map(n => this.read(id, "inbox/" + n, message)).sort((a, b) => a.sequence - b.sequence);
|
|
108
|
+
}
|
|
109
|
+
completions(id) {
|
|
110
|
+
return readdirSync(join(this.directory(id), "completions")).filter(n => n.endsWith(".json"))
|
|
111
|
+
.map(n => this.read(id, "completions/" + n, cycle)).sort((a, b) => (a.finishedAt ?? 0) - (b.finishedAt ?? 0));
|
|
112
|
+
}
|
|
113
|
+
reference(id) {
|
|
114
|
+
if (this.host === "pi")
|
|
115
|
+
return { host: "pi", file: this.session(id) };
|
|
116
|
+
const ref = this.read(id, "native-session.json", dshSessionRef);
|
|
117
|
+
requireThat(ref.sessionId === id, "dsh 原生会话身份不匹配。");
|
|
118
|
+
return ref;
|
|
119
|
+
}
|
|
120
|
+
session(id) {
|
|
121
|
+
requireThat(this.host === "pi", "该宿主不使用 Pi 会话文件引用。");
|
|
122
|
+
const ref = this.optional(id, "session-ref.json", sessionRef);
|
|
123
|
+
requireThat(ref, "缺少原生会话引用;不导入旧记录,请重新 start。");
|
|
124
|
+
return ref.file;
|
|
125
|
+
}
|
|
126
|
+
/** The child fixes the native path before accepting input; never reset it to null. */
|
|
127
|
+
bindSession(id, file) {
|
|
128
|
+
const previous = this.session(id);
|
|
129
|
+
requireThat(previous === null || previous === file, "子会话文件引用不匹配。");
|
|
130
|
+
if (previous === null)
|
|
131
|
+
this.write(id, "session-ref.json", { file }, sessionRef);
|
|
132
|
+
}
|
|
133
|
+
/** Short controller edits are serialized; an abandoned lock fails closed. */
|
|
134
|
+
control(work) {
|
|
135
|
+
this.check();
|
|
136
|
+
mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
137
|
+
const path = join(this.root, ".control");
|
|
138
|
+
try {
|
|
139
|
+
mkdirSync(path, { mode: 0o700 });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error.code !== "EEXIST")
|
|
143
|
+
throw error;
|
|
144
|
+
throw Object.assign(new Error("子会话控制正在进行;稍后重试。崩溃遗留的 .control 需停止相关执行实例后手动移除。"), { code: "PBOT_CONTROL_BUSY" });
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return work();
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
rmSync(path, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
enqueue(id, text, delivery, files = [], requestId = randomUUID()) {
|
|
154
|
+
fileId(requestId);
|
|
155
|
+
const run = this.read(id, "launch.json", launch), previous = this.optional(id, "inbox/" + requestId + ".json", message);
|
|
156
|
+
if (previous) {
|
|
157
|
+
requireThat(previous.generation === run.generation && previous.message === text && previous.delivery === delivery && canonical(previous.files) === canonical(files), "请求 ID 已被不同输入使用。");
|
|
158
|
+
return previous;
|
|
159
|
+
}
|
|
160
|
+
run.sequence++;
|
|
161
|
+
this.write(id, "launch.json", run, launch);
|
|
162
|
+
const value = { requestId, generation: run.generation, sequence: run.sequence, message: text, delivery, files };
|
|
163
|
+
this.write(id, "inbox/" + value.requestId + ".json", value, message, true);
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The root must already exist. Never follow aliases inside internal storage. */
|
|
2
|
+
export declare function internalPath(directory: string, relative: string): string;
|
|
3
|
+
export declare function syncDirectory(directory: string): void;
|
|
4
|
+
/** Validators return plain data and must reject incomplete contracts. */
|
|
5
|
+
export declare function readJsonFile<T>(directory: string, relative: string, validate: (value: unknown) => T): T;
|
|
6
|
+
/**
|
|
7
|
+
* A failure after publication does not mean that the old value remains.
|
|
8
|
+
* Callers must stop writes/sends on I/O failure, even if a subsequent read succeeds.
|
|
9
|
+
* createOnly publishes by link, so an existing destination is never overwritten.
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeJsonFile<T>(directory: string, relative: string, value: unknown, validate: (value: unknown) => T, createOnly?: boolean): boolean;
|