@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.
Files changed (191) hide show
  1. package/README.md +93 -127
  2. package/dist/agent/behavior.js +79 -0
  3. package/dist/agent/chats.js +88 -0
  4. package/dist/agent/commands.js +191 -0
  5. package/dist/agent/compaction.js +51 -0
  6. package/dist/agent/deliver.js +139 -0
  7. package/dist/agent/direct-send.js +69 -0
  8. package/dist/agent/harness.js +79 -0
  9. package/dist/agent/health.js +33 -0
  10. package/dist/agent/models.js +69 -0
  11. package/dist/agent/progress-format.js +87 -0
  12. package/dist/agent/progress.js +173 -0
  13. package/dist/agent/prompt.js +79 -0
  14. package/dist/agent/replies.js +10 -0
  15. package/dist/agent/run.js +160 -0
  16. package/dist/agent/scheduling.js +291 -0
  17. package/dist/agent/time.js +18 -0
  18. package/dist/cli.js +88 -191
  19. package/dist/config/settings.js +60 -0
  20. package/dist/extensions/browser.js +326 -0
  21. package/dist/extensions/final-text.js +23 -0
  22. package/dist/extensions/memory.js +109 -0
  23. package/dist/extensions/message.js +33 -0
  24. package/dist/extensions/read-image.js +72 -0
  25. package/dist/extensions/restart.js +100 -0
  26. package/dist/extensions/schedules.js +42 -0
  27. package/dist/extensions/subagent.js +54 -0
  28. package/dist/extensions/web.js +100 -0
  29. package/dist/extensions/workspace.js +98 -0
  30. package/dist/main.js +578 -88
  31. package/dist/transport/attachments.js +37 -0
  32. package/dist/transport/http.js +110 -0
  33. package/dist/{rich-text.js → transport/rich-text.js} +25 -13
  34. package/dist/{send.js → transport/send.js} +13 -13
  35. package/dist/transport/watch.js +124 -0
  36. package/dist/web/app.js +826 -0
  37. package/dist/web/history.js +87 -0
  38. package/dist/web/memory.js +81 -0
  39. package/dist/web/page.js +29 -0
  40. package/dist/web/schedules.js +63 -0
  41. package/dist/web/server.js +382 -0
  42. package/dist/web/style.css +577 -0
  43. package/dist/web/tasks.js +97 -0
  44. package/docs/adr/0001-durable-owns-state.md +11 -0
  45. package/docs/adr/0002-module-layering.md +11 -0
  46. package/docs/adr/0003-replies-at-most-once.md +11 -0
  47. package/docs/adr/0004-native-extensions.md +11 -0
  48. package/docs/adr/0005-history-migration.md +11 -0
  49. package/docs/adr/0006-jsonl-storage.md +11 -0
  50. package/docs/adr/0007-images-by-path.md +11 -0
  51. package/docs/adr/0008-compaction-and-run.md +11 -0
  52. package/docs/adr/0009-one-shot-cutover.md +11 -0
  53. package/docs/adr/0010-test-budget.md +11 -0
  54. package/docs/adr/0011-reconcile-unsent-answers.md +24 -0
  55. package/docs/adr/0012-run-in-owned-conversation.md +22 -0
  56. package/docs/adr/0013-thin-adapters-around-durable.md +18 -0
  57. package/docs/adr/0014-ephemeral-private-storage.md +13 -0
  58. package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +21 -0
  59. package/docs/adr/0016-process-http-transport.md +21 -0
  60. package/docs/adr/0017-browser-cli-owned-scope.md +22 -0
  61. package/docs/adr/0018-tasks-without-prompt-api.md +19 -0
  62. package/docs/adr/0019-remove-scheduled-services.md +41 -0
  63. package/docs/adr/0020-chat-task-progress.md +41 -0
  64. package/docs/adr/0021-readable-progress-without-link-previews.md +33 -0
  65. package/docs/adr/0022-progress-admission-and-settlement.md +35 -0
  66. package/docs/adr/0023-retain-progress-type-prefixes.md +18 -0
  67. package/docs/adr/0024-status-marker-before-type-prefix.md +13 -0
  68. package/docs/adr/0025-reusable-progress-and-command-preview.md +49 -0
  69. package/docs/adr/0026-inline-command-only-requests.md +16 -0
  70. package/docs/adr/0027-command-preview-width.md +10 -0
  71. package/docs/adr/0028-command-preview-120.md +8 -0
  72. package/docs/adr/0030-shared-web-read-cache.md +33 -0
  73. package/docs/adr/0031-quiet-compaction-progress.md +32 -0
  74. package/docs/adr/0032-native-background-schedules.md +58 -0
  75. package/docs/adr/0033-operator-scheduled-run.md +23 -0
  76. package/docs/adr/0034-machine-local-schedule-time.md +36 -0
  77. package/docs/adr/0035-external-business-task-extensions.md +56 -0
  78. package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +37 -0
  79. package/docs/adr/0037-restart-during-in-flight-work.md +30 -0
  80. package/docs/adr/0038-restart-any-time.md +25 -0
  81. package/docs/adr/0039-rerun-interrupted-tools.md +24 -0
  82. package/docs/adr/0040-chat-progress-notices.md +44 -0
  83. package/docs/adr/0041-restart-and-recovery.md +28 -0
  84. package/docs/adr/0042-background-schedules-and-workspace-extensions.md +44 -0
  85. package/docs/adr/0043-remove-legacy-migration.md +23 -0
  86. package/docs/adr/0044-message-channels.md +32 -0
  87. package/docs/adr/0045-chat-scheduling-tools.md +51 -0
  88. package/docs/adr/0046-selective-command-redaction.md +31 -0
  89. package/docs/adr/0047-one-scheduling-path.md +158 -0
  90. package/docs/adr/0048-quiet-attachment-reads.md +11 -0
  91. package/docs/adr/0049-per-chat-progress-notices.md +21 -0
  92. package/docs/adr/0052-host-owned-self-restart.md +53 -0
  93. package/docs/adr/README.md +54 -0
  94. package/docs/api.md +104 -0
  95. package/docs/architecture.md +164 -0
  96. package/docs/browser.md +93 -0
  97. package/docs/memory.md +27 -0
  98. package/docs/screenshot.png +0 -0
  99. package/docs/workspace-extensions.md +133 -0
  100. package/examples/workspace-extensions/system-context/index.ts +69 -0
  101. package/examples/workspace-extensions/system-context/package.json +3 -0
  102. package/ops/README.md +250 -0
  103. package/ops/preflight.mjs +28 -0
  104. package/package.json +36 -37
  105. package/dist/agent.d.ts +0 -84
  106. package/dist/agent.d.ts.map +0 -1
  107. package/dist/agent.js +0 -780
  108. package/dist/agent.js.map +0 -1
  109. package/dist/cli.d.ts +0 -15
  110. package/dist/cli.d.ts.map +0 -1
  111. package/dist/cli.js.map +0 -1
  112. package/dist/imessage.d.ts +0 -33
  113. package/dist/imessage.d.ts.map +0 -1
  114. package/dist/imessage.js +0 -76
  115. package/dist/imessage.js.map +0 -1
  116. package/dist/logger.d.ts +0 -33
  117. package/dist/logger.d.ts.map +0 -1
  118. package/dist/logger.js +0 -104
  119. package/dist/logger.js.map +0 -1
  120. package/dist/main.d.ts +0 -5
  121. package/dist/main.d.ts.map +0 -1
  122. package/dist/main.js.map +0 -1
  123. package/dist/memory.d.ts +0 -53
  124. package/dist/memory.d.ts.map +0 -1
  125. package/dist/memory.js +0 -196
  126. package/dist/memory.js.map +0 -1
  127. package/dist/model-health.d.ts +0 -11
  128. package/dist/model-health.d.ts.map +0 -1
  129. package/dist/model-health.js +0 -69
  130. package/dist/model-health.js.map +0 -1
  131. package/dist/pipeline.d.ts +0 -27
  132. package/dist/pipeline.d.ts.map +0 -1
  133. package/dist/pipeline.js +0 -59
  134. package/dist/pipeline.js.map +0 -1
  135. package/dist/queue.d.ts +0 -23
  136. package/dist/queue.d.ts.map +0 -1
  137. package/dist/queue.js +0 -89
  138. package/dist/queue.js.map +0 -1
  139. package/dist/reminders.d.ts +0 -42
  140. package/dist/reminders.d.ts.map +0 -1
  141. package/dist/reminders.js +0 -207
  142. package/dist/reminders.js.map +0 -1
  143. package/dist/rich-text.d.ts +0 -26
  144. package/dist/rich-text.d.ts.map +0 -1
  145. package/dist/rich-text.js.map +0 -1
  146. package/dist/self-echo.d.ts +0 -19
  147. package/dist/self-echo.d.ts.map +0 -1
  148. package/dist/self-echo.js +0 -54
  149. package/dist/self-echo.js.map +0 -1
  150. package/dist/send.d.ts +0 -24
  151. package/dist/send.d.ts.map +0 -1
  152. package/dist/send.js.map +0 -1
  153. package/dist/settings.d.ts +0 -44
  154. package/dist/settings.d.ts.map +0 -1
  155. package/dist/settings.js +0 -80
  156. package/dist/settings.js.map +0 -1
  157. package/dist/store.d.ts +0 -57
  158. package/dist/store.d.ts.map +0 -1
  159. package/dist/store.js +0 -124
  160. package/dist/store.js.map +0 -1
  161. package/dist/tasks.d.ts +0 -95
  162. package/dist/tasks.d.ts.map +0 -1
  163. package/dist/tasks.js +0 -384
  164. package/dist/tasks.js.map +0 -1
  165. package/dist/types.d.ts +0 -93
  166. package/dist/types.d.ts.map +0 -1
  167. package/dist/types.js +0 -38
  168. package/dist/types.js.map +0 -1
  169. package/dist/watch.d.ts +0 -21
  170. package/dist/watch.d.ts.map +0 -1
  171. package/dist/watch.js +0 -219
  172. package/dist/watch.js.map +0 -1
  173. package/dist/web/data.d.ts +0 -11
  174. package/dist/web/data.d.ts.map +0 -1
  175. package/dist/web/data.js +0 -56
  176. package/dist/web/data.js.map +0 -1
  177. package/dist/web/html.d.ts +0 -5
  178. package/dist/web/html.d.ts.map +0 -1
  179. package/dist/web/html.js +0 -16
  180. package/dist/web/html.js.map +0 -1
  181. package/dist/web/index.d.ts +0 -25
  182. package/dist/web/index.d.ts.map +0 -1
  183. package/dist/web/index.js +0 -335
  184. package/dist/web/index.js.map +0 -1
  185. package/dist/web/render.d.ts +0 -11
  186. package/dist/web/render.d.ts.map +0 -1
  187. package/dist/web/render.js +0 -56
  188. package/dist/web/render.js.map +0 -1
  189. package/dist/web/templates/logs.eta +0 -75
  190. package/dist/web/templates/memory.eta +0 -91
  191. package/dist/web/templates/page.eta +0 -98
