dsbot-session-create 0.0.0-stage → 0.1.0
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.md +144 -2
- package/cordis.patch.yml +4 -0
- package/index.js +1178 -0
- package/package.json +35 -4
package/index.js
ADDED
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
|
|
4
|
+
// src/autoresume.ts
|
|
5
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
6
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
7
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/core.ts
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { appendFile, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
var PROMPT_TIMEOUT_MS = 30000;
|
|
15
|
+
var HISTORY_TIMEOUT_MS = 20000;
|
|
16
|
+
var SESSION_OPTION_TIMEOUT_MS = 1e4;
|
|
17
|
+
var MESSAGE_TIMEOUT_MS = 1e4;
|
|
18
|
+
var MESSAGE_CLIP = 2000;
|
|
19
|
+
var MESSAGES_MAX = 200;
|
|
20
|
+
var MESSAGES_DEFAULT = 40;
|
|
21
|
+
function pickService(ctx, name) {
|
|
22
|
+
try {
|
|
23
|
+
const direct = ctx?.[name];
|
|
24
|
+
if (direct !== undefined && direct !== null)
|
|
25
|
+
return direct;
|
|
26
|
+
} catch {}
|
|
27
|
+
try {
|
|
28
|
+
return ctx?.get?.(name);
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function resolveServices(ctx) {
|
|
34
|
+
return {
|
|
35
|
+
get: (name) => safeGet(ctx, name),
|
|
36
|
+
tools: pickService(ctx, "tools"),
|
|
37
|
+
sessionController: pickService(ctx, "sessionController"),
|
|
38
|
+
sessionTitle: pickService(ctx, "sessionTitle"),
|
|
39
|
+
workspaceRegistry: pickService(ctx, "workspaceRegistry"),
|
|
40
|
+
agentPresets: pickService(ctx, "agentPresets"),
|
|
41
|
+
commands: pickService(ctx, "commands")
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function safeGet(ctx, name) {
|
|
45
|
+
try {
|
|
46
|
+
return ctx?.get?.(name);
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class SessionCreateError extends Error {
|
|
53
|
+
code;
|
|
54
|
+
constructor(code, message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "SessionCreateError";
|
|
57
|
+
this.code = code;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function reasonOf(error) {
|
|
61
|
+
return error instanceof Error ? error.message : String(error);
|
|
62
|
+
}
|
|
63
|
+
function preflight(ctx) {
|
|
64
|
+
const missing = [];
|
|
65
|
+
if (typeof ctx?.tools?.register !== "function")
|
|
66
|
+
missing.push("tools.register");
|
|
67
|
+
if (typeof ctx?.sessionController?.create !== "function")
|
|
68
|
+
missing.push("sessionController.create");
|
|
69
|
+
if (typeof ctx?.sessionController?.prompt !== "function")
|
|
70
|
+
missing.push("sessionController.prompt");
|
|
71
|
+
if (typeof ctx?.sessionController?.resolveAgent !== "function")
|
|
72
|
+
missing.push("sessionController.resolveAgent");
|
|
73
|
+
if (typeof ctx?.sessionTitle?.rename !== "function")
|
|
74
|
+
missing.push("sessionTitle.rename");
|
|
75
|
+
if (typeof ctx?.workspaceRegistry?.get !== "function")
|
|
76
|
+
missing.push("workspaceRegistry.get");
|
|
77
|
+
if (typeof ctx?.workspaceRegistry?.archiveSession !== "function")
|
|
78
|
+
missing.push("workspaceRegistry.archiveSession");
|
|
79
|
+
return missing;
|
|
80
|
+
}
|
|
81
|
+
function preflightRead(ctx) {
|
|
82
|
+
const missing = [];
|
|
83
|
+
if (typeof ctx?.sessionController?.resolveAgent !== "function")
|
|
84
|
+
missing.push("sessionController.resolveAgent");
|
|
85
|
+
if (typeof ctx?.sessionController?.page !== "function")
|
|
86
|
+
missing.push("sessionController.page");
|
|
87
|
+
return missing;
|
|
88
|
+
}
|
|
89
|
+
function preflightOptions(ctx) {
|
|
90
|
+
const missing = [];
|
|
91
|
+
if (typeof ctx?.agentPresets?.list !== "function")
|
|
92
|
+
missing.push("agentPresets.list");
|
|
93
|
+
if (typeof ctx?.commands?.execute !== "function")
|
|
94
|
+
missing.push("commands.execute");
|
|
95
|
+
return missing;
|
|
96
|
+
}
|
|
97
|
+
function validate(args) {
|
|
98
|
+
const title = (args?.title ?? "").trim();
|
|
99
|
+
const prompt = (args?.prompt ?? "").trim();
|
|
100
|
+
const folder = args?.folder?.trim() || undefined;
|
|
101
|
+
const workspace = args?.workspace?.trim() || undefined;
|
|
102
|
+
if (title === "")
|
|
103
|
+
throw new SessionCreateError("missing-title", "a title is required; no Session was made.");
|
|
104
|
+
if (prompt === "")
|
|
105
|
+
throw new SessionCreateError("missing-prompt", "a prompt is required; no Session was made.");
|
|
106
|
+
if (folder !== undefined && workspace !== undefined) {
|
|
107
|
+
throw new SessionCreateError("two-placements", "give either folder or workspace, not both; no Session was made.");
|
|
108
|
+
}
|
|
109
|
+
if (folder === undefined && workspace === undefined) {
|
|
110
|
+
throw new SessionCreateError("no-placement", "give exactly one of folder or workspace; no Session was made.");
|
|
111
|
+
}
|
|
112
|
+
return { title, prompt, folder, workspace };
|
|
113
|
+
}
|
|
114
|
+
function readOptions(args) {
|
|
115
|
+
const preset = args?.preset?.trim() || undefined;
|
|
116
|
+
const permission = args?.permission?.trim() || undefined;
|
|
117
|
+
return {
|
|
118
|
+
...preset === undefined ? {} : { preset },
|
|
119
|
+
...permission === undefined ? {} : { permission }
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function isFolder(path) {
|
|
123
|
+
try {
|
|
124
|
+
return (await stat(path)).isDirectory();
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function resolvePlacement(ctx, folder, workspace) {
|
|
130
|
+
if (folder !== undefined) {
|
|
131
|
+
if (!await isFolder(folder)) {
|
|
132
|
+
throw new SessionCreateError("folder-missing", `"${folder}" is not an existing folder; no Session was made.`);
|
|
133
|
+
}
|
|
134
|
+
return { placement: { cwd: folder }, text: `folder ${folder}` };
|
|
135
|
+
}
|
|
136
|
+
const wanted = workspace;
|
|
137
|
+
const registry = ctx.workspaceRegistry;
|
|
138
|
+
const direct = registry.get?.(wanted);
|
|
139
|
+
const found = direct ?? await registry.resolveByPath?.(wanted);
|
|
140
|
+
if (!found) {
|
|
141
|
+
throw new SessionCreateError("workspace-unknown", `no DSH workspace matches "${wanted}"; no Session was made.`);
|
|
142
|
+
}
|
|
143
|
+
const id = found.id ?? (direct === undefined ? undefined : wanted);
|
|
144
|
+
if (typeof id !== "string" || id === "") {
|
|
145
|
+
throw new SessionCreateError("workspace-unknown", `the DSH workspace matched "${wanted}" but gave no id; no Session was made.`);
|
|
146
|
+
}
|
|
147
|
+
return { placement: { workspaceId: id }, text: `workspace ${found.title ?? id}` };
|
|
148
|
+
}
|
|
149
|
+
async function archiveQuietly(ctx, sessionId) {
|
|
150
|
+
try {
|
|
151
|
+
await ctx.workspaceRegistry?.archiveSession?.(sessionId, { stopActivity: true });
|
|
152
|
+
return { ok: true };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function shortOf(value) {
|
|
158
|
+
if (typeof value === "string")
|
|
159
|
+
return value;
|
|
160
|
+
if (value === undefined)
|
|
161
|
+
return "undefined";
|
|
162
|
+
if (value === null)
|
|
163
|
+
return "null";
|
|
164
|
+
try {
|
|
165
|
+
return JSON.stringify(value) ?? String(value);
|
|
166
|
+
} catch {
|
|
167
|
+
return String(value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function resolvePreset(ctx, preset) {
|
|
171
|
+
const presets = ctx.agentPresets;
|
|
172
|
+
if (typeof presets?.list !== "function") {
|
|
173
|
+
throw new SessionCreateError("host-missing", "this harness has no agentPresets.list, so `preset` could not be checked. No Session was made.");
|
|
174
|
+
}
|
|
175
|
+
const entries = await presets.list();
|
|
176
|
+
const all = Array.isArray(entries) ? entries : [];
|
|
177
|
+
const ids = all.map((entry) => entry?.id).filter((id) => typeof id === "string");
|
|
178
|
+
const found = all.find((entry) => entry?.id === preset);
|
|
179
|
+
if (found === undefined) {
|
|
180
|
+
const available = ids.length === 0 ? "(the harness reported none)" : ids.join(", ");
|
|
181
|
+
throw new SessionCreateError("preset-unknown", `no agent preset "${preset}" on this harness (available: ${available}). No Session was made.`);
|
|
182
|
+
}
|
|
183
|
+
if (found.broken !== undefined) {
|
|
184
|
+
const available = ids.length === 0 ? "(the harness reported none)" : ids.join(", ");
|
|
185
|
+
throw new SessionCreateError("preset-broken", `the agent preset "${preset}" is broken on this harness (${shortOf(found.broken)}); available: ${available}. No Session was made.`);
|
|
186
|
+
}
|
|
187
|
+
return preset;
|
|
188
|
+
}
|
|
189
|
+
function commandVerdict(outcome) {
|
|
190
|
+
const result = outcome?.result;
|
|
191
|
+
const kind = result?.kind;
|
|
192
|
+
if (kind === "success")
|
|
193
|
+
return { ok: true };
|
|
194
|
+
if (kind === "error") {
|
|
195
|
+
return { ok: false, reason: typeof result?.text === "string" && result.text !== "" ? result.text : "the host rejected the command" };
|
|
196
|
+
}
|
|
197
|
+
return { ok: false, reason: `unrecognized command result (kind=${shortOf(kind)}; raw=${shortOf(outcome).slice(0, 300)})` };
|
|
198
|
+
}
|
|
199
|
+
async function applyPermission(ctx, controller, sessionId, permission, signal, timeoutMs) {
|
|
200
|
+
const commands = ctx.commands;
|
|
201
|
+
if (typeof commands?.execute !== "function") {
|
|
202
|
+
throw new SessionCreateError("host-missing", "this harness has no commands.execute, so `permission` was not applied.");
|
|
203
|
+
}
|
|
204
|
+
const agent = (await controller.resolveAgent?.(sessionId))?.agent;
|
|
205
|
+
if (agent === undefined) {
|
|
206
|
+
throw new SessionCreateError("options-failed", "the Session was made but it could not be reached to set its permission (session unavailable).");
|
|
207
|
+
}
|
|
208
|
+
const outcome = await commands.execute(agent, `/permission ${permission}`, [], signal ?? AbortSignal.timeout(timeoutMs ?? SESSION_OPTION_TIMEOUT_MS));
|
|
209
|
+
const verdict = commandVerdict(outcome);
|
|
210
|
+
if (!verdict.ok) {
|
|
211
|
+
throw new SessionCreateError("options-failed", `the host refused the permission "${permission}": ${verdict.reason}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function recordFailedDispatch(ledgerPath, record) {
|
|
215
|
+
try {
|
|
216
|
+
await appendDispatch(ledgerPath, {
|
|
217
|
+
sessionId: record.sessionId,
|
|
218
|
+
title: record.title,
|
|
219
|
+
placement: record.placement,
|
|
220
|
+
dispatchedAt: Date.now(),
|
|
221
|
+
promptHead: record.promptHead,
|
|
222
|
+
...record.preset === undefined ? {} : { preset: record.preset },
|
|
223
|
+
...record.dispatcherSessionId === undefined ? {} : { dispatcherSessionId: record.dispatcherSessionId },
|
|
224
|
+
failed: record.failure
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return error instanceof Error ? error.message : String(error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function createSessionIn(ctx, args, opts = {}) {
|
|
232
|
+
const missing = preflight(ctx);
|
|
233
|
+
if (missing.length > 0) {
|
|
234
|
+
throw new SessionCreateError("host-missing", `this harness is missing ${missing.join(", ")}; no Session was made.`);
|
|
235
|
+
}
|
|
236
|
+
const { title, prompt, folder, workspace } = validate(args);
|
|
237
|
+
const options = readOptions(args);
|
|
238
|
+
const preset = options.preset === undefined ? undefined : await resolvePreset(ctx, options.preset);
|
|
239
|
+
if (options.permission !== undefined && typeof ctx.commands?.execute !== "function") {
|
|
240
|
+
throw new SessionCreateError("host-missing", "this harness has no commands.execute, so `permission` could not be applied. No Session was made.");
|
|
241
|
+
}
|
|
242
|
+
const { placement, text } = await resolvePlacement(ctx, folder, workspace);
|
|
243
|
+
const controller = ctx.sessionController;
|
|
244
|
+
const created = await controller.create?.({ ...placement, ...preset === undefined ? {} : { agentPreset: preset } });
|
|
245
|
+
const sessionId = created?.sessionId;
|
|
246
|
+
if (typeof sessionId !== "string" || sessionId === "") {
|
|
247
|
+
throw new SessionCreateError("no-session-id", "the host returned no session id; nothing was written.");
|
|
248
|
+
}
|
|
249
|
+
const ledgerPath = opts.ledger ?? defaultLedgerPath();
|
|
250
|
+
let renamed = true;
|
|
251
|
+
try {
|
|
252
|
+
const resolved = await controller.resolveAgent?.(sessionId);
|
|
253
|
+
if (!resolved || "error" in resolved) {
|
|
254
|
+
throw new Error(resolved?.error?.message ?? "session unavailable");
|
|
255
|
+
}
|
|
256
|
+
await ctx.sessionTitle?.rename?.(resolved.agent?.session, title);
|
|
257
|
+
} catch {
|
|
258
|
+
renamed = false;
|
|
259
|
+
}
|
|
260
|
+
if (options.permission !== undefined) {
|
|
261
|
+
try {
|
|
262
|
+
await applyPermission(ctx, controller, sessionId, options.permission, opts.signal, opts.timeoutMs);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const archived = await archiveQuietly(ctx, sessionId);
|
|
265
|
+
const tail = archived.ok ? `the Session ${sessionId} was archived.` : `the Session ${sessionId} may still be in the session list (archiving it failed: ${archived.reason}).`;
|
|
266
|
+
const failure = error instanceof Error ? error.message : String(error);
|
|
267
|
+
const ledgerNote = await recordFailedDispatch(ledgerPath, {
|
|
268
|
+
sessionId,
|
|
269
|
+
title,
|
|
270
|
+
placement: text,
|
|
271
|
+
promptHead: headOf(prompt, 200),
|
|
272
|
+
failure,
|
|
273
|
+
...preset === undefined ? {} : { preset },
|
|
274
|
+
...opts.dispatcherSessionId === undefined ? {} : { dispatcherSessionId: opts.dispatcherSessionId }
|
|
275
|
+
});
|
|
276
|
+
const note = ledgerNote === undefined ? ` It was recorded in the dispatch ledger (${ledgerPath}) so it can be found later.` : ` The dispatch ledger could not record it either (${ledgerNote}); keep this id by hand.`;
|
|
277
|
+
const code = error instanceof SessionCreateError ? error.code : "options-failed";
|
|
278
|
+
throw new SessionCreateError(code, `the Session was made but its permission was not applied (${reasonOf(error)}); ${tail}${note}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const applied = {
|
|
282
|
+
...preset === undefined ? {} : { preset },
|
|
283
|
+
...options.permission === undefined ? {} : { permission: options.permission }
|
|
284
|
+
};
|
|
285
|
+
try {
|
|
286
|
+
await controller.prompt?.({
|
|
287
|
+
requestId: randomUUID(),
|
|
288
|
+
sessionId,
|
|
289
|
+
mode: "queue",
|
|
290
|
+
content: [{ type: "text", text: prompt }]
|
|
291
|
+
}, opts.signal ?? AbortSignal.timeout(opts.timeoutMs ?? PROMPT_TIMEOUT_MS));
|
|
292
|
+
} catch (error) {
|
|
293
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
294
|
+
const archived = await archiveQuietly(ctx, sessionId);
|
|
295
|
+
const tail = archived.ok ? `the Session ${sessionId} was archived.` : `the Session ${sessionId} may still be in the session list (archiving it failed: ${archived.reason}).`;
|
|
296
|
+
throw new SessionCreateError("prompt-failed", `the Session was made but the prompt did not go through (${reason}); ${tail}`);
|
|
297
|
+
}
|
|
298
|
+
let ledgerError;
|
|
299
|
+
try {
|
|
300
|
+
await appendDispatch(ledgerPath, {
|
|
301
|
+
sessionId,
|
|
302
|
+
title,
|
|
303
|
+
placement: text,
|
|
304
|
+
dispatchedAt: Date.now(),
|
|
305
|
+
promptHead: headOf(prompt, 200),
|
|
306
|
+
...applied.preset === undefined ? {} : { preset: applied.preset },
|
|
307
|
+
...applied.permission === undefined ? {} : { permission: applied.permission },
|
|
308
|
+
...opts.dispatcherSessionId === undefined ? {} : { dispatcherSessionId: opts.dispatcherSessionId }
|
|
309
|
+
});
|
|
310
|
+
} catch (error) {
|
|
311
|
+
ledgerError = error instanceof Error ? error.message : String(error);
|
|
312
|
+
}
|
|
313
|
+
return { sessionId, title, placement: text, renamed, ...applied, ledgerPath, ...ledgerError === undefined ? {} : { ledgerError } };
|
|
314
|
+
}
|
|
315
|
+
var LEDGER_FILE = "dispatched-sessions.jsonl";
|
|
316
|
+
function defaultLedgerPath(env = process.env, home = homedir()) {
|
|
317
|
+
const explicit = (env.DSH_SESSION_CREATE_LEDGER ?? "").trim();
|
|
318
|
+
if (explicit !== "")
|
|
319
|
+
return explicit;
|
|
320
|
+
const root = (env.DSH_HOME ?? "").trim() || join(home, ".dsh");
|
|
321
|
+
return join(root, "bot", "session-create", LEDGER_FILE);
|
|
322
|
+
}
|
|
323
|
+
function headOf(text, max) {
|
|
324
|
+
if (text.length <= max)
|
|
325
|
+
return text;
|
|
326
|
+
return text.slice(0, /[\uD800-\uDBFF]/.test(text[max - 1] ?? "") ? max - 1 : max);
|
|
327
|
+
}
|
|
328
|
+
async function appendDispatch(ledgerPath, record) {
|
|
329
|
+
await mkdir(dirname(ledgerPath), { recursive: true });
|
|
330
|
+
await appendFile(ledgerPath, `${JSON.stringify(record)}
|
|
331
|
+
`, "utf8");
|
|
332
|
+
}
|
|
333
|
+
async function readLedger(ledgerPath) {
|
|
334
|
+
let text;
|
|
335
|
+
try {
|
|
336
|
+
text = await readFile(ledgerPath, "utf8");
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (error?.code === "ENOENT")
|
|
339
|
+
return [];
|
|
340
|
+
throw new SessionCreateError("ledger-unreadable", `the dispatch ledger ${ledgerPath} exists but could not be read (${reasonOf(error)}).`);
|
|
341
|
+
}
|
|
342
|
+
const bySession = new Map;
|
|
343
|
+
for (const line of text.split(`
|
|
344
|
+
`)) {
|
|
345
|
+
if (line.trim() === "")
|
|
346
|
+
continue;
|
|
347
|
+
try {
|
|
348
|
+
const record = JSON.parse(line);
|
|
349
|
+
if (typeof record?.sessionId !== "string" || record.sessionId === "")
|
|
350
|
+
continue;
|
|
351
|
+
bySession.set(record.sessionId, record);
|
|
352
|
+
} catch {}
|
|
353
|
+
}
|
|
354
|
+
const records = [...bySession.values()];
|
|
355
|
+
records.sort((a, b) => (Number(a.dispatchedAt) || 0) - (Number(b.dispatchedAt) || 0));
|
|
356
|
+
return records;
|
|
357
|
+
}
|
|
358
|
+
async function markRead(ledgerPath, sessionId, seq, at = Date.now()) {
|
|
359
|
+
const records = await readLedger(ledgerPath);
|
|
360
|
+
const at_ = records.findIndex((record) => record.sessionId === sessionId);
|
|
361
|
+
if (at_ < 0)
|
|
362
|
+
return false;
|
|
363
|
+
records[at_] = { ...records[at_], lastReadSeq: seq, lastReadAt: at };
|
|
364
|
+
const body = records.map((record) => `${JSON.stringify(record)}
|
|
365
|
+
`).join("");
|
|
366
|
+
const temp = `${ledgerPath}.tmp-${randomUUID()}`;
|
|
367
|
+
await writeFile(temp, body, "utf8");
|
|
368
|
+
await rename(temp, ledgerPath);
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
async function markNotified(ledgerPath, sessionId, at = Date.now()) {
|
|
372
|
+
const records = await readLedger(ledgerPath);
|
|
373
|
+
const index = records.findIndex((record) => record.sessionId === sessionId);
|
|
374
|
+
if (index < 0)
|
|
375
|
+
return false;
|
|
376
|
+
records[index] = { ...records[index], notifiedAt: at };
|
|
377
|
+
const body = records.map((record) => `${JSON.stringify(record)}
|
|
378
|
+
`).join("");
|
|
379
|
+
const temp = `${ledgerPath}.tmp-${randomUUID()}`;
|
|
380
|
+
await writeFile(temp, body, "utf8");
|
|
381
|
+
await rename(temp, ledgerPath);
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
function blockText(content) {
|
|
385
|
+
if (!Array.isArray(content))
|
|
386
|
+
return "";
|
|
387
|
+
return content.filter((block) => {
|
|
388
|
+
const b = block;
|
|
389
|
+
return b?.type === "text" && typeof b.text === "string";
|
|
390
|
+
}).map((block) => block.text).join(`
|
|
391
|
+
`).trim();
|
|
392
|
+
}
|
|
393
|
+
function clipText(text) {
|
|
394
|
+
if (text.length <= MESSAGE_CLIP)
|
|
395
|
+
return text;
|
|
396
|
+
return `${text.slice(0, MESSAGE_CLIP)}… (cut; ${text.length} characters in all)`;
|
|
397
|
+
}
|
|
398
|
+
function renderTranscript(events) {
|
|
399
|
+
let turn;
|
|
400
|
+
let headSeq = 0;
|
|
401
|
+
const ended = new Set;
|
|
402
|
+
const messages = [];
|
|
403
|
+
for (const raw of events) {
|
|
404
|
+
const event = raw;
|
|
405
|
+
if (event === null || typeof event !== "object")
|
|
406
|
+
continue;
|
|
407
|
+
if (typeof event.seq === "number" && Number.isInteger(event.seq))
|
|
408
|
+
headSeq = Math.max(headSeq, event.seq);
|
|
409
|
+
if (event.type === "turn/start") {
|
|
410
|
+
const n = event.data?.turn;
|
|
411
|
+
if (typeof n === "number" && Number.isInteger(n))
|
|
412
|
+
turn = n;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (event.type === "turn/end") {
|
|
416
|
+
const n = event.data?.turn;
|
|
417
|
+
if (typeof n === "number" && Number.isInteger(n))
|
|
418
|
+
ended.add(n);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (event.type === "user/message") {
|
|
422
|
+
const source = event.data?.source;
|
|
423
|
+
if (source?.kind !== "user")
|
|
424
|
+
continue;
|
|
425
|
+
const text = blockText(event.data?.content);
|
|
426
|
+
if (text === "")
|
|
427
|
+
continue;
|
|
428
|
+
messages.push({ role: "user", text: clipText(text), seq: event.seq ?? 0, time: event.time ?? 0 });
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (event.type === "assistant/message") {
|
|
432
|
+
const message = event.data?.message;
|
|
433
|
+
const text = blockText(message?.content);
|
|
434
|
+
if (text === "")
|
|
435
|
+
continue;
|
|
436
|
+
messages.push({ role: "assistant", text: clipText(text), seq: event.seq ?? 0, time: event.time ?? 0 });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const status = turn === undefined ? "not-started" : ended.has(turn) ? "idle" : "running";
|
|
440
|
+
return { status, turn, headSeq, messages };
|
|
441
|
+
}
|
|
442
|
+
async function readSessionIn(ctx, args = {}, opts = {}) {
|
|
443
|
+
const missing = preflightRead(ctx);
|
|
444
|
+
if (missing.length > 0) {
|
|
445
|
+
throw new SessionCreateError("host-missing", `this harness cannot read a Session's history: it is missing ${missing.join(", ")}.`);
|
|
446
|
+
}
|
|
447
|
+
const ledgerPath = opts.ledger ?? defaultLedgerPath();
|
|
448
|
+
const wanted = (args.sessionId ?? "").trim();
|
|
449
|
+
let fromLedger;
|
|
450
|
+
let sessionId = wanted;
|
|
451
|
+
let ledgerError;
|
|
452
|
+
if (sessionId === "") {
|
|
453
|
+
const records = await readLedger(ledgerPath);
|
|
454
|
+
fromLedger = records.at(-1);
|
|
455
|
+
if (fromLedger === undefined) {
|
|
456
|
+
throw new SessionCreateError("no-session", `give a session id: the dispatch ledger ${ledgerPath} has no entry yet.`);
|
|
457
|
+
}
|
|
458
|
+
sessionId = fromLedger.sessionId;
|
|
459
|
+
} else {
|
|
460
|
+
try {
|
|
461
|
+
fromLedger = (await readLedger(ledgerPath)).find((record) => record.sessionId === sessionId);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
ledgerError = reasonOf(error);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const controller = ctx.sessionController;
|
|
467
|
+
const resolved = await controller.resolveAgent?.(sessionId);
|
|
468
|
+
if (!resolved || "error" in resolved || resolved.agent?.session === undefined || resolved.agent.session === null) {
|
|
469
|
+
throw new SessionCreateError("session-unknown", `no Session "${sessionId}" is known to this harness${resolved?.error?.message ? ` (${resolved.error.message})` : ""}.`);
|
|
470
|
+
}
|
|
471
|
+
const seq = Number(resolved.agent.session.seq);
|
|
472
|
+
if (!Number.isFinite(seq)) {
|
|
473
|
+
throw new SessionCreateError("session-no-seq", `the harness gave no sequence number for Session "${sessionId}"; nothing was read.`);
|
|
474
|
+
}
|
|
475
|
+
const last = clamp(args.last ?? MESSAGES_DEFAULT, 1, MESSAGES_MAX);
|
|
476
|
+
let page;
|
|
477
|
+
try {
|
|
478
|
+
page = await controller.page?.({ address: { kind: "session", sessionId }, throughSeq: seq - 1, maxMessages: last }, opts.signal ?? AbortSignal.timeout(opts.timeoutMs ?? HISTORY_TIMEOUT_MS));
|
|
479
|
+
} catch (error) {
|
|
480
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
481
|
+
throw new SessionCreateError("read-failed", `the harness would not give Session "${sessionId}" back (${reason}).`);
|
|
482
|
+
}
|
|
483
|
+
if (page === undefined) {
|
|
484
|
+
throw new SessionCreateError("read-failed", `the harness returned nothing for Session "${sessionId}".`);
|
|
485
|
+
}
|
|
486
|
+
const rendered = renderTranscript((page.records ?? []).map((record) => record?.event));
|
|
487
|
+
const headSeq = Math.max(seq - 1, rendered.headSeq);
|
|
488
|
+
const newEvents = fromLedger?.lastReadSeq === undefined ? undefined : Math.max(0, headSeq - fromLedger.lastReadSeq);
|
|
489
|
+
const hostStatus = resolved.agent?.status;
|
|
490
|
+
const status = hostStatus === "running" ? "running" : hostStatus === "idle" ? "idle" : rendered.status;
|
|
491
|
+
const statusFrom = hostStatus === "running" || hostStatus === "idle" ? "host" : "transcript";
|
|
492
|
+
if (opts.markRead !== false && ledgerError === undefined) {
|
|
493
|
+
try {
|
|
494
|
+
await markRead(ledgerPath, sessionId, headSeq);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
ledgerError = reasonOf(error);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
...rendered,
|
|
501
|
+
status,
|
|
502
|
+
statusFrom,
|
|
503
|
+
headSeq,
|
|
504
|
+
sessionId,
|
|
505
|
+
hasMore: page.hasMore === true,
|
|
506
|
+
...fromLedger?.title === undefined ? {} : { title: fromLedger.title },
|
|
507
|
+
...fromLedger?.placement === undefined ? {} : { placement: fromLedger.placement },
|
|
508
|
+
...fromLedger?.dispatchedAt === undefined ? {} : { dispatchedAt: fromLedger.dispatchedAt },
|
|
509
|
+
...fromLedger?.lastReadSeq === undefined ? {} : { lastReadSeq: fromLedger.lastReadSeq },
|
|
510
|
+
...newEvents === undefined ? {} : { newEvents },
|
|
511
|
+
...ledgerError === undefined ? {} : { ledgerError }
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function preflightMessage(ctx) {
|
|
515
|
+
const missing = [];
|
|
516
|
+
if (typeof ctx?.sessionController?.resolveAgent !== "function")
|
|
517
|
+
missing.push("sessionController.resolveAgent");
|
|
518
|
+
if (typeof ctx?.sessionController?.prompt !== "function")
|
|
519
|
+
missing.push("sessionController.prompt");
|
|
520
|
+
return missing;
|
|
521
|
+
}
|
|
522
|
+
function validateMessage(args) {
|
|
523
|
+
const sessionId = (args?.sessionId ?? "").trim();
|
|
524
|
+
const message = (args?.message ?? "").trim();
|
|
525
|
+
if (sessionId === "")
|
|
526
|
+
throw new SessionCreateError("missing-session-id", "a sessionId is required; nothing was sent.");
|
|
527
|
+
if (message === "")
|
|
528
|
+
throw new SessionCreateError("missing-message", "a message is required; nothing was sent.");
|
|
529
|
+
return { sessionId, message };
|
|
530
|
+
}
|
|
531
|
+
async function messageSessionIn(ctx, args = {}, opts = {}) {
|
|
532
|
+
const missing = preflightMessage(ctx);
|
|
533
|
+
if (missing.length > 0) {
|
|
534
|
+
throw new SessionCreateError("host-missing", `this harness cannot send into a Session: it is missing ${missing.join(", ")}. Nothing was sent.`);
|
|
535
|
+
}
|
|
536
|
+
const { sessionId, message } = validateMessage(args);
|
|
537
|
+
const controller = ctx.sessionController;
|
|
538
|
+
const ledgerPath = opts.ledger ?? defaultLedgerPath();
|
|
539
|
+
let title;
|
|
540
|
+
let placement;
|
|
541
|
+
let ledgerError;
|
|
542
|
+
try {
|
|
543
|
+
const record = (await readLedger(ledgerPath)).find((entry) => entry.sessionId === sessionId);
|
|
544
|
+
title = record?.title;
|
|
545
|
+
placement = record?.placement;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
ledgerError = reasonOf(error);
|
|
548
|
+
}
|
|
549
|
+
const resolved = await controller.resolveAgent?.(sessionId);
|
|
550
|
+
if (!resolved || "error" in resolved || resolved.agent?.session === undefined || resolved.agent.session === null) {
|
|
551
|
+
throw new SessionCreateError("session-unknown", `no Session "${sessionId}" is known to this harness${resolved?.error?.message ? ` (${resolved.error.message})` : ""}; nothing was sent.`);
|
|
552
|
+
}
|
|
553
|
+
let outcome;
|
|
554
|
+
try {
|
|
555
|
+
outcome = await controller.prompt?.({
|
|
556
|
+
requestId: randomUUID(),
|
|
557
|
+
sessionId,
|
|
558
|
+
mode: "queue",
|
|
559
|
+
content: [{ type: "text", text: message }]
|
|
560
|
+
}, opts.signal ?? AbortSignal.timeout(opts.timeoutMs ?? MESSAGE_TIMEOUT_MS));
|
|
561
|
+
} catch (error) {
|
|
562
|
+
throw new SessionCreateError("message-failed", `the message did not go into Session "${sessionId}" (${reasonOf(error)}); nothing was delivered.`);
|
|
563
|
+
}
|
|
564
|
+
if (outcome === false) {
|
|
565
|
+
throw new SessionCreateError("message-refused", `the harness refused the message for Session "${sessionId}"; nothing was delivered.`);
|
|
566
|
+
}
|
|
567
|
+
return { sessionId, ...title === undefined ? {} : { title }, ...placement === undefined ? {} : { placement }, ...ledgerError === undefined ? {} : { ledgerError } };
|
|
568
|
+
}
|
|
569
|
+
async function listDispatchedIn(ctx, args = {}, opts = {}) {
|
|
570
|
+
const ledgerPath = opts.ledger ?? defaultLedgerPath();
|
|
571
|
+
const limit = clamp(args.limit ?? 5, 1, 20);
|
|
572
|
+
const records = (await readLedger(ledgerPath)).slice(-limit).reverse();
|
|
573
|
+
const lines = [];
|
|
574
|
+
for (const record of records) {
|
|
575
|
+
try {
|
|
576
|
+
const transcript = await readSessionIn(ctx, { sessionId: record.sessionId, last: 6 }, { ...opts, ledger: ledgerPath, markRead: false });
|
|
577
|
+
lines.push({
|
|
578
|
+
record,
|
|
579
|
+
status: transcript.status,
|
|
580
|
+
headSeq: transcript.headSeq,
|
|
581
|
+
...transcript.newEvents === undefined ? {} : { newEvents: transcript.newEvents }
|
|
582
|
+
});
|
|
583
|
+
} catch (error) {
|
|
584
|
+
lines.push({ record, error: error instanceof Error ? error.message : String(error) });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return { ledgerPath, lines };
|
|
588
|
+
}
|
|
589
|
+
function clamp(value, min, max) {
|
|
590
|
+
const n = Number.isFinite(value) ? Math.round(value) : min;
|
|
591
|
+
return Math.min(Math.max(n, min), max);
|
|
592
|
+
}
|
|
593
|
+
var SHANGHAI_OFFSET_MS = 8 * 60 * 60 * 1000;
|
|
594
|
+
function stamp(time) {
|
|
595
|
+
if (!Number.isFinite(time) || time <= 0)
|
|
596
|
+
return "?";
|
|
597
|
+
const d = new Date(time + SHANGHAI_OFFSET_MS);
|
|
598
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
599
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
600
|
+
}
|
|
601
|
+
var QUIET_WINDOW_MS = 120000;
|
|
602
|
+
var NOTIFY_TIMEOUT_MS = 1e4;
|
|
603
|
+
function dispatcherSessionOf(exec) {
|
|
604
|
+
const id = exec?.agent?.session?.id;
|
|
605
|
+
return typeof id === "string" && id !== "" ? id : undefined;
|
|
606
|
+
}
|
|
607
|
+
function receiptText(record, sessionId, endTime, turn, reason) {
|
|
608
|
+
const title = typeof record?.title === "string" && record.title !== "" ? record.title : "(untitled)";
|
|
609
|
+
const last = `turn ${turn === undefined ? "?" : turn}${reason === undefined ? "" : `, reason: ${reason}`}`;
|
|
610
|
+
return [
|
|
611
|
+
`[Dispatch receipt · ${title}]`,
|
|
612
|
+
`The Session ${sessionId} you dispatched went quiet at ${stamp(endTime)} (${last}).`,
|
|
613
|
+
`Read what it did with read_session_transcript (sessionId "${sessionId}"). No reply needed; act only if the result calls for it.`
|
|
614
|
+
].join(`
|
|
615
|
+
`);
|
|
616
|
+
}
|
|
617
|
+
function createDispatchNotifier(deps = {}) {
|
|
618
|
+
const ledgerPath = deps.ledger ?? defaultLedgerPath();
|
|
619
|
+
const quietMs = deps.quietMs ?? QUIET_WINDOW_MS;
|
|
620
|
+
const now = deps.now ?? (() => Date.now());
|
|
621
|
+
const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(() => {
|
|
622
|
+
fn();
|
|
623
|
+
}, ms));
|
|
624
|
+
const clearTimer = deps.clearTimer ?? ((handle2) => clearTimeout(handle2));
|
|
625
|
+
const log = deps.log ?? ((line) => console.warn(`[dsbot-session-create] ${line}`));
|
|
626
|
+
const pending = new Map;
|
|
627
|
+
const notified = new Set;
|
|
628
|
+
async function recordOf(sessionId) {
|
|
629
|
+
try {
|
|
630
|
+
return (await readLedger(ledgerPath)).find((record) => record.sessionId === sessionId);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
log(`the dispatch ledger could not be read, so no receipt was sent for ${sessionId} (${reasonOf(error)}).`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function resolveQuietly(controller, sessionId) {
|
|
637
|
+
try {
|
|
638
|
+
return await controller.resolveAgent?.(sessionId);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
log(`the harness would not resolve the Session ${sessionId} (${reasonOf(error)}).`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async function deliver(sessionId, endTime, turn, reason) {
|
|
645
|
+
if (notified.has(sessionId))
|
|
646
|
+
return;
|
|
647
|
+
const record = await recordOf(sessionId);
|
|
648
|
+
if (record === undefined)
|
|
649
|
+
return;
|
|
650
|
+
if (typeof record.notifiedAt === "number" && record.notifiedAt >= endTime)
|
|
651
|
+
return;
|
|
652
|
+
const dispatcher = record.dispatcherSessionId;
|
|
653
|
+
if (dispatcher === undefined || dispatcher === "") {
|
|
654
|
+
log(`${sessionId} went quiet, but its ledger line has no dispatcherSessionId (it was dispatched before this feature), so no receipt was sent.`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (dispatcher === sessionId)
|
|
658
|
+
return;
|
|
659
|
+
const controller = deps.controllerOf?.();
|
|
660
|
+
if (typeof controller?.resolveAgent !== "function" || typeof controller?.prompt !== "function") {
|
|
661
|
+
log(`${sessionId} went quiet, but this harness has no sessionController.resolveAgent/prompt, so no receipt was sent.`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const resolved = await resolveQuietly(controller, sessionId);
|
|
665
|
+
if (resolved?.agent?.status !== "idle") {
|
|
666
|
+
log(`${sessionId} went quiet, but the harness does not report it idle (status=${shortOf(resolved?.agent?.status)}), so no receipt was sent.`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const target = await resolveQuietly(controller, dispatcher);
|
|
670
|
+
if (target === undefined || target.error !== undefined || target.agent === undefined) {
|
|
671
|
+
log(`${sessionId} went quiet, but the dispatching Session ${dispatcher} is gone, so there is nowhere to send the receipt.`);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
await controller.prompt({
|
|
676
|
+
requestId: randomUUID(),
|
|
677
|
+
sessionId: dispatcher,
|
|
678
|
+
mode: "queue",
|
|
679
|
+
content: [{ type: "text", text: receiptText(record, sessionId, endTime, turn, reason) }]
|
|
680
|
+
}, AbortSignal.timeout(NOTIFY_TIMEOUT_MS));
|
|
681
|
+
} catch (error) {
|
|
682
|
+
log(`the receipt for ${sessionId} did not go through (${reasonOf(error)}); the next turn to end will try again.`);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
notified.add(sessionId);
|
|
686
|
+
try {
|
|
687
|
+
await markNotified(ledgerPath, sessionId, now());
|
|
688
|
+
} catch (error) {
|
|
689
|
+
log(`the receipt for ${sessionId} was delivered, but the ledger could not record notifiedAt (${reasonOf(error)}).`);
|
|
690
|
+
}
|
|
691
|
+
log(`receipt for ${sessionId} delivered to the dispatching Session ${dispatcher}.`);
|
|
692
|
+
}
|
|
693
|
+
async function handle(session, event) {
|
|
694
|
+
const sessionId = typeof session?.id === "string" ? session.id : undefined;
|
|
695
|
+
if (sessionId === undefined || sessionId === "")
|
|
696
|
+
return;
|
|
697
|
+
const type = event?.type;
|
|
698
|
+
if (type !== "turn/start" && type !== "turn/end")
|
|
699
|
+
return;
|
|
700
|
+
const record = await recordOf(sessionId);
|
|
701
|
+
if (record === undefined)
|
|
702
|
+
return;
|
|
703
|
+
if (type === "turn/start") {
|
|
704
|
+
const previous2 = pending.get(sessionId);
|
|
705
|
+
if (previous2 !== undefined) {
|
|
706
|
+
clearTimer(previous2.handle);
|
|
707
|
+
pending.delete(sessionId);
|
|
708
|
+
}
|
|
709
|
+
notified.delete(sessionId);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const reason = typeof event?.data?.reason?.kind === "string" ? event.data.reason.kind : undefined;
|
|
713
|
+
if (reason === "aborted")
|
|
714
|
+
return;
|
|
715
|
+
const previous = pending.get(sessionId);
|
|
716
|
+
if (previous !== undefined)
|
|
717
|
+
clearTimer(previous.handle);
|
|
718
|
+
const endTime = now();
|
|
719
|
+
const turn = typeof event?.data?.turn === "number" ? event.data.turn : undefined;
|
|
720
|
+
const handle2 = setTimer(() => {
|
|
721
|
+
pending.delete(sessionId);
|
|
722
|
+
return deliver(sessionId, endTime, turn, reason);
|
|
723
|
+
}, quietMs);
|
|
724
|
+
pending.set(sessionId, { endTime, handle: handle2 });
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
async onEvent(session, event) {
|
|
728
|
+
try {
|
|
729
|
+
await handle(session, event);
|
|
730
|
+
} catch (error) {
|
|
731
|
+
log(`a session event could not be handled (${reasonOf(error)}).`);
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
pending: () => [...pending.entries()].map(([sessionId, quiet]) => ({ sessionId, endTime: quiet.endTime }))
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function installDispatchNotify(ctx, opts = {}) {
|
|
738
|
+
const on = ctx?.on;
|
|
739
|
+
if (typeof on !== "function") {
|
|
740
|
+
console.warn("[dsbot-session-create] this harness context has no `on`, so a Session that finishes cannot report back by itself; read_session_transcript still works.");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const notifier = createDispatchNotifier({ ...opts, controllerOf: () => resolveServices(ctx).sessionController });
|
|
744
|
+
on.call(ctx, "session/event", (session, event) => {
|
|
745
|
+
notifier.onEvent(session, event);
|
|
746
|
+
});
|
|
747
|
+
return notifier;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/autoresume.ts
|
|
751
|
+
var AUTORESUME_ENV_FLAG = "DSH_AUTORESUME_DISABLE";
|
|
752
|
+
var AUTORESUME_STATE_DIR = "F:\\DSBOT\\state";
|
|
753
|
+
var AUTORESUME_PENDING_FILE = join2(AUTORESUME_STATE_DIR, "restart-pending.json");
|
|
754
|
+
var AUTORESUME_ACK_FILE = join2(AUTORESUME_STATE_DIR, "restart-pending.ack.json");
|
|
755
|
+
var AUTORESUME_RETRY_DELAYS_MS = [6000, 12000, 20000];
|
|
756
|
+
var AUTORESUME_DELAY_MS = AUTORESUME_RETRY_DELAYS_MS[0];
|
|
757
|
+
var AUTORESUME_TIMEOUT_MS = 1e4;
|
|
758
|
+
function reasonOf2(error) {
|
|
759
|
+
return error instanceof Error ? error.message : String(error);
|
|
760
|
+
}
|
|
761
|
+
async function writeAtomicDefault(path, text) {
|
|
762
|
+
const dir = dirname2(path);
|
|
763
|
+
await mkdir2(dir, { recursive: true });
|
|
764
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
765
|
+
await writeFile2(tmp, text, "utf8");
|
|
766
|
+
await rename2(tmp, path);
|
|
767
|
+
}
|
|
768
|
+
function isStrictlyNewer(candidate, acked) {
|
|
769
|
+
const fresh = Date.parse(candidate);
|
|
770
|
+
const old = Date.parse(acked);
|
|
771
|
+
if (!Number.isNaN(fresh) && !Number.isNaN(old))
|
|
772
|
+
return fresh > old;
|
|
773
|
+
return candidate > acked;
|
|
774
|
+
}
|
|
775
|
+
function wakeupText(pointer, pendingFile) {
|
|
776
|
+
const raw = pointer?.host_pid;
|
|
777
|
+
const pid = typeof raw === "number" || typeof raw === "string" ? String(raw) : "未知";
|
|
778
|
+
return `[重启自检] 宿主已重启(上一个宿主 pid ${pid})。盘上有接手指针:${pendingFile} —— 先读它再动手,不要凭记忆。`;
|
|
779
|
+
}
|
|
780
|
+
async function recordFailure(input) {
|
|
781
|
+
const record = {
|
|
782
|
+
result: "send-failed",
|
|
783
|
+
attempts: input.attempts,
|
|
784
|
+
reason: input.reason,
|
|
785
|
+
attempted_at: new Date(input.now()).toISOString()
|
|
786
|
+
};
|
|
787
|
+
if (input.sessionId !== undefined && input.sessionId !== "")
|
|
788
|
+
record.session_id = input.sessionId;
|
|
789
|
+
try {
|
|
790
|
+
await input.writeAtomic(input.ackFile, `${JSON.stringify(record, null, 2)}
|
|
791
|
+
`);
|
|
792
|
+
return;
|
|
793
|
+
} catch (error) {
|
|
794
|
+
const ackError = reasonOf2(error);
|
|
795
|
+
input.log(`startup self-check: the failure record could not be written to ${input.ackFile} (${ackError}); the next start will still try again.`);
|
|
796
|
+
return ackError;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function readAckedWrittenAt(ackFile, readText, log) {
|
|
800
|
+
let text;
|
|
801
|
+
try {
|
|
802
|
+
text = await readText(ackFile);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
if (error?.code !== "ENOENT") {
|
|
805
|
+
log(`startup self-check: ${ackFile} could not be read (${reasonOf2(error)}), so this pointer is treated as not yet confirmed.`);
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
let parsed;
|
|
810
|
+
try {
|
|
811
|
+
parsed = JSON.parse(text);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
log(`startup self-check: ${ackFile} is not valid JSON (${reasonOf2(error)}), so this pointer is treated as not yet confirmed.`);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const writtenAt = parsed?.written_at;
|
|
817
|
+
if (typeof writtenAt !== "string" || writtenAt === "") {
|
|
818
|
+
log(`startup self-check: ${ackFile} has no written_at (a failure record, or an old format), so this pointer is treated as not yet confirmed.`);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
return writtenAt;
|
|
822
|
+
}
|
|
823
|
+
async function runAutoResume(deps = {}) {
|
|
824
|
+
const env = deps.env ?? process.env;
|
|
825
|
+
const log = deps.log ?? ((line) => console.warn(`[dsbot-session-create] ${line}`));
|
|
826
|
+
const attempts = Number.isFinite(deps.attempt) && deps.attempt >= 1 ? Math.trunc(deps.attempt) : 1;
|
|
827
|
+
if (env[AUTORESUME_ENV_FLAG] === "1") {
|
|
828
|
+
const reason = `${AUTORESUME_ENV_FLAG}=1`;
|
|
829
|
+
log(`startup self-check skipped (${reason}); no pointer was read.`);
|
|
830
|
+
return { status: "disabled", reason };
|
|
831
|
+
}
|
|
832
|
+
const pendingFile = deps.pendingFile ?? AUTORESUME_PENDING_FILE;
|
|
833
|
+
const ackFile = deps.ackFile ?? AUTORESUME_ACK_FILE;
|
|
834
|
+
const readText = deps.readText ?? ((path) => readFile2(path, "utf8"));
|
|
835
|
+
const writeAtomic = deps.writeAtomic ?? writeAtomicDefault;
|
|
836
|
+
const now = deps.now ?? (() => Date.now());
|
|
837
|
+
let pointer;
|
|
838
|
+
try {
|
|
839
|
+
const text2 = await readText(pendingFile);
|
|
840
|
+
const parsed = JSON.parse(text2);
|
|
841
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
842
|
+
const reason = `${pendingFile} is not a JSON object`;
|
|
843
|
+
log(`startup self-check: ${reason}; nothing was sent.`);
|
|
844
|
+
return { status: "no-pointer", reason };
|
|
845
|
+
}
|
|
846
|
+
pointer = parsed;
|
|
847
|
+
} catch (error) {
|
|
848
|
+
const reason = reasonOf2(error);
|
|
849
|
+
log(`startup self-check: no usable pointer at ${pendingFile} (${reason}); nothing was sent.`);
|
|
850
|
+
return { status: "no-pointer", reason };
|
|
851
|
+
}
|
|
852
|
+
const writtenAt = pointer.written_at;
|
|
853
|
+
if (typeof writtenAt !== "string" || writtenAt === "") {
|
|
854
|
+
const reason = `${pendingFile} has no written_at`;
|
|
855
|
+
log(`startup self-check: ${reason}, so it cannot be compared with the ack; nothing was sent.`);
|
|
856
|
+
return { status: "written-at-missing", reason };
|
|
857
|
+
}
|
|
858
|
+
const acked = await readAckedWrittenAt(ackFile, readText, log);
|
|
859
|
+
if (acked !== undefined && !isStrictlyNewer(writtenAt, acked)) {
|
|
860
|
+
log(`startup self-check: the pointer written at ${writtenAt} was already confirmed (ack ${acked}); nothing was sent.`);
|
|
861
|
+
return { status: "already-acked", writtenAt };
|
|
862
|
+
}
|
|
863
|
+
const notify = pointer.notify_session_id;
|
|
864
|
+
const sessionId = typeof notify === "string" ? notify.trim() : "";
|
|
865
|
+
if (sessionId === "") {
|
|
866
|
+
const reason = `${pendingFile} has no usable notify_session_id (found ${typeof notify})`;
|
|
867
|
+
log(`startup self-check: ${reason}; nothing was sent.`);
|
|
868
|
+
return { status: "no-target", reason };
|
|
869
|
+
}
|
|
870
|
+
const fail = async (reason) => {
|
|
871
|
+
log(`startup self-check: attempt ${attempts} did not wake Session "${sessionId}" (${reason}); the pointer stays unconfirmed.`);
|
|
872
|
+
const ackError2 = await recordFailure({ ackFile, writeAtomic, log, reason, attempts, sessionId, now });
|
|
873
|
+
return { status: "send-failed", writtenAt, sessionId, reason, attempts, ...ackError2 === undefined ? {} : { ackError: ackError2 } };
|
|
874
|
+
};
|
|
875
|
+
const controller = deps.controllerOf?.();
|
|
876
|
+
const text = wakeupText(pointer, pendingFile);
|
|
877
|
+
if (typeof controller?.prompt !== "function") {
|
|
878
|
+
return await fail("this harness has no sessionController.prompt");
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
const outcome = await controller.prompt({
|
|
882
|
+
requestId: randomUUID2(),
|
|
883
|
+
sessionId,
|
|
884
|
+
mode: "queue",
|
|
885
|
+
content: [{ type: "text", text }]
|
|
886
|
+
}, deps.signal ?? AbortSignal.timeout(AUTORESUME_TIMEOUT_MS));
|
|
887
|
+
if (outcome === false) {
|
|
888
|
+
return await fail(`the harness refused the message for Session "${sessionId}"`);
|
|
889
|
+
}
|
|
890
|
+
} catch (error) {
|
|
891
|
+
return await fail(reasonOf2(error));
|
|
892
|
+
}
|
|
893
|
+
const deliveredAt = now();
|
|
894
|
+
const ack = {
|
|
895
|
+
written_at: writtenAt,
|
|
896
|
+
host_pid: typeof pointer.host_pid === "number" || typeof pointer.host_pid === "string" ? pointer.host_pid : null,
|
|
897
|
+
host_start: pointer.host_start ?? null,
|
|
898
|
+
session_id: sessionId,
|
|
899
|
+
result: "delivered",
|
|
900
|
+
attempts,
|
|
901
|
+
delivered_at: new Date(deliveredAt).toISOString(),
|
|
902
|
+
pending_file: pendingFile,
|
|
903
|
+
message: text
|
|
904
|
+
};
|
|
905
|
+
let ackError;
|
|
906
|
+
try {
|
|
907
|
+
await writeAtomic(ackFile, `${JSON.stringify(ack, null, 2)}
|
|
908
|
+
`);
|
|
909
|
+
} catch (error) {
|
|
910
|
+
ackError = reasonOf2(error);
|
|
911
|
+
log(`startup self-check: delivered to ${sessionId}, but the ack could not be written to ${ackFile} (${ackError}); the next start will deliver once more.`);
|
|
912
|
+
}
|
|
913
|
+
log(`startup self-check: the pointer written at ${writtenAt} woke Session ${sessionId} (attempt ${attempts}).`);
|
|
914
|
+
return { status: "delivered", writtenAt, sessionId, attempts, ...ackError === undefined ? {} : { ackError } };
|
|
915
|
+
}
|
|
916
|
+
function defaultSchedule(fn, ms) {
|
|
917
|
+
const handle = setTimeout(() => {
|
|
918
|
+
fn();
|
|
919
|
+
}, ms);
|
|
920
|
+
handle.unref?.();
|
|
921
|
+
return handle;
|
|
922
|
+
}
|
|
923
|
+
function installAutoResume(ctx, opts = {}) {
|
|
924
|
+
const log = opts.log ?? ((line) => console.warn(`[dsbot-session-create] ${line}`));
|
|
925
|
+
const env = opts.env ?? process.env;
|
|
926
|
+
if (env[AUTORESUME_ENV_FLAG] === "1") {
|
|
927
|
+
log(`startup self-check is off (${AUTORESUME_ENV_FLAG}=1); no pointer will be read.`);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const schedule = opts.schedule ?? defaultSchedule;
|
|
931
|
+
const configured = opts.retryDelaysMs ?? AUTORESUME_RETRY_DELAYS_MS;
|
|
932
|
+
const delays = configured.filter((ms) => Number.isFinite(ms) && ms >= 0);
|
|
933
|
+
if (delays.length === 0)
|
|
934
|
+
delays.push(AUTORESUME_DELAY_MS);
|
|
935
|
+
if (opts.delayMs !== undefined)
|
|
936
|
+
delays[0] = opts.delayMs;
|
|
937
|
+
const baseDeps = {
|
|
938
|
+
pendingFile: opts.pendingFile,
|
|
939
|
+
ackFile: opts.ackFile,
|
|
940
|
+
env,
|
|
941
|
+
log,
|
|
942
|
+
readText: opts.readText,
|
|
943
|
+
writeAtomic: opts.writeAtomic,
|
|
944
|
+
signal: opts.signal,
|
|
945
|
+
now: opts.now,
|
|
946
|
+
controllerOf: opts.controllerOf ?? (() => resolveServices(ctx).sessionController)
|
|
947
|
+
};
|
|
948
|
+
const attempt = (index) => {
|
|
949
|
+
if (index >= delays.length) {
|
|
950
|
+
log(`startup self-check gave up after ${delays.length} attempt(s); the pointer stays unconfirmed, so the next host start will try again.`);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
try {
|
|
954
|
+
schedule(() => {
|
|
955
|
+
return runAutoResume({ ...baseDeps, attempt: index + 1 }).then((outcome) => {
|
|
956
|
+
if (outcome.status === "send-failed")
|
|
957
|
+
attempt(index + 1);
|
|
958
|
+
return outcome;
|
|
959
|
+
}).catch((error) => {
|
|
960
|
+
log(`startup self-check crashed (${reasonOf2(error)}); the host is unaffected.`);
|
|
961
|
+
attempt(index + 1);
|
|
962
|
+
});
|
|
963
|
+
}, delays[index]);
|
|
964
|
+
} catch (error) {
|
|
965
|
+
log(`startup self-check could not be scheduled (${reasonOf2(error)}); nothing will be sent.`);
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
attempt(0);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/index.ts
|
|
972
|
+
var name = "session-create";
|
|
973
|
+
var inject = ["tools"];
|
|
974
|
+
var output = {
|
|
975
|
+
schema: { type: "string" },
|
|
976
|
+
render: (_args, value) => [{ type: "text", text: String(value) }]
|
|
977
|
+
};
|
|
978
|
+
var CREATE_DESCRIPTION = [
|
|
979
|
+
"Start a new Session in a DSH workspace or an existing working folder you name, and deliver one prompt into it.",
|
|
980
|
+
"Give exactly one of `workspace` (a workspace id, or its exact folder path) or `folder` (any existing folder).",
|
|
981
|
+
"A `folder` Session joins no DSH workspace, so the user's project list stays as it is; a `workspace` Session shows up under that workspace.",
|
|
982
|
+
"Optionally set the Session's agent preset (`preset`) and permission mode (`permission`) before it starts; leave them out for the host default.",
|
|
983
|
+
"The session id is written to a dispatch ledger, so its result can be collected later with read_session_transcript.",
|
|
984
|
+
"Use it to open a work line in a project without creating a Bot. The Session belongs to the user, not to any Bot."
|
|
985
|
+
].join(" ") + `
|
|
986
|
+
` + [
|
|
987
|
+
"派会话前先自问三句:",
|
|
988
|
+
"① 要落盘还是回传?落盘就在 prompt 里写死输出路径,只回摘要+路径。",
|
|
989
|
+
"② 只有 Main Bot 能派吗?(值不值得做/范围多大/谁收口提交——三条不全就别派,交清单给 Main Bot)",
|
|
990
|
+
"③ 派出去就管到底:prompt 写清终点(改完→测过→本地 commit),不许写「先别提交」。"
|
|
991
|
+
].join(`
|
|
992
|
+
`);
|
|
993
|
+
var MESSAGE_DESCRIPTION = [
|
|
994
|
+
"Send one more message into a Session that already exists, by its session id. No new Session is created.",
|
|
995
|
+
"This is how a dispatch that is already running gets corrected or added to, when handing it a file is not enough.",
|
|
996
|
+
"The message is queued: it does not interrupt the turn that is running, it is picked up after it (or when the Session starts one).",
|
|
997
|
+
"A session id this harness does not know is refused and nothing is sent; the call reports what it actually did.",
|
|
998
|
+
"Read what the Session did with it afterwards using read_session_transcript."
|
|
999
|
+
].join(" ");
|
|
1000
|
+
var READ_DESCRIPTION = [
|
|
1001
|
+
"Read back what a Session has said and where it stands, by session id: its latest messages (oldest first) and whether its current turn has already ended.",
|
|
1002
|
+
"This is how a dispatched Session's result is collected without opening it by hand. Omit `sessionId` to read the most recent dispatch from the ledger.",
|
|
1003
|
+
"`last` sets how many messages to bring back (1..200, default 40); older ones remain readable with a bigger value."
|
|
1004
|
+
].join(" ");
|
|
1005
|
+
var LIST_DESCRIPTION = [
|
|
1006
|
+
"List the Sessions this plugin dispatched, newest first, with each one's state right now: still working, its last turn finished, or a read error.",
|
|
1007
|
+
"Use it to see which dispatches are ready to collect; read_session_transcript then gives the content."
|
|
1008
|
+
].join(" ");
|
|
1009
|
+
function statusLine(transcript) {
|
|
1010
|
+
const how = transcript.statusFrom === "host" ? "the harness reports this Session as" : "";
|
|
1011
|
+
if (transcript.statusFrom === "host") {
|
|
1012
|
+
return transcript.status === "running" ? `still working (${how} running)` : `finished (${how} idle)`;
|
|
1013
|
+
}
|
|
1014
|
+
if (transcript.status === "running")
|
|
1015
|
+
return `still working (turn ${transcript.turn} has not ended, read from the Session's own events)`;
|
|
1016
|
+
if (transcript.status === "idle")
|
|
1017
|
+
return `finished (turn ${transcript.turn} ended, read from the Session's own events)`;
|
|
1018
|
+
return "the prompt has not started a turn yet";
|
|
1019
|
+
}
|
|
1020
|
+
function formatTranscript(transcript) {
|
|
1021
|
+
const head = [
|
|
1022
|
+
`Session ${transcript.sessionId}${transcript.title === undefined ? "" : ` · "${transcript.title}"`} — ${statusLine(transcript)}`,
|
|
1023
|
+
transcript.placement === undefined ? "" : `place: ${transcript.placement}${transcript.dispatchedAt === undefined ? "" : ` · dispatched ${stamp(transcript.dispatchedAt)}`}`,
|
|
1024
|
+
`head seq ${transcript.headSeq}${transcript.newEvents === undefined ? "" : ` (${transcript.newEvents} new event(s) since the last read)`}`
|
|
1025
|
+
].filter((line) => line !== "");
|
|
1026
|
+
const body = transcript.messages.map((message) => `[${message.role} · ${stamp(message.time)}] ${message.text}`);
|
|
1027
|
+
const tail = [];
|
|
1028
|
+
if (body.length === 0)
|
|
1029
|
+
tail.push("(no user or assistant message in the range that was read)");
|
|
1030
|
+
if (transcript.hasMore)
|
|
1031
|
+
tail.push("(older events exist; call again with a bigger `last` to see more)");
|
|
1032
|
+
if (transcript.ledgerError !== undefined)
|
|
1033
|
+
tail.push(`(the dispatch ledger could not be used: ${transcript.ledgerError})`);
|
|
1034
|
+
return [...head, ...body.length === 0 ? [] : body, ...tail].join(`
|
|
1035
|
+
`);
|
|
1036
|
+
}
|
|
1037
|
+
function formatMessage(result) {
|
|
1038
|
+
const head = `Queued a message into Session ${result.sessionId}${result.title === undefined ? "" : ` · "${result.title}"`}.`;
|
|
1039
|
+
const place = result.placement === undefined ? "" : `place: ${result.placement}`;
|
|
1040
|
+
const ledger = result.ledgerError === undefined ? "" : `(the dispatch ledger could not be read, so this Session was not matched to a dispatch: ${result.ledgerError})`;
|
|
1041
|
+
return [
|
|
1042
|
+
head,
|
|
1043
|
+
...place === "" ? [] : [place],
|
|
1044
|
+
`The host took it into the Session's queue (mode "queue"): it does not interrupt the turn that is running, and the Session picks it up after it. Nothing yet proves the Session has read it — read_session_transcript shows that.`,
|
|
1045
|
+
...ledger === "" ? [] : [ledger]
|
|
1046
|
+
].join(`
|
|
1047
|
+
`);
|
|
1048
|
+
}
|
|
1049
|
+
function apply(ctx) {
|
|
1050
|
+
const services = resolveServices(ctx);
|
|
1051
|
+
const seen = preflight(services);
|
|
1052
|
+
const seenRead = preflightRead(services);
|
|
1053
|
+
const seenMessage = preflightMessage(services);
|
|
1054
|
+
const seenOptions = preflightOptions(services);
|
|
1055
|
+
console.log(seen.length === 0 ? "[dsbot-session-create] loaded; the host services create_session needs were all present at load." : `[dsbot-session-create] loaded; at load MISSING: ${seen.join(", ")} (the tool re-checks on every call)`);
|
|
1056
|
+
console.log(seenRead.length === 0 ? "[dsbot-session-create] loaded; the host services the read-back tools need were all present at load." : `[dsbot-session-create] loaded; at load MISSING for read-back: ${seenRead.join(", ")} (the tools re-check on every call)`);
|
|
1057
|
+
console.log(seenMessage.length === 0 ? "[dsbot-session-create] loaded; the host services session_message needs were all present at load." : `[dsbot-session-create] loaded; at load MISSING for session_message: ${seenMessage.join(", ")} (the tool re-checks on every call)`);
|
|
1058
|
+
console.log(seenOptions.length === 0 ? "[dsbot-session-create] loaded; the host services the session options need were all present at load." : `[dsbot-session-create] loaded; at load MISSING for session options: ${seenOptions.join(", ")} (create_session re-checks on every call)`);
|
|
1059
|
+
const register = services.tools?.register?.bind(services.tools);
|
|
1060
|
+
register?.(defineTool({
|
|
1061
|
+
name: "create_session",
|
|
1062
|
+
description: CREATE_DESCRIPTION,
|
|
1063
|
+
parameters: {
|
|
1064
|
+
title: { type: "string", required: true, description: "Name of the new Session, as it shows in the user's session list." },
|
|
1065
|
+
prompt: { type: "string", required: true, description: "The message delivered into the new Session." },
|
|
1066
|
+
workspace: { type: "string", description: "A DSH workspace: its id, or its exact folder path. Give this or `folder`, not both." },
|
|
1067
|
+
folder: { type: "string", description: "Full path of a folder that exists. The Session works there and joins no DSH workspace. Give this or `workspace`, not both." },
|
|
1068
|
+
preset: { type: "string", description: "Agent preset for the new Session, fixed when it is created: `standard`, `minimal`, `ptc` (tools run as code), or `creator`. A name the host does not know is refused and the refusal lists the ones it has. Omit for the host default." },
|
|
1069
|
+
permission: { type: "string", description: "Permission mode for the new Session, set with the host's own `/permission` command before the prompt: `read-only`, `workspace-write`, or `danger-full-access`. Any other name is refused by the host, which lists the valid ones. Omit for the host default." }
|
|
1070
|
+
},
|
|
1071
|
+
output,
|
|
1072
|
+
async execute(args, exec) {
|
|
1073
|
+
try {
|
|
1074
|
+
const result = await createSessionIn(resolveServices(ctx), args, { dispatcherSessionId: dispatcherSessionOf(exec) });
|
|
1075
|
+
const renameNote = result.renamed ? "" : " (the host would not rename it, so it keeps its default title)";
|
|
1076
|
+
const ledgerNote = result.ledgerError === undefined ? `Recorded in the dispatch ledger (${result.ledgerPath}); read_session_transcript collects its result later.` : `WARNING: the Session is running, but recording it in the dispatch ledger failed (${result.ledgerError}) — keep this session id by hand.`;
|
|
1077
|
+
const options = [
|
|
1078
|
+
result.preset === undefined ? "" : `preset ${result.preset}`,
|
|
1079
|
+
result.permission === undefined ? "" : `permission ${result.permission}`
|
|
1080
|
+
].filter((part) => part !== "");
|
|
1081
|
+
return [
|
|
1082
|
+
`Started a new Session in ${result.placement}.`,
|
|
1083
|
+
`- title: ${result.title}${renameNote}`,
|
|
1084
|
+
`- session id: ${result.sessionId}`,
|
|
1085
|
+
...options.length === 0 ? [] : [`- options: ${options.join(" · ")} (permission is what was requested; the Session itself is the record)`],
|
|
1086
|
+
"The prompt is queued in the Session.",
|
|
1087
|
+
ledgerNote
|
|
1088
|
+
].join(`
|
|
1089
|
+
`);
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (error instanceof SessionCreateError)
|
|
1092
|
+
return `Could not start the Session: ${error.message}`;
|
|
1093
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1094
|
+
return `Could not start the Session: ${reason}`;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}));
|
|
1098
|
+
register?.(defineTool({
|
|
1099
|
+
name: "read_session_transcript",
|
|
1100
|
+
description: READ_DESCRIPTION,
|
|
1101
|
+
parameters: {
|
|
1102
|
+
sessionId: { type: "string", description: "The Session to read. Omit to read the most recent dispatch in the ledger." },
|
|
1103
|
+
last: { type: "number", description: "How many of the newest messages to bring back (1..200; default 40)." }
|
|
1104
|
+
},
|
|
1105
|
+
output,
|
|
1106
|
+
async execute(args) {
|
|
1107
|
+
try {
|
|
1108
|
+
return formatTranscript(await readSessionIn(resolveServices(ctx), args));
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
if (error instanceof SessionCreateError)
|
|
1111
|
+
return `Could not read the Session: ${error.message}`;
|
|
1112
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1113
|
+
return `Could not read the Session: ${reason}`;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}));
|
|
1117
|
+
register?.(defineTool({
|
|
1118
|
+
name: "list_dispatched_sessions",
|
|
1119
|
+
description: LIST_DESCRIPTION,
|
|
1120
|
+
parameters: {
|
|
1121
|
+
limit: { type: "number", description: "How many of the newest dispatches to show (1..20; default 5)." }
|
|
1122
|
+
},
|
|
1123
|
+
output,
|
|
1124
|
+
async execute(args) {
|
|
1125
|
+
const live = resolveServices(ctx);
|
|
1126
|
+
const missing = preflightRead(live);
|
|
1127
|
+
let ledger;
|
|
1128
|
+
try {
|
|
1129
|
+
ledger = await listDispatchedIn(live, args);
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1132
|
+
return `Could not read the dispatch ledger: ${reason}`;
|
|
1133
|
+
}
|
|
1134
|
+
if (ledger.lines.length === 0)
|
|
1135
|
+
return `No Session has been dispatched from this plugin yet (ledger: ${ledger.ledgerPath}).`;
|
|
1136
|
+
const header = missing.length === 0 ? `Dispatched Sessions, newest first (ledger: ${ledger.ledgerPath})` : `Dispatched Sessions, newest first (ledger: ${ledger.ledgerPath}; this harness cannot read them back — missing ${missing.join(", ")})`;
|
|
1137
|
+
const lines = ledger.lines.map((line, index) => {
|
|
1138
|
+
const head = `${index + 1}. ${line.record.sessionId} · "${line.record.title}" · ${line.record.placement}`;
|
|
1139
|
+
const when = `dispatched ${stamp(line.record.dispatchedAt)}`;
|
|
1140
|
+
if (line.error !== undefined)
|
|
1141
|
+
return `${head}
|
|
1142
|
+
- unreadable: ${line.error} (${when})`;
|
|
1143
|
+
const state = line.status === "running" ? "still working" : line.status === "idle" ? "its last turn finished" : "not started";
|
|
1144
|
+
const fresh = line.newEvents === undefined ? "" : ` · ${line.newEvents} new event(s) since the last read`;
|
|
1145
|
+
return `${head}
|
|
1146
|
+
- ${state} · head seq ${line.headSeq}${fresh} · ${when}`;
|
|
1147
|
+
});
|
|
1148
|
+
return [header, ...lines].join(`
|
|
1149
|
+
`);
|
|
1150
|
+
}
|
|
1151
|
+
}));
|
|
1152
|
+
register?.(defineTool({
|
|
1153
|
+
name: "session_message",
|
|
1154
|
+
description: MESSAGE_DESCRIPTION,
|
|
1155
|
+
parameters: {
|
|
1156
|
+
sessionId: { type: "string", required: true, description: "The Session to send into: an id this harness already knows. No new Session is made; an unknown id is refused." },
|
|
1157
|
+
message: { type: "string", required: true, description: "The message to deliver into that Session. It is queued, so it lands after the turn that is running." }
|
|
1158
|
+
},
|
|
1159
|
+
output,
|
|
1160
|
+
async execute(args) {
|
|
1161
|
+
try {
|
|
1162
|
+
return formatMessage(await messageSessionIn(resolveServices(ctx), args));
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
if (error instanceof SessionCreateError)
|
|
1165
|
+
return `Could not send into the Session: ${error.message}`;
|
|
1166
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1167
|
+
return `Could not send into the Session: ${reason}`;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}));
|
|
1171
|
+
installDispatchNotify(ctx);
|
|
1172
|
+
installAutoResume(ctx);
|
|
1173
|
+
}
|
|
1174
|
+
export {
|
|
1175
|
+
name,
|
|
1176
|
+
inject,
|
|
1177
|
+
apply
|
|
1178
|
+
};
|