@kingcrab/pi-imessage 0.0.43 → 1.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 +93 -127
- package/dist/agent/behavior.js +79 -0
- package/dist/agent/chats.js +88 -0
- package/dist/agent/commands.js +191 -0
- package/dist/agent/compaction.js +51 -0
- package/dist/agent/deliver.js +139 -0
- package/dist/agent/direct-send.js +69 -0
- package/dist/agent/harness.js +79 -0
- package/dist/agent/health.js +33 -0
- package/dist/agent/models.js +69 -0
- package/dist/agent/progress-format.js +87 -0
- package/dist/agent/progress.js +173 -0
- package/dist/agent/prompt.js +79 -0
- package/dist/agent/replies.js +10 -0
- package/dist/agent/run.js +160 -0
- package/dist/agent/scheduling.js +291 -0
- package/dist/agent/time.js +18 -0
- package/dist/cli.js +88 -191
- package/dist/config/settings.js +60 -0
- package/dist/extensions/browser.js +326 -0
- package/dist/extensions/final-text.js +23 -0
- package/dist/extensions/memory.js +109 -0
- package/dist/extensions/message.js +33 -0
- package/dist/extensions/read-image.js +72 -0
- package/dist/extensions/restart.js +100 -0
- package/dist/extensions/schedules.js +42 -0
- package/dist/extensions/subagent.js +54 -0
- package/dist/extensions/web.js +100 -0
- package/dist/extensions/workspace.js +98 -0
- package/dist/main.js +578 -88
- package/dist/transport/attachments.js +37 -0
- package/dist/transport/http.js +110 -0
- package/dist/{rich-text.js → transport/rich-text.js} +25 -13
- package/dist/{send.js → transport/send.js} +13 -13
- package/dist/transport/watch.js +124 -0
- package/dist/web/app.js +826 -0
- package/dist/web/history.js +87 -0
- package/dist/web/memory.js +81 -0
- package/dist/web/page.js +29 -0
- package/dist/web/schedules.js +63 -0
- package/dist/web/server.js +382 -0
- package/dist/web/style.css +577 -0
- package/dist/web/tasks.js +97 -0
- package/docs/adr/0001-durable-owns-state.md +11 -0
- package/docs/adr/0002-module-layering.md +11 -0
- package/docs/adr/0003-replies-at-most-once.md +11 -0
- package/docs/adr/0004-native-extensions.md +11 -0
- package/docs/adr/0005-history-migration.md +11 -0
- package/docs/adr/0006-jsonl-storage.md +11 -0
- package/docs/adr/0007-images-by-path.md +11 -0
- package/docs/adr/0008-compaction-and-run.md +11 -0
- package/docs/adr/0009-one-shot-cutover.md +11 -0
- package/docs/adr/0010-test-budget.md +11 -0
- package/docs/adr/0011-reconcile-unsent-answers.md +24 -0
- package/docs/adr/0012-run-in-owned-conversation.md +22 -0
- package/docs/adr/0013-thin-adapters-around-durable.md +18 -0
- package/docs/adr/0014-ephemeral-private-storage.md +13 -0
- package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +21 -0
- package/docs/adr/0016-process-http-transport.md +21 -0
- package/docs/adr/0017-browser-cli-owned-scope.md +22 -0
- package/docs/adr/0018-tasks-without-prompt-api.md +19 -0
- package/docs/adr/0019-remove-scheduled-services.md +41 -0
- package/docs/adr/0020-chat-task-progress.md +41 -0
- package/docs/adr/0021-readable-progress-without-link-previews.md +33 -0
- package/docs/adr/0022-progress-admission-and-settlement.md +35 -0
- package/docs/adr/0023-retain-progress-type-prefixes.md +18 -0
- package/docs/adr/0024-status-marker-before-type-prefix.md +13 -0
- package/docs/adr/0025-reusable-progress-and-command-preview.md +49 -0
- package/docs/adr/0026-inline-command-only-requests.md +16 -0
- package/docs/adr/0027-command-preview-width.md +10 -0
- package/docs/adr/0028-command-preview-120.md +8 -0
- package/docs/adr/0030-shared-web-read-cache.md +33 -0
- package/docs/adr/0031-quiet-compaction-progress.md +32 -0
- package/docs/adr/0032-native-background-schedules.md +58 -0
- package/docs/adr/0033-operator-scheduled-run.md +23 -0
- package/docs/adr/0034-machine-local-schedule-time.md +36 -0
- package/docs/adr/0035-external-business-task-extensions.md +56 -0
- package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +37 -0
- package/docs/adr/0037-restart-during-in-flight-work.md +30 -0
- package/docs/adr/0038-restart-any-time.md +25 -0
- package/docs/adr/0039-rerun-interrupted-tools.md +24 -0
- package/docs/adr/0040-chat-progress-notices.md +44 -0
- package/docs/adr/0041-restart-and-recovery.md +28 -0
- package/docs/adr/0042-background-schedules-and-workspace-extensions.md +44 -0
- package/docs/adr/0043-remove-legacy-migration.md +23 -0
- package/docs/adr/0044-message-channels.md +32 -0
- package/docs/adr/0045-chat-scheduling-tools.md +51 -0
- package/docs/adr/0046-selective-command-redaction.md +31 -0
- package/docs/adr/0047-one-scheduling-path.md +158 -0
- package/docs/adr/0048-quiet-attachment-reads.md +11 -0
- package/docs/adr/0049-per-chat-progress-notices.md +21 -0
- package/docs/adr/0052-host-owned-self-restart.md +53 -0
- package/docs/adr/README.md +54 -0
- package/docs/api.md +104 -0
- package/docs/architecture.md +164 -0
- package/docs/browser.md +93 -0
- package/docs/memory.md +27 -0
- package/docs/screenshot.png +0 -0
- package/docs/workspace-extensions.md +133 -0
- package/examples/workspace-extensions/system-context/index.ts +69 -0
- package/examples/workspace-extensions/system-context/package.json +3 -0
- package/ops/README.md +250 -0
- package/ops/preflight.mjs +28 -0
- package/package.json +36 -37
- package/dist/agent.d.ts +0 -84
- package/dist/agent.d.ts.map +0 -1
- package/dist/agent.js +0 -780
- package/dist/agent.js.map +0 -1
- package/dist/cli.d.ts +0 -15
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/imessage.d.ts +0 -33
- package/dist/imessage.d.ts.map +0 -1
- package/dist/imessage.js +0 -76
- package/dist/imessage.js.map +0 -1
- package/dist/logger.d.ts +0 -33
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -104
- package/dist/logger.js.map +0 -1
- package/dist/main.d.ts +0 -5
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/memory.d.ts +0 -53
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js +0 -196
- package/dist/memory.js.map +0 -1
- package/dist/model-health.d.ts +0 -11
- package/dist/model-health.d.ts.map +0 -1
- package/dist/model-health.js +0 -69
- package/dist/model-health.js.map +0 -1
- package/dist/pipeline.d.ts +0 -27
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/pipeline.js +0 -59
- package/dist/pipeline.js.map +0 -1
- package/dist/queue.d.ts +0 -23
- package/dist/queue.d.ts.map +0 -1
- package/dist/queue.js +0 -89
- package/dist/queue.js.map +0 -1
- package/dist/reminders.d.ts +0 -42
- package/dist/reminders.d.ts.map +0 -1
- package/dist/reminders.js +0 -207
- package/dist/reminders.js.map +0 -1
- package/dist/rich-text.d.ts +0 -26
- package/dist/rich-text.d.ts.map +0 -1
- package/dist/rich-text.js.map +0 -1
- package/dist/self-echo.d.ts +0 -19
- package/dist/self-echo.d.ts.map +0 -1
- package/dist/self-echo.js +0 -54
- package/dist/self-echo.js.map +0 -1
- package/dist/send.d.ts +0 -24
- package/dist/send.d.ts.map +0 -1
- package/dist/send.js.map +0 -1
- package/dist/settings.d.ts +0 -44
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -80
- package/dist/settings.js.map +0 -1
- package/dist/store.d.ts +0 -57
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -124
- package/dist/store.js.map +0 -1
- package/dist/tasks.d.ts +0 -95
- package/dist/tasks.d.ts.map +0 -1
- package/dist/tasks.js +0 -384
- package/dist/tasks.js.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -38
- package/dist/types.js.map +0 -1
- package/dist/watch.d.ts +0 -21
- package/dist/watch.d.ts.map +0 -1
- package/dist/watch.js +0 -219
- package/dist/watch.js.map +0 -1
- package/dist/web/data.d.ts +0 -11
- package/dist/web/data.d.ts.map +0 -1
- package/dist/web/data.js +0 -56
- package/dist/web/data.js.map +0 -1
- package/dist/web/html.d.ts +0 -5
- package/dist/web/html.d.ts.map +0 -1
- package/dist/web/html.js +0 -16
- package/dist/web/html.js.map +0 -1
- package/dist/web/index.d.ts +0 -25
- package/dist/web/index.d.ts.map +0 -1
- package/dist/web/index.js +0 -335
- package/dist/web/index.js.map +0 -1
- package/dist/web/render.d.ts +0 -11
- package/dist/web/render.d.ts.map +0 -1
- package/dist/web/render.js +0 -56
- package/dist/web/render.js.map +0 -1
- package/dist/web/templates/logs.eta +0 -75
- package/dist/web/templates/memory.eta +0 -91
- package/dist/web/templates/page.eta +0 -98
package/dist/main.js
CHANGED
|
@@ -1,94 +1,584 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
|
+
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { createRegistry } from "@earendil-works/pi-durable";
|
|
4
|
+
import { CodingTools } from "@earendil-works/pi-durable/tools";
|
|
5
|
+
import { Chats, chatConversation, submitMessage, WatchCursor, } from "./agent/chats.js";
|
|
6
|
+
import { compactionReply, isCommand, runCommand } from "./agent/commands.js";
|
|
7
|
+
import { compactChats } from "./agent/compaction.js";
|
|
8
|
+
import { deliverReplies, recoverSending } from "./agent/deliver.js";
|
|
9
|
+
import { deliverDirect } from "./agent/direct-send.js";
|
|
10
|
+
import { openHarness } from "./agent/harness.js";
|
|
11
|
+
import { submitHealth } from "./agent/health.js";
|
|
12
|
+
import { openModels, readDefaults, requestSettings, withModelPolicy } from "./agent/models.js";
|
|
13
|
+
import { startProgress } from "./agent/progress.js";
|
|
14
|
+
import { loadPrompt } from "./agent/prompt.js";
|
|
15
|
+
import { Runs, runRecord } from "./agent/run.js";
|
|
16
|
+
import { deliverScheduled, EXECUTION_KIND, Schedules, schedulingExtension } from "./agent/scheduling.js";
|
|
17
|
+
import { isReplyEnabled, readSettings } from "./config/settings.js";
|
|
18
|
+
import { createBrowser } from "./extensions/browser.js";
|
|
19
|
+
import { memoryExtension } from "./extensions/memory.js";
|
|
20
|
+
import { messageExtension } from "./extensions/message.js";
|
|
21
|
+
import { ImageRead } from "./extensions/read-image.js";
|
|
22
|
+
import { launchInTerminal, restartExtension } from "./extensions/restart.js";
|
|
23
|
+
import { validateSchedules } from "./extensions/schedules.js";
|
|
24
|
+
import { Subagent } from "./extensions/subagent.js";
|
|
25
|
+
import { webExtension } from "./extensions/web.js";
|
|
26
|
+
import { loadWorkspaceExtensions, workspaceExtension } from "./extensions/workspace.js";
|
|
27
|
+
import { archiveAttachments } from "./transport/attachments.js";
|
|
28
|
+
import { openHttpTransport } from "./transport/http.js";
|
|
29
|
+
import { createWatcher } from "./transport/watch.js";
|
|
30
|
+
import { startWeb } from "./web/server.js";
|
|
31
|
+
/** Explicit startup; pass an isolated runtime in tests instead of opening installed auth. */
|
|
32
|
+
export async function startService(options,
|
|
33
|
+
/** The application owns process HTTP resources; standalone callers retain their transport. */
|
|
34
|
+
reloadHttpTransport) {
|
|
35
|
+
let runtime = options.runtime;
|
|
36
|
+
let modelPolicy = (await readSettings(options.workingDir)).modelPolicy;
|
|
37
|
+
// Installed auth and models; an isolated test runtime has nothing to reload.
|
|
38
|
+
let installed;
|
|
39
|
+
if (!runtime) {
|
|
40
|
+
installed = await openModels(options.agentDir);
|
|
41
|
+
runtime = {
|
|
42
|
+
models: installed,
|
|
43
|
+
defaults: await readDefaults(installed, options.workingDir, options.agentDir, modelPolicy),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const defaults = { ...runtime.defaults, model: { ...runtime.defaults.model } };
|
|
47
|
+
const browser = await createBrowser(options.workingDir, options.agentDir);
|
|
48
|
+
if (!runtime.models.getModel(defaults.model.provider, defaults.model.modelId))
|
|
49
|
+
throw new Error(`Model is unavailable: ${defaults.model.provider}/${defaults.model.modelId}`);
|
|
50
|
+
async function loadExtensions() {
|
|
51
|
+
const extensions = [
|
|
52
|
+
await loadPrompt(options.workingDir, options.agentDir),
|
|
53
|
+
...(!options.extensions
|
|
54
|
+
? [
|
|
55
|
+
messageExtension({
|
|
56
|
+
async owner(id) {
|
|
57
|
+
const chats = (await harness.snapshot(Chats, BACKGROUND_CONTEXT))?.items ?? [];
|
|
58
|
+
const runs = (await harness.snapshot(Runs, BACKGROUND_CONTEXT))?.items ?? [];
|
|
59
|
+
const chatGuid = [...chats, ...runs].find((item) => item.conversationId === id)?.chatGuid;
|
|
60
|
+
if (!chatGuid)
|
|
61
|
+
throw new Error("No destination chat for send_message");
|
|
62
|
+
return chatGuid;
|
|
63
|
+
},
|
|
64
|
+
send: (input) => service.sendDirect(input),
|
|
65
|
+
}),
|
|
66
|
+
]
|
|
67
|
+
: []),
|
|
68
|
+
// ImageRead replaces CodingTools.read, while text reads still use the native tool.
|
|
69
|
+
...(options.extensions
|
|
70
|
+
? await options.extensions()
|
|
71
|
+
: [
|
|
72
|
+
CodingTools,
|
|
73
|
+
ImageRead,
|
|
74
|
+
await memoryExtension(options.workingDir),
|
|
75
|
+
webExtension(),
|
|
76
|
+
browser.extension,
|
|
77
|
+
Subagent,
|
|
78
|
+
workspaceExtension(reload),
|
|
79
|
+
// ADR 0052: the service runs from a release directory; the controller starts after the call settles.
|
|
80
|
+
restartExtension({
|
|
81
|
+
workingDir: options.workingDir,
|
|
82
|
+
currentRelease: process.cwd(),
|
|
83
|
+
deployRoot: "/tmp",
|
|
84
|
+
settled: (taskId) => harness.waitForTask(taskId, BACKGROUND_CONTEXT),
|
|
85
|
+
launch: launchInTerminal,
|
|
86
|
+
}),
|
|
87
|
+
]),
|
|
88
|
+
...(await loadWorkspaceExtensions(options.workingDir)),
|
|
89
|
+
];
|
|
90
|
+
validateSchedules(extensions);
|
|
91
|
+
const names = new Set();
|
|
92
|
+
for (const extension of extensions) {
|
|
93
|
+
if (names.has(extension.name))
|
|
94
|
+
throw new Error(`Duplicate extension name: ${extension.name}`);
|
|
95
|
+
names.add(extension.name);
|
|
96
|
+
}
|
|
97
|
+
// ADR 0039: an interrupted tool reruns on recovery unless it declares `unsafe`.
|
|
98
|
+
return extensions.map((extension) => extension.tools
|
|
99
|
+
? { ...extension, tools: extension.tools.map((tool) => ({ ...tool, replay: tool.replay ?? "safe" })) }
|
|
100
|
+
: extension);
|
|
101
|
+
}
|
|
102
|
+
const settings = SettingsManager.create(options.workingDir, options.agentDir);
|
|
103
|
+
let activeExtensions = await loadExtensions();
|
|
104
|
+
const owner = await openHarness(options.workingDir, withModelPolicy(runtime.models, () => modelPolicy), activeExtensions, requestSettings(settings));
|
|
105
|
+
const { harness, storage } = owner;
|
|
106
|
+
const scheduling = schedulingExtension(harness, defaults);
|
|
107
|
+
let progress;
|
|
108
|
+
try {
|
|
109
|
+
// The native owner lock is acquired before cleaning up browsers left by a previous process.
|
|
110
|
+
await browser.close();
|
|
111
|
+
if (owner.registry.snapshot().extension(scheduling.extension.name))
|
|
112
|
+
throw new Error(`Duplicate extension name: ${scheduling.extension.name}`);
|
|
113
|
+
owner.registry.install(scheduling.extension);
|
|
114
|
+
for (const { record } of (await harness.inspect(BACKGROUND_CONTEXT)).tasks) {
|
|
115
|
+
const task = owner.registry.snapshot().task(record.kind);
|
|
116
|
+
if (!task ||
|
|
117
|
+
task.definition.version < record.version ||
|
|
118
|
+
(task.definition.version > record.version && !task.definition.migrate))
|
|
119
|
+
throw new Error(`Missing definition or migration for unfinished task: ${record.kind}`);
|
|
120
|
+
}
|
|
121
|
+
// Refuse missing business code needed by an admitted occurrence, not disabled sleeping schedules.
|
|
122
|
+
const savedSchedules = await harness.snapshot(Schedules, BACKGROUND_CONTEXT);
|
|
123
|
+
for (const { record } of (await harness.inspect(BACKGROUND_CONTEXT)).tasks) {
|
|
124
|
+
if (record.kind !== EXECUTION_KIND)
|
|
125
|
+
continue;
|
|
126
|
+
const input = record.input;
|
|
127
|
+
const jobId = input && typeof input === "object" && !Array.isArray(input) ? input.jobId : undefined;
|
|
128
|
+
const saved = savedSchedules?.items.find((schedule) => schedule.id === jobId);
|
|
129
|
+
if (saved &&
|
|
130
|
+
saved.id !== "compact-chats" &&
|
|
131
|
+
!activeExtensions.some((extension) => extension.schedules?.some((schedule) => schedule.id === saved.id)))
|
|
132
|
+
throw new Error(`Missing extension for unfinished schedule: ${saved.id}`);
|
|
133
|
+
}
|
|
134
|
+
await scheduling.initialize(activeExtensions);
|
|
135
|
+
await recoverSending(harness);
|
|
136
|
+
progress = await startProgress(harness, options.workingDir, {
|
|
137
|
+
sendMessage: options.send,
|
|
138
|
+
sendAttachment: options.sendAttachment,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
await owner.close(browser.close);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
let reloading = Promise.resolve();
|
|
146
|
+
function reload() {
|
|
147
|
+
const operation = reloading.then(reloadResources);
|
|
148
|
+
reloading = operation.then(() => undefined, () => undefined);
|
|
149
|
+
return operation;
|
|
150
|
+
}
|
|
151
|
+
/** Running calls keep the code they started with; later requests and new chats use the reloaded state. */
|
|
152
|
+
async function reloadResources() {
|
|
153
|
+
const nextPolicy = (await readSettings(options.workingDir)).modelPolicy;
|
|
154
|
+
const nextDefaults = installed
|
|
155
|
+
? await readDefaults(installed, options.workingDir, options.agentDir, nextPolicy)
|
|
156
|
+
: defaults;
|
|
157
|
+
const next = await loadExtensions();
|
|
158
|
+
const activeNames = new Set(activeExtensions.map((extension) => extension.name));
|
|
159
|
+
const unfinishedTasks = (await harness.inspect(BACKGROUND_CONTEXT)).tasks;
|
|
160
|
+
// Retain removed task code conservatively: an already running call can still admit children.
|
|
161
|
+
const retained = activeExtensions.flatMap((extension) => {
|
|
162
|
+
const replacement = next.find((item) => item.name === extension.name);
|
|
163
|
+
const tasks = extension.tasks?.filter((task) => !replacement?.tasks?.some((current) => current.definition.name === task.definition.name));
|
|
164
|
+
if (!tasks?.length)
|
|
165
|
+
return [];
|
|
166
|
+
if (replacement) {
|
|
167
|
+
next[next.indexOf(replacement)] = { ...replacement, tasks: [...(replacement.tasks ?? []), ...tasks] };
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
return [{ name: extension.name, tasks }];
|
|
171
|
+
});
|
|
172
|
+
const candidate = createRegistry();
|
|
173
|
+
const names = new Set();
|
|
174
|
+
for (const extension of [
|
|
175
|
+
...owner.registry
|
|
176
|
+
.snapshot()
|
|
177
|
+
.installed()
|
|
178
|
+
.filter((extension) => !activeNames.has(extension.name)),
|
|
179
|
+
...retained,
|
|
180
|
+
...next,
|
|
181
|
+
]) {
|
|
182
|
+
if (names.has(extension.name))
|
|
183
|
+
throw new Error(`Duplicate extension name: ${extension.name}`);
|
|
184
|
+
names.add(extension.name);
|
|
185
|
+
candidate.install(extension);
|
|
186
|
+
}
|
|
187
|
+
// Old running tools may admit tasks after inspection; changed versions still need migrations.
|
|
188
|
+
for (const previous of owner.registry.snapshot().tasks()) {
|
|
189
|
+
const task = candidate.snapshot().task(previous.definition.name);
|
|
190
|
+
if (task &&
|
|
191
|
+
(task.definition.version < previous.definition.version ||
|
|
192
|
+
(task.definition.version > previous.definition.version && !task.definition.migrate)))
|
|
193
|
+
throw new Error(`Missing migration for unfinished task or late child: ${previous.definition.name}`);
|
|
194
|
+
}
|
|
195
|
+
for (const { record } of unfinishedTasks) {
|
|
196
|
+
const task = candidate.snapshot().task(record.kind);
|
|
197
|
+
if (!task ||
|
|
198
|
+
task.definition.version < record.version ||
|
|
199
|
+
(task.definition.version > record.version && !task.definition.migrate))
|
|
200
|
+
throw new Error(`Missing definition or migration for unfinished task: ${record.kind}`);
|
|
201
|
+
}
|
|
202
|
+
// A failed business initializer rolls back all schedule/configuration changes before publication.
|
|
203
|
+
await reloadHttpTransport?.();
|
|
204
|
+
await settings.reload();
|
|
205
|
+
await scheduling.initialize(next, undefined, () => {
|
|
206
|
+
Object.assign(defaults, nextDefaults);
|
|
207
|
+
modelPolicy = nextPolicy;
|
|
208
|
+
// Publish before newly due triggers can admit business work.
|
|
209
|
+
for (const extension of activeExtensions)
|
|
210
|
+
owner.registry.uninstall(extension);
|
|
211
|
+
for (const extension of [...retained, ...next])
|
|
212
|
+
owner.registry.install(extension);
|
|
213
|
+
activeExtensions = [...retained, ...next];
|
|
214
|
+
});
|
|
215
|
+
console.log(new Date().toISOString(), "Agent reloaded", { model: defaults.model });
|
|
216
|
+
return { ...defaults, model: { ...defaults.model } };
|
|
217
|
+
}
|
|
218
|
+
let closed = false;
|
|
219
|
+
let closing;
|
|
220
|
+
const operations = new Set();
|
|
221
|
+
function track(run) {
|
|
222
|
+
if (closed)
|
|
223
|
+
return Promise.reject(new Error("Agent is closed"));
|
|
224
|
+
const operation = run();
|
|
225
|
+
operations.add(operation);
|
|
226
|
+
void operation.finally(() => operations.delete(operation)).catch(() => { });
|
|
227
|
+
return operation;
|
|
228
|
+
}
|
|
229
|
+
const service = {
|
|
230
|
+
harness,
|
|
231
|
+
async health() {
|
|
232
|
+
const started = Date.now();
|
|
233
|
+
const submission = await track(() => submitHealth(harness, defaults));
|
|
234
|
+
const result = await submission.wait(BACKGROUND_CONTEXT);
|
|
235
|
+
if (result.status !== "done" || result.type !== "input")
|
|
236
|
+
throw new Error("Model health request failed");
|
|
237
|
+
const answer = (await harness.commit((tx) => tx.entry(result.answer), BACKGROUND_CONTEXT))?.model?.[0];
|
|
238
|
+
if (answer?.role !== "assistant")
|
|
239
|
+
throw new Error("Model health answer missing");
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
model: `${answer.provider}/${answer.model}`,
|
|
243
|
+
latencyMs: Date.now() - started,
|
|
244
|
+
checkedAt: new Date().toISOString(),
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
submit(input, { logDisabled = false } = {}) {
|
|
248
|
+
const snapshot = { ...input, attachments: input.attachments ? [...input.attachments] : undefined };
|
|
249
|
+
return track(async () => {
|
|
250
|
+
const settings = await readSettings(options.workingDir);
|
|
251
|
+
const enabled = isReplyEnabled(settings, snapshot.chatGuid);
|
|
252
|
+
if (!enabled && !logDisabled)
|
|
253
|
+
throw new Error("Chat is disabled");
|
|
254
|
+
if (enabled && isCommand(snapshot.text))
|
|
255
|
+
throw new Error("Slash commands go through command()");
|
|
256
|
+
return submitMessage(harness, defaults, snapshot, enabled);
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
/** Records the command once by its source GUID, then runs it; an interrupted command is not replayed. */
|
|
260
|
+
command(input) {
|
|
261
|
+
const snapshot = { ...input, attachments: input.attachments ? [...input.attachments] : undefined };
|
|
262
|
+
return track(async () => {
|
|
263
|
+
if (!isCommand(snapshot.text))
|
|
264
|
+
throw new Error("A slash command is required");
|
|
265
|
+
const settings = await readSettings(options.workingDir);
|
|
266
|
+
if (!isReplyEnabled(settings, snapshot.chatGuid)) {
|
|
267
|
+
await submitMessage(harness, defaults, snapshot, false);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const conversation = await chatConversation(harness, defaults, snapshot.chatGuid);
|
|
271
|
+
if (await harness.commit((tx) => tx.submissionByRequest(conversation.id, snapshot.guid), BACKGROUND_CONTEXT))
|
|
272
|
+
return;
|
|
273
|
+
await conversation.submit({
|
|
274
|
+
type: "write",
|
|
275
|
+
requestId: snapshot.guid,
|
|
276
|
+
entry: { kind: "imessage.command", data: { text: snapshot.text } },
|
|
277
|
+
}, BACKGROUND_CONTEXT);
|
|
278
|
+
const result = await runCommand({
|
|
279
|
+
harness,
|
|
280
|
+
models: runtime.models,
|
|
281
|
+
defaults,
|
|
282
|
+
reload,
|
|
283
|
+
workingDir: options.workingDir,
|
|
284
|
+
chatGuid: snapshot.chatGuid,
|
|
285
|
+
guid: snapshot.guid,
|
|
286
|
+
text: snapshot.text,
|
|
287
|
+
async closeBrowsers(chatId) {
|
|
288
|
+
const ids = new Set([
|
|
289
|
+
chatId,
|
|
290
|
+
...((await harness.snapshot(Runs, BACKGROUND_CONTEXT))?.items ?? [])
|
|
291
|
+
.filter((run) => run.chat === chatId)
|
|
292
|
+
.map((run) => run.conversationId),
|
|
293
|
+
]);
|
|
294
|
+
const conversations = await harness.commit(async (tx) => {
|
|
295
|
+
const records = [];
|
|
296
|
+
let cursor;
|
|
297
|
+
do {
|
|
298
|
+
const page = await tx.scanConversations({}, 100, cursor);
|
|
299
|
+
records.push(...page.items);
|
|
300
|
+
cursor = page.next;
|
|
301
|
+
} while (cursor);
|
|
302
|
+
return records;
|
|
303
|
+
}, BACKGROUND_CONTEXT);
|
|
304
|
+
let previous = 0;
|
|
305
|
+
do {
|
|
306
|
+
previous = ids.size;
|
|
307
|
+
for (const child of conversations)
|
|
308
|
+
if (child.owner && ids.has(child.owner.conversationId))
|
|
309
|
+
ids.add(child.id);
|
|
310
|
+
} while (ids.size !== previous);
|
|
311
|
+
await browser.close([...ids]);
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
const reply = async () => {
|
|
315
|
+
let text = result.reply;
|
|
316
|
+
if (result.wait !== undefined) {
|
|
317
|
+
const { outcome } = (await harness.waitForTask(result.wait, BACKGROUND_CONTEXT)).state;
|
|
318
|
+
const id = outcome.status === "completed" ? outcome.result.submissionId : undefined;
|
|
319
|
+
const placement = id === undefined
|
|
320
|
+
? undefined
|
|
321
|
+
: await (await harness.submission(id, BACKGROUND_CONTEXT))?.status(BACKGROUND_CONTEXT);
|
|
322
|
+
text = compactionReply(outcome, placement);
|
|
323
|
+
}
|
|
324
|
+
if (text)
|
|
325
|
+
await deliverDirect(harness, { chatGuid: snapshot.chatGuid, requestId: `command:${snapshot.guid}`, text }, { sendMessage: options.send, sendAttachment: options.sendAttachment });
|
|
326
|
+
};
|
|
327
|
+
// A long command (compaction) must not hold the watcher; its reply follows the task.
|
|
328
|
+
if (result.wait === undefined)
|
|
329
|
+
await reply();
|
|
330
|
+
else
|
|
331
|
+
void track(reply).catch((error) => console.warn("Command reply failed", snapshot.guid, error));
|
|
332
|
+
return result;
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
/**
|
|
336
|
+
* Immediate tool sends bypass reply allowlists. What was attempted is recorded once in the chat
|
|
337
|
+
* conversation, so a later reply there has its context.
|
|
338
|
+
*/
|
|
339
|
+
sendDirect(input) {
|
|
340
|
+
const snapshot = { ...input };
|
|
341
|
+
return track(async () => {
|
|
342
|
+
const receipt = await deliverDirect(harness, snapshot, {
|
|
343
|
+
sendMessage: options.send,
|
|
344
|
+
sendAttachment: options.sendAttachment,
|
|
345
|
+
});
|
|
346
|
+
const parts = [
|
|
347
|
+
receipt.textStatus === "sent" || receipt.textStatus === "unknown"
|
|
348
|
+
? `[sent message, ${receipt.textStatus}]\n${receipt.text}`
|
|
349
|
+
: undefined,
|
|
350
|
+
receipt.fileStatus === "sent" || receipt.fileStatus === "unknown"
|
|
351
|
+
? `[sent file, ${receipt.fileStatus}] ${receipt.filePath}`
|
|
352
|
+
: undefined,
|
|
353
|
+
].filter((part) => part !== undefined);
|
|
354
|
+
if (parts.length > 0)
|
|
355
|
+
await (await chatConversation(harness, defaults, receipt.chatGuid)).submit({
|
|
356
|
+
type: "write",
|
|
357
|
+
requestId: `direct:${receipt.requestId}`,
|
|
358
|
+
entry: runRecord(parts.join("\n"), Date.now()),
|
|
359
|
+
}, BACKGROUND_CONTEXT);
|
|
360
|
+
return receipt;
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
/** Admit quiet native compactions; observing their results must not hold shutdown open. */
|
|
364
|
+
compact() {
|
|
365
|
+
return track(async () => {
|
|
366
|
+
const tasks = await compactChats(harness);
|
|
367
|
+
for (const task of tasks)
|
|
368
|
+
void harness
|
|
369
|
+
.waitForTask(task, BACKGROUND_CONTEXT)
|
|
370
|
+
.then((settled) => {
|
|
371
|
+
if (settled.state.outcome.status !== "completed")
|
|
372
|
+
console.warn("Scheduled compaction ended", task, settled.state.outcome);
|
|
373
|
+
})
|
|
374
|
+
.catch((error) => {
|
|
375
|
+
if (!closed)
|
|
376
|
+
console.warn("Scheduled compaction observation failed", task, error);
|
|
377
|
+
});
|
|
378
|
+
return tasks;
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
// Explicit host boundary; no model loop, timer or transport runs on import.
|
|
382
|
+
deliver() {
|
|
383
|
+
return track(async () => {
|
|
384
|
+
const settings = await readSettings(options.workingDir);
|
|
385
|
+
await deliverReplies(harness, storage, async (chatGuid, text) => {
|
|
386
|
+
await progress.flush();
|
|
387
|
+
await options.send(chatGuid, text, settings.richText);
|
|
388
|
+
}, (chatGuid) => isReplyEnabled(settings, chatGuid));
|
|
389
|
+
await deliverScheduled(harness, (input) => service.sendDirect(input), (chatGuid) => isReplyEnabled(settings, chatGuid));
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
close() {
|
|
393
|
+
closed = true;
|
|
394
|
+
closing ??= (async () => {
|
|
395
|
+
await Promise.allSettled([...operations]);
|
|
396
|
+
await progress.close();
|
|
397
|
+
await owner.close(browser.close);
|
|
398
|
+
})();
|
|
399
|
+
return closing;
|
|
45
400
|
},
|
|
401
|
+
};
|
|
402
|
+
console.log(new Date().toISOString(), "Agent ready", { model: defaults.model });
|
|
403
|
+
return service;
|
|
404
|
+
}
|
|
405
|
+
/** One host poll loop for incoming rows and saved replies; Durable owns model execution. */
|
|
406
|
+
export async function startMessaging(options, reloadHttpTransport) {
|
|
407
|
+
const intervalMs = options.intervalMs ?? 2000;
|
|
408
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0)
|
|
409
|
+
throw new Error("Invalid messaging poll interval");
|
|
410
|
+
const service = await startService(options, reloadHttpTransport);
|
|
411
|
+
let watcher;
|
|
412
|
+
try {
|
|
413
|
+
const cursor = await service.harness.snapshot(WatchCursor, BACKGROUND_CONTEXT);
|
|
414
|
+
watcher = createWatcher({
|
|
415
|
+
dbPath: options.dbPath,
|
|
416
|
+
cursor: cursor?.rowid,
|
|
417
|
+
async accept(message) {
|
|
418
|
+
const attachments = await archiveAttachments(options.workingDir, message.chatGuid, message.attachments);
|
|
419
|
+
if (isCommand(message.text))
|
|
420
|
+
await service.command({ ...message, attachments });
|
|
421
|
+
else
|
|
422
|
+
await service.submit({ ...message, attachments }, { logDisabled: true });
|
|
423
|
+
},
|
|
424
|
+
saveCursor: (rowid) => service.harness.commit(async (tx) => {
|
|
425
|
+
(await tx.doc(WatchCursor)).rowid = rowid;
|
|
426
|
+
}, BACKGROUND_CONTEXT),
|
|
427
|
+
onError: options.onError,
|
|
428
|
+
});
|
|
429
|
+
try {
|
|
430
|
+
// Save even a fresh high-water mark, so downtime before the first message cannot create a gap.
|
|
431
|
+
await service.harness.commit(async (tx) => {
|
|
432
|
+
(await tx.doc(WatchCursor)).rowid = watcher.cursor;
|
|
433
|
+
}, BACKGROUND_CONTEXT);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
await watcher.stop();
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
await service.close();
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
let closed = false;
|
|
445
|
+
let pending;
|
|
446
|
+
let closing;
|
|
447
|
+
let timer;
|
|
448
|
+
let started = false;
|
|
449
|
+
function poll() {
|
|
450
|
+
if (closed)
|
|
451
|
+
return Promise.reject(new Error("Messaging is closed"));
|
|
452
|
+
pending ??= (async () => {
|
|
453
|
+
try {
|
|
454
|
+
await watcher.poll();
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
// An unreadable attachment must not strand another chat's completed reply.
|
|
458
|
+
if (!closed)
|
|
459
|
+
await service.deliver();
|
|
460
|
+
}
|
|
461
|
+
})().finally(() => {
|
|
462
|
+
pending = undefined;
|
|
463
|
+
});
|
|
464
|
+
return pending;
|
|
465
|
+
}
|
|
466
|
+
function tick() {
|
|
467
|
+
void poll()
|
|
468
|
+
.catch(options.onError)
|
|
469
|
+
.finally(() => {
|
|
470
|
+
if (!closed)
|
|
471
|
+
timer = setTimeout(tick, intervalMs);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function start() {
|
|
475
|
+
if (closed)
|
|
476
|
+
throw new Error("Messaging is closed");
|
|
477
|
+
if (started)
|
|
478
|
+
return;
|
|
479
|
+
// Resume once, after application initialization; Durable owns recovery.
|
|
480
|
+
service.harness.resume();
|
|
481
|
+
started = true;
|
|
482
|
+
timer = setTimeout(tick, 0);
|
|
483
|
+
console.log(new Date().toISOString(), "Messaging ready", {
|
|
484
|
+
cursor: watcher.cursor,
|
|
485
|
+
intervalMs,
|
|
486
|
+
scheduling: "durable-background-tasks",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
if (options.autostart !== false)
|
|
491
|
+
start();
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
await watcher.stop();
|
|
495
|
+
await service.close();
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
start,
|
|
500
|
+
harness: service.harness,
|
|
501
|
+
health: service.health,
|
|
502
|
+
sendDirect(input) {
|
|
503
|
+
if (closed)
|
|
504
|
+
return Promise.reject(new Error("Messaging is closed"));
|
|
505
|
+
return service.sendDirect(input);
|
|
506
|
+
},
|
|
507
|
+
poll,
|
|
508
|
+
close() {
|
|
509
|
+
closed = true;
|
|
510
|
+
clearTimeout(timer);
|
|
511
|
+
closing ??= (async () => {
|
|
512
|
+
await watcher.stop();
|
|
513
|
+
await pending?.catch(() => { });
|
|
514
|
+
await service.close();
|
|
515
|
+
})();
|
|
516
|
+
return closing;
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/** Production wiring is explicit; tests pass a faux runtime, temporary database and fake senders. */
|
|
521
|
+
export async function startApplication(options) {
|
|
522
|
+
console.log(new Date().toISOString(), "Application starting", {
|
|
523
|
+
pid: process.pid,
|
|
524
|
+
node: process.version,
|
|
525
|
+
workingDir: options.workingDir,
|
|
46
526
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
echoFilter,
|
|
56
|
-
agent,
|
|
57
|
-
checkModelHealth,
|
|
58
|
-
reminders,
|
|
527
|
+
// Initialize before model/auth refresh. Injected runtimes keep control of process transport.
|
|
528
|
+
const httpSettings = options.runtime
|
|
529
|
+
? undefined
|
|
530
|
+
: SettingsManager.create(options.workingDir, options.agentDir);
|
|
531
|
+
const transport = httpSettings
|
|
532
|
+
? openHttpTransport({
|
|
533
|
+
proxy: httpSettings.getGlobalSettings().httpProxy,
|
|
534
|
+
idleTimeoutMs: httpSettings.getHttpIdleTimeoutMs(),
|
|
59
535
|
})
|
|
60
|
-
:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (workerEnabled) {
|
|
64
|
-
watcher.start();
|
|
65
|
-
bot.start();
|
|
66
|
-
reminders.start();
|
|
67
|
-
}
|
|
68
|
-
if (web)
|
|
69
|
-
web.start();
|
|
70
|
-
let shuttingDown = false;
|
|
71
|
-
async function shutdown() {
|
|
72
|
-
if (shuttingDown)
|
|
536
|
+
: undefined;
|
|
537
|
+
async function reloadHttpTransport() {
|
|
538
|
+
if (!transport || !httpSettings)
|
|
73
539
|
return;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
540
|
+
await httpSettings.reload();
|
|
541
|
+
transport.reload({
|
|
542
|
+
proxy: httpSettings.getGlobalSettings().httpProxy,
|
|
543
|
+
idleTimeoutMs: httpSettings.getHttpIdleTimeoutMs(),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
let messaging;
|
|
547
|
+
let web;
|
|
548
|
+
let closing;
|
|
549
|
+
function close() {
|
|
550
|
+
closing ??= (async () => {
|
|
551
|
+
const results = await Promise.allSettled([web?.close(), messaging?.close()]);
|
|
552
|
+
// Messaging and Harness work have stopped before draining/releasing HTTP resources.
|
|
553
|
+
await transport?.close();
|
|
554
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
555
|
+
if (failure?.status === "rejected")
|
|
556
|
+
throw failure.reason;
|
|
557
|
+
console.log(new Date().toISOString(), "Application resources closed");
|
|
558
|
+
})();
|
|
559
|
+
return closing;
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
messaging = await startMessaging({ ...options, autostart: false }, reloadHttpTransport);
|
|
563
|
+
if (options.web !== false) {
|
|
564
|
+
web = await startWeb({
|
|
565
|
+
workingDir: options.workingDir,
|
|
566
|
+
agent: messaging,
|
|
567
|
+
...options.web,
|
|
568
|
+
});
|
|
569
|
+
console.log(new Date().toISOString(), "Web server listening", {
|
|
570
|
+
host: web.address.address,
|
|
571
|
+
port: web.address.port,
|
|
572
|
+
});
|
|
80
573
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.log(
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
574
|
+
else
|
|
575
|
+
console.log(new Date().toISOString(), "Web server disabled");
|
|
576
|
+
messaging.start();
|
|
577
|
+
console.log(new Date().toISOString(), "Application started");
|
|
578
|
+
return { messaging, web, close };
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
await close().catch(() => { });
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
89
584
|
}
|
|
90
|
-
main().catch((error) => {
|
|
91
|
-
console.error("[sid] Fatal:", error);
|
|
92
|
-
process.exit(1);
|
|
93
|
-
});
|
|
94
|
-
//# sourceMappingURL=main.js.map
|