package/dist/main.js CHANGED
@@ -1,94 +1,584 @@
1
- /**
2
- * Sid — iMessage friend entry point.
3
- */
4
- import "dotenv/config";
5
- import { homedir } from "node:os";
6
- import { join } from "node:path";
7
- import { createAgentManager } from "./agent.js";
8
- import { createIMessageBot } from "./imessage.js";
9
- import { createAppLogger, createDigestLogger } from "./logger.js";
10
- import { createModelHealthChecker } from "./model-health.js";
11
- import { createAsyncQueue } from "./queue.js";
12
- import { createReminderService } from "./reminders.js";
13
- import { createSelfEchoFilter } from "./self-echo.js";
14
- import { checkEnvironment, createMessageSender } from "./send.js";
15
- import { readSettings, writeSettings } from "./settings.js";
16
- import { createChatStore } from "./store.js";
17
- import { createWatcher } from "./watch.js";
18
- import { createWebServer } from "./web/index.js";
19
- async function main() {
20
- const webEnabled = process.env.WEB_ENABLED !== "false";
21
- const workerEnabled = process.env.WORKER_ENABLED !== "false";
22
- const webHost = process.env.WEB_HOST || "localhost";
23
- const webPort = Number.parseInt(process.env.WEB_PORT || "7750", 10);
24
- const workingDir = process.env.WORKING_DIR || join(homedir(), ".pi", "imessage");
25
- // Loggers must be created before anything else so all console output is captured.
26
- const appLogger = createAppLogger(workingDir);
27
- const digestLogger = createDigestLogger(workingDir);
28
- // Pre-flight: ensure Messages.app is running and iMessage is active.
29
- await checkEnvironment();
30
- const sender = createMessageSender();
31
- const echoFilter = createSelfEchoFilter();
32
- const getSettings = () => readSettings(workingDir);
33
- const setSettings = (updated) => writeSettings(workingDir, updated);
34
- const agent = await createAgentManager({ workingDir });
35
- const checkModelHealth = createModelHealthChecker(workingDir);
36
- const store = createChatStore({ workingDir });
37
- const queue = createAsyncQueue(join(workingDir, "queue.json"));
38
- const watcher = createWatcher({ queue });
39
- const bot = createIMessageBot({ queue, agent, sender, echoFilter, store, getSettings, digestLogger });
40
- const reminders = createReminderService({
41
- workingDir,
42
- deliver: async (reminder) => {
43
- echoFilter.remember(reminder.chatGuid, reminder.text);
44
- await sender.sendMessage(reminder.chatGuid, reminder.text);
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
- const web = webEnabled
48
- ? createWebServer({
49
- workingDir,
50
- host: webHost,
51
- port: webPort,
52
- getSettings,
53
- setSettings,
54
- sender,
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
- : null;
61
- console.log(`[sid] workspace: ${workingDir}`);
62
- console.log(`[sid] worker: ${workerEnabled ? "active" : "shadow"}`);
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
- shuttingDown = true;
75
- console.log("[sid] Shutting down…");
76
- if (workerEnabled) {
77
- watcher.stop();
78
- bot.stop();
79
- await reminders.stop();
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
- await web?.stop();
82
- digestLogger.close();
83
- appLogger.close();
84
- console.log("[sid] Shutdown complete");
85
- process.exit(0);
86
- }
87
- process.on("SIGINT", () => shutdown());
88
- process.on("SIGTERM", () => shutdown());
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