@adecore/agents 0.0.1 → 0.17.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/README.md +33 -0
- package/dist/async.d.ts +2 -0
- package/dist/async.js +31 -0
- package/dist/chat/attachment-store.d.ts +10 -0
- package/dist/chat/attachment-store.js +93 -0
- package/dist/chat/backend.d.ts +214 -0
- package/dist/chat/backend.js +1 -0
- package/dist/chat/background-work.d.ts +8 -0
- package/dist/chat/background-work.js +33 -0
- package/dist/chat/bookmark-store.d.ts +25 -0
- package/dist/chat/bookmark-store.js +141 -0
- package/dist/chat/chat-core.d.ts +311 -0
- package/dist/chat/chat-core.js +1025 -0
- package/dist/chat/chat-log.d.ts +33 -0
- package/dist/chat/chat-log.js +163 -0
- package/dist/chat/chat-process.d.ts +46 -0
- package/dist/chat/chat-process.js +190 -0
- package/dist/chat/chat-session.d.ts +209 -0
- package/dist/chat/chat-session.js +1511 -0
- package/dist/chat/chat-store.d.ts +37 -0
- package/dist/chat/chat-store.js +202 -0
- package/dist/chat/chat-title.d.ts +10 -0
- package/dist/chat/chat-title.js +74 -0
- package/dist/chat/claude-backend.d.ts +35 -0
- package/dist/chat/claude-backend.js +214 -0
- package/dist/chat/claude-protocol.d.ts +41 -0
- package/dist/chat/claude-protocol.js +642 -0
- package/dist/chat/claude-title.d.ts +11 -0
- package/dist/chat/claude-title.js +97 -0
- package/dist/chat/claude-transcript.d.ts +28 -0
- package/dist/chat/claude-transcript.js +211 -0
- package/dist/chat/codex-backend.d.ts +51 -0
- package/dist/chat/codex-backend.js +364 -0
- package/dist/chat/codex-protocol.d.ts +56 -0
- package/dist/chat/codex-protocol.js +767 -0
- package/dist/chat/codex-thread.d.ts +32 -0
- package/dist/chat/codex-thread.js +131 -0
- package/dist/chat/codex-transport.d.ts +42 -0
- package/dist/chat/codex-transport.js +188 -0
- package/dist/chat/composer-preferences.d.ts +15 -0
- package/dist/chat/composer-preferences.js +46 -0
- package/dist/chat/context-breakdown.d.ts +2 -0
- package/dist/chat/context-breakdown.js +74 -0
- package/dist/chat/delta-coalescer.d.ts +12 -0
- package/dist/chat/delta-coalescer.js +55 -0
- package/dist/chat/errors.d.ts +4 -0
- package/dist/chat/errors.js +4 -0
- package/dist/chat/fake-claude.d.ts +3 -0
- package/dist/chat/fake-claude.js +982 -0
- package/dist/chat/fake-cli.d.ts +25 -0
- package/dist/chat/fake-cli.js +157 -0
- package/dist/chat/fake-codex.d.ts +10 -0
- package/dist/chat/fake-codex.js +502 -0
- package/dist/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
- package/dist/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
- package/dist/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
- package/dist/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
- package/dist/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
- package/dist/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
- package/dist/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
- package/dist/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
- package/dist/chat/input.d.ts +19 -0
- package/dist/chat/input.js +71 -0
- package/dist/chat/limit-resume.d.ts +11 -0
- package/dist/chat/limit-resume.js +68 -0
- package/dist/chat/projector.d.ts +60 -0
- package/dist/chat/projector.js +860 -0
- package/dist/chat/request-summary.d.ts +3 -0
- package/dist/chat/request-summary.js +143 -0
- package/dist/chat/subagent-projection.d.ts +8 -0
- package/dist/chat/subagent-projection.js +40 -0
- package/dist/chat/subagent-reader.d.ts +66 -0
- package/dist/chat/subagent-reader.js +396 -0
- package/dist/chat/subagent-settlement.d.ts +6 -0
- package/dist/chat/subagent-settlement.js +103 -0
- package/dist/chat/thread.d.ts +30 -0
- package/dist/chat/thread.js +147 -0
- package/dist/chat/wake-chat.d.ts +20 -0
- package/dist/chat/wake-chat.js +27 -0
- package/dist/client-sinks.d.ts +9 -0
- package/dist/client-sinks.js +34 -0
- package/dist/coded-error.d.ts +5 -0
- package/dist/coded-error.js +17 -0
- package/dist/context/argv.d.ts +11 -0
- package/dist/context/argv.js +58 -0
- package/dist/context/refusal.d.ts +9 -0
- package/dist/context/refusal.js +38 -0
- package/dist/context/verb.d.ts +85 -0
- package/dist/context/verb.js +233 -0
- package/dist/error-text.d.ts +5 -0
- package/dist/error-text.js +24 -0
- package/dist/events.d.ts +12 -0
- package/dist/events.js +1 -0
- package/dist/fs.d.ts +9 -0
- package/dist/fs.js +107 -0
- package/dist/host/agent-host.d.ts +28 -0
- package/dist/host/agent-host.js +105 -0
- package/dist/host/environment.d.ts +7 -0
- package/dist/host/environment.js +22 -0
- package/dist/host/handlers.d.ts +30 -0
- package/dist/host/handlers.js +111 -0
- package/dist/host/memory-port.d.ts +2 -0
- package/dist/host/memory-port.js +25 -0
- package/dist/host/wiring.d.ts +40 -0
- package/dist/host/wiring.js +120 -0
- package/dist/lineage.d.ts +44 -0
- package/dist/lineage.js +134 -0
- package/dist/messages/deliver-message.d.ts +21 -0
- package/dist/messages/deliver-message.js +42 -0
- package/dist/messages/deliver-notice.d.ts +25 -0
- package/dist/messages/deliver-notice.js +75 -0
- package/dist/messages/notice-notes.d.ts +15 -0
- package/dist/messages/notice-notes.js +26 -0
- package/dist/messages/notice-store.d.ts +30 -0
- package/dist/messages/notice-store.js +120 -0
- package/dist/modes.d.ts +11 -0
- package/dist/modes.js +28 -0
- package/dist/outbox/manual-clock.d.ts +10 -0
- package/dist/outbox/manual-clock.js +25 -0
- package/dist/outbox/outbox-worker.d.ts +44 -0
- package/dist/outbox/outbox-worker.js +173 -0
- package/dist/outbox/outbox.d.ts +37 -0
- package/dist/outbox/outbox.js +76 -0
- package/dist/providers/accounts/accounts.d.ts +20 -0
- package/dist/providers/accounts/accounts.js +158 -0
- package/dist/providers/accounts/launch.d.ts +17 -0
- package/dist/providers/accounts/launch.js +30 -0
- package/dist/providers/accounts/service.d.ts +87 -0
- package/dist/providers/accounts/service.js +538 -0
- package/dist/providers/accounts/shadow-home.d.ts +8 -0
- package/dist/providers/accounts/shadow-home.js +70 -0
- package/dist/providers/accounts/status.d.ts +12 -0
- package/dist/providers/accounts/status.js +85 -0
- package/dist/providers/accounts/store.d.ts +14 -0
- package/dist/providers/accounts/store.js +112 -0
- package/dist/providers/accounts/test-accounts.d.ts +12 -0
- package/dist/providers/accounts/test-accounts.js +38 -0
- package/dist/providers/accounts/variables.d.ts +27 -0
- package/dist/providers/accounts/variables.js +94 -0
- package/dist/providers/catalog.d.ts +17 -0
- package/dist/providers/catalog.js +76 -0
- package/dist/providers/claude-models.json +114 -0
- package/dist/providers/claude-provider.d.ts +4 -0
- package/dist/providers/claude-provider.js +29 -0
- package/dist/providers/claude.d.ts +15 -0
- package/dist/providers/claude.js +105 -0
- package/dist/providers/codex-models.json +135 -0
- package/dist/providers/codex-provider.d.ts +4 -0
- package/dist/providers/codex-provider.js +27 -0
- package/dist/providers/codex-rules.d.ts +9 -0
- package/dist/providers/codex-rules.js +35 -0
- package/dist/providers/codex.d.ts +14 -0
- package/dist/providers/codex.js +52 -0
- package/dist/providers/detect.d.ts +6 -0
- package/dist/providers/detect.js +30 -0
- package/dist/providers/provider.d.ts +25 -0
- package/dist/providers/provider.js +1 -0
- package/dist/providers/registry.d.ts +28 -0
- package/dist/providers/registry.js +118 -0
- package/dist/record-directory.d.ts +27 -0
- package/dist/record-directory.js +114 -0
- package/dist/run-process.d.ts +13 -0
- package/dist/run-process.js +42 -0
- package/dist/serializer.d.ts +9 -0
- package/dist/serializer.js +34 -0
- package/dist/skills.d.ts +21 -0
- package/dist/skills.js +174 -0
- package/dist/tasks/background-limit.d.ts +16 -0
- package/dist/tasks/background-limit.js +55 -0
- package/dist/tasks/end-children.d.ts +34 -0
- package/dist/tasks/end-children.js +53 -0
- package/dist/tasks/give-task.d.ts +14 -0
- package/dist/tasks/give-task.js +33 -0
- package/dist/tasks/task-coordinator.d.ts +52 -0
- package/dist/tasks/task-coordinator.js +323 -0
- package/dist/tasks/task-store.d.ts +49 -0
- package/dist/tasks/task-store.js +197 -0
- package/dist/tasks/task-work.d.ts +47 -0
- package/dist/tasks/task-work.js +28 -0
- package/dist/tasks/waiting-child.d.ts +51 -0
- package/dist/tasks/waiting-child.js +143 -0
- package/dist/tasks/wake-parent.d.ts +19 -0
- package/dist/tasks/wake-parent.js +109 -0
- package/dist/tasks/wiring.d.ts +51 -0
- package/dist/tasks/wiring.js +151 -0
- package/dist/title-file.d.ts +2 -0
- package/dist/title-file.js +55 -0
- package/dist/usage/accounts.d.ts +15 -0
- package/dist/usage/accounts.js +64 -0
- package/dist/usage/aggregate.d.ts +12 -0
- package/dist/usage/aggregate.js +154 -0
- package/dist/usage/exchange.d.ts +16 -0
- package/dist/usage/exchange.js +84 -0
- package/dist/usage/index-file.d.ts +15 -0
- package/dist/usage/index-file.js +123 -0
- package/dist/usage/limits/monitor.d.ts +50 -0
- package/dist/usage/limits/monitor.js +179 -0
- package/dist/usage/limits/normalize.d.ts +21 -0
- package/dist/usage/limits/normalize.js +160 -0
- package/dist/usage/limits/probe.d.ts +9 -0
- package/dist/usage/limits/probe.js +113 -0
- package/dist/usage/prices-snapshot.json +1071 -0
- package/dist/usage/pricing.d.ts +34 -0
- package/dist/usage/pricing.js +186 -0
- package/dist/usage/projects.d.ts +17 -0
- package/dist/usage/projects.js +49 -0
- package/dist/usage/readers/claude.d.ts +3 -0
- package/dist/usage/readers/claude.js +59 -0
- package/dist/usage/readers/codex.d.ts +23 -0
- package/dist/usage/readers/codex.js +155 -0
- package/dist/usage/record.d.ts +15 -0
- package/dist/usage/record.js +39 -0
- package/dist/usage/roots.d.ts +12 -0
- package/dist/usage/roots.js +29 -0
- package/dist/usage/scanner.d.ts +30 -0
- package/dist/usage/scanner.js +234 -0
- package/dist/usage/usage-service.d.ts +35 -0
- package/dist/usage/usage-service.js +130 -0
- package/dist/watch-seam.d.ts +29 -0
- package/dist/watch-seam.js +78 -0
- package/dist/watch-test-helpers.d.ts +28 -0
- package/dist/watch-test-helpers.js +86 -0
- package/package.json +42 -3
- package/src/async.ts +40 -0
- package/src/chat/attachment-store.ts +108 -0
- package/src/chat/backend.ts +199 -0
- package/src/chat/background-work.ts +42 -0
- package/src/chat/bookmark-store.ts +166 -0
- package/src/chat/chat-core.ts +1228 -0
- package/src/chat/chat-log.ts +187 -0
- package/src/chat/chat-process.ts +237 -0
- package/src/chat/chat-session.ts +1723 -0
- package/src/chat/chat-store.ts +261 -0
- package/src/chat/chat-title.ts +90 -0
- package/src/chat/claude-backend.ts +235 -0
- package/src/chat/claude-protocol.ts +684 -0
- package/src/chat/claude-title.ts +106 -0
- package/src/chat/claude-transcript.ts +233 -0
- package/src/chat/codex-backend.ts +397 -0
- package/src/chat/codex-protocol.ts +872 -0
- package/src/chat/codex-thread.ts +162 -0
- package/src/chat/codex-transport.ts +224 -0
- package/src/chat/composer-preferences.ts +67 -0
- package/src/chat/context-breakdown.ts +80 -0
- package/src/chat/delta-coalescer.ts +62 -0
- package/src/chat/errors.ts +25 -0
- package/src/chat/fake-claude.ts +1006 -0
- package/src/chat/fake-cli.ts +199 -0
- package/src/chat/fake-codex.ts +534 -0
- package/src/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
- package/src/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
- package/src/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
- package/src/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
- package/src/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
- package/src/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
- package/src/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
- package/src/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
- package/src/chat/input.ts +89 -0
- package/src/chat/limit-resume.ts +74 -0
- package/src/chat/projector.ts +969 -0
- package/src/chat/request-summary.ts +168 -0
- package/src/chat/subagent-projection.ts +45 -0
- package/src/chat/subagent-reader.ts +469 -0
- package/src/chat/subagent-settlement.ts +111 -0
- package/src/chat/thread.ts +167 -0
- package/src/chat/wake-chat.ts +45 -0
- package/src/client-sinks.ts +39 -0
- package/src/coded-error.ts +18 -0
- package/src/context/argv.ts +61 -0
- package/src/context/refusal.ts +50 -0
- package/src/context/verb.ts +392 -0
- package/src/error-text.ts +28 -0
- package/src/events.ts +9 -0
- package/src/fs.ts +118 -0
- package/src/host/agent-host.ts +124 -0
- package/src/host/environment.ts +31 -0
- package/src/host/handlers.ts +142 -0
- package/src/host/memory-port.ts +27 -0
- package/src/host/wiring.ts +164 -0
- package/src/lineage.ts +159 -0
- package/src/messages/deliver-message.ts +61 -0
- package/src/messages/deliver-notice.ts +120 -0
- package/src/messages/notice-notes.ts +34 -0
- package/src/messages/notice-store.ts +142 -0
- package/src/modes.ts +38 -0
- package/src/outbox/manual-clock.ts +31 -0
- package/src/outbox/outbox-worker.ts +214 -0
- package/src/outbox/outbox.ts +106 -0
- package/src/providers/accounts/accounts.ts +192 -0
- package/src/providers/accounts/launch.ts +55 -0
- package/src/providers/accounts/service.ts +632 -0
- package/src/providers/accounts/shadow-home.ts +85 -0
- package/src/providers/accounts/status.ts +106 -0
- package/src/providers/accounts/store.ts +158 -0
- package/src/providers/accounts/test-accounts.ts +48 -0
- package/src/providers/accounts/variables.ts +132 -0
- package/src/providers/catalog.ts +90 -0
- package/src/providers/claude-models.json +114 -0
- package/src/providers/claude-provider.ts +32 -0
- package/src/providers/claude.ts +123 -0
- package/src/providers/codex-models.json +135 -0
- package/src/providers/codex-provider.ts +30 -0
- package/src/providers/codex-rules.ts +54 -0
- package/src/providers/codex.ts +69 -0
- package/src/providers/detect.ts +40 -0
- package/src/providers/provider.ts +46 -0
- package/src/providers/registry.ts +150 -0
- package/src/record-directory.ts +136 -0
- package/src/run-process.ts +61 -0
- package/src/serializer.ts +41 -0
- package/src/skills.ts +199 -0
- package/src/tasks/background-limit.ts +72 -0
- package/src/tasks/end-children.ts +100 -0
- package/src/tasks/give-task.ts +52 -0
- package/src/tasks/task-coordinator.ts +384 -0
- package/src/tasks/task-store.ts +236 -0
- package/src/tasks/task-work.ts +62 -0
- package/src/tasks/waiting-child.ts +203 -0
- package/src/tasks/wake-parent.ts +142 -0
- package/src/tasks/wiring.ts +231 -0
- package/src/title-file.ts +57 -0
- package/src/usage/accounts.ts +86 -0
- package/src/usage/aggregate.ts +220 -0
- package/src/usage/exchange.ts +93 -0
- package/src/usage/index-file.ts +178 -0
- package/src/usage/limits/monitor.ts +239 -0
- package/src/usage/limits/normalize.ts +188 -0
- package/src/usage/limits/probe.ts +120 -0
- package/src/usage/prices-snapshot.json +1071 -0
- package/src/usage/pricing.ts +236 -0
- package/src/usage/projects.ts +65 -0
- package/src/usage/readers/claude.ts +60 -0
- package/src/usage/readers/codex.ts +196 -0
- package/src/usage/record.ts +60 -0
- package/src/usage/roots.ts +39 -0
- package/src/usage/scanner.ts +265 -0
- package/src/usage/usage-service.ts +161 -0
- package/src/watch-seam.ts +111 -0
- package/src/watch-test-helpers.ts +102 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { closeSync, fstatSync, mkdirSync, openSync, readSync, rmSync, writeSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { ChatEventSchema, type ChatEvent } from '@adecore/agent-contracts';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { errorText } from '../error-text.ts';
|
|
6
|
+
import { writeAtomicSync } from '../fs.ts';
|
|
7
|
+
|
|
8
|
+
export interface ChatLogLine {
|
|
9
|
+
seq: number;
|
|
10
|
+
at: number;
|
|
11
|
+
event: ChatEvent;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Past this the next write of the record folds the log into it. */
|
|
15
|
+
export const COMPACT_ABOVE_BYTES = 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
const LineSchema = z.object({ seq: z.number().int().positive(), at: z.number(), event: ChatEventSchema });
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The lines of a log as they were written. A line that does not parse is left out: a crash in the
|
|
21
|
+
* middle of an append leaves a torn last line, and what came before it is still good.
|
|
22
|
+
*/
|
|
23
|
+
export function parseLog(raw: string): ChatLogLine[] {
|
|
24
|
+
const lines: ChatLogLine[] = [];
|
|
25
|
+
for (const text of raw.split('\n')) {
|
|
26
|
+
if (text === '') {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let parsed: ReturnType<typeof LineSchema.safeParse>;
|
|
30
|
+
try {
|
|
31
|
+
parsed = LineSchema.safeParse(JSON.parse(text));
|
|
32
|
+
} catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (parsed.success && parsed.data.seq > (lines.at(-1)?.seq ?? 0)) {
|
|
36
|
+
lines.push(parsed.data);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ChatLogState {
|
|
43
|
+
// The last seq anything on disk accounts for, the snapshot or the log.
|
|
44
|
+
seq: number;
|
|
45
|
+
resetSeq: number;
|
|
46
|
+
// What the log file still holds, oldest first.
|
|
47
|
+
lines: ChatLogLine[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* Number and synchronously append each broadcast event so disk, memory and snapshot boundaries keep
|
|
52
|
+
* one order. Recent lines stay in memory for immediate `since` attachment.
|
|
53
|
+
*/
|
|
54
|
+
export class ChatLog {
|
|
55
|
+
private readonly path: string | null;
|
|
56
|
+
private lines: ChatLogLine[];
|
|
57
|
+
private bytes: number;
|
|
58
|
+
// Every event after this seq is in `lines`; anything at or before it only the snapshot still has.
|
|
59
|
+
private base: number;
|
|
60
|
+
private fd: number | null = null;
|
|
61
|
+
private seqValue: number;
|
|
62
|
+
private resetSeqValue: number;
|
|
63
|
+
// False once a line failed to reach the disk, until a fold writes the file again.
|
|
64
|
+
private written = true;
|
|
65
|
+
|
|
66
|
+
constructor(path: string | null, state: ChatLogState = { seq: 0, resetSeq: 0, lines: [] }) {
|
|
67
|
+
this.path = path;
|
|
68
|
+
this.lines = state.lines;
|
|
69
|
+
this.bytes = state.lines.reduce((sum, line) => sum + JSON.stringify(line).length + 1, 0);
|
|
70
|
+
this.seqValue = Math.max(state.seq, state.lines.at(-1)?.seq ?? 0);
|
|
71
|
+
this.resetSeqValue = state.resetSeq;
|
|
72
|
+
const first = state.lines[0];
|
|
73
|
+
// A log with a hole in front of it only answers from after the hole.
|
|
74
|
+
this.base = first === undefined ? this.seqValue : first.seq - 1;
|
|
75
|
+
for (let i = 1; i < state.lines.length; i++) {
|
|
76
|
+
if (state.lines[i]!.seq !== state.lines[i - 1]!.seq + 1) {
|
|
77
|
+
this.base = state.lines[i - 1]!.seq;
|
|
78
|
+
this.lines = state.lines.slice(i);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get seq(): number {
|
|
84
|
+
return this.seqValue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get resetSeq(): number {
|
|
88
|
+
return this.resetSeqValue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* How much the log file holds, which is what decides whether writing a snapshot folds it in. */
|
|
92
|
+
get size(): number {
|
|
93
|
+
return this.bytes;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Whether the file holds every line since the last fold, which is what lets a snapshot wait. */
|
|
97
|
+
get onDisk(): boolean {
|
|
98
|
+
return this.written;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Numbers an event and writes it down; answers its seq. The seq holds even when the disk refused the line. */
|
|
102
|
+
append(event: ChatEvent, at: number): number {
|
|
103
|
+
this.seqValue += 1;
|
|
104
|
+
const line: ChatLogLine = { seq: this.seqValue, at, event };
|
|
105
|
+
if (event.type === 'reset') {
|
|
106
|
+
this.resetSeqValue = line.seq;
|
|
107
|
+
}
|
|
108
|
+
const text = `${JSON.stringify(line)}\n`;
|
|
109
|
+
this.lines.push(line);
|
|
110
|
+
this.bytes += text.length;
|
|
111
|
+
if (this.path !== null) {
|
|
112
|
+
try {
|
|
113
|
+
this.fd ??= this.open(this.path);
|
|
114
|
+
writeSync(this.fd, text);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
this.written = false;
|
|
117
|
+
// The event still reaches every client; only a restart before the next snapshot loses it.
|
|
118
|
+
console.error(`Appending to ${this.path} failed:`, errorText(e));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return line.seq;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
* What came after `since`, when all of it is still here: null for a seq from before a reset (the
|
|
126
|
+
* thread it built on is gone), from before the lines held (folded into the snapshot) or from a
|
|
127
|
+
* stream this chat never reached.
|
|
128
|
+
*/
|
|
129
|
+
after(since: number): ChatEvent[] | null {
|
|
130
|
+
if (since < this.resetSeqValue || since < this.base || since > this.seqValue) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return this.lines.filter((line) => line.seq > since).map((line) => line.event);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/*
|
|
137
|
+
* Folds the log into a snapshot that holds everything up to `seq`. What came after it (events
|
|
138
|
+
* broadcast while that snapshot was on its way to disk) is written again as the whole log, in
|
|
139
|
+
* one synchronous step, so no append can land between the two. A snapshot older than the last
|
|
140
|
+
* fold changes nothing.
|
|
141
|
+
*/
|
|
142
|
+
compact(seq: number): void {
|
|
143
|
+
if (seq <= this.base) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.lines = this.lines.filter((line) => line.seq > seq);
|
|
147
|
+
this.base = seq;
|
|
148
|
+
const text = this.lines.map((line) => `${JSON.stringify(line)}\n`).join('');
|
|
149
|
+
this.bytes = text.length;
|
|
150
|
+
if (this.path === null) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.close();
|
|
154
|
+
try {
|
|
155
|
+
if (text === '') {
|
|
156
|
+
rmSync(this.path, { force: true });
|
|
157
|
+
} else {
|
|
158
|
+
writeAtomicSync(this.path, text);
|
|
159
|
+
}
|
|
160
|
+
this.written = true;
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error(`Compacting ${this.path} failed:`, errorText(e));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
close(): void {
|
|
167
|
+
if (this.fd !== null) {
|
|
168
|
+
closeSync(this.fd);
|
|
169
|
+
this.fd = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* A torn last line from a crash gets its newline first, so the next line does not run into it and get lost with it. */
|
|
174
|
+
private open(path: string): number {
|
|
175
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
176
|
+
const fd = openSync(path, 'a+', 0o600);
|
|
177
|
+
const { size } = fstatSync(fd);
|
|
178
|
+
if (size > 0) {
|
|
179
|
+
const last = Buffer.alloc(1);
|
|
180
|
+
readSync(fd, last, 0, 1, size - 1);
|
|
181
|
+
if (last[0] !== 0x0a) {
|
|
182
|
+
writeSync(fd, '\n');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return fd;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* The part of a child process a chat backend talks to: lines in over stdin, lines out over stdout,
|
|
6
|
+
* a signal and an exit. A test hands in a CLI that runs in the same process instead of a real one.
|
|
7
|
+
*/
|
|
8
|
+
export interface ChatProcess {
|
|
9
|
+
readonly pid: number;
|
|
10
|
+
readonly stdin: {
|
|
11
|
+
write(chunk: string): unknown;
|
|
12
|
+
flush(): unknown;
|
|
13
|
+
end(): unknown;
|
|
14
|
+
};
|
|
15
|
+
readonly stdout: ReadableStream<Uint8Array>;
|
|
16
|
+
// Only read for the tail a crash note carries; a fake that never writes there may leave it out.
|
|
17
|
+
readonly stderr?: ReadableStream<Uint8Array>;
|
|
18
|
+
// Reaches every process the CLI started as well, which share its process group.
|
|
19
|
+
kill(signal: 'SIGTERM' | 'SIGKILL'): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChatSpawnOptions {
|
|
23
|
+
command: string[];
|
|
24
|
+
cwd: string;
|
|
25
|
+
env: Record<string, string>;
|
|
26
|
+
onExit(exitCode: number | null): void;
|
|
27
|
+
onError?(error: Error): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SpawnChatProcess = (options: ChatSpawnOptions) => ChatProcess;
|
|
31
|
+
|
|
32
|
+
// How long an exit waits for the rest of stdout; a process the CLI started may hold the pipe open.
|
|
33
|
+
const STDOUT_DRAIN_MS = 250;
|
|
34
|
+
|
|
35
|
+
export function spawnChatProcess(options: ChatSpawnOptions): ChatProcess {
|
|
36
|
+
const [command, ...args] = options.command;
|
|
37
|
+
if (command === undefined) {
|
|
38
|
+
throw new Error('No command to start');
|
|
39
|
+
}
|
|
40
|
+
const child = spawn(command, args, {
|
|
41
|
+
cwd: options.cwd,
|
|
42
|
+
env: options.env,
|
|
43
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
+
// A group of its own, so ending the chat also ends what its agent started (a dev server, a watcher).
|
|
45
|
+
detached: true
|
|
46
|
+
});
|
|
47
|
+
// Node reports a missing executable as an event after the call; a start that failed says so on the spot.
|
|
48
|
+
child.on('error', () => undefined);
|
|
49
|
+
const pid = child.pid;
|
|
50
|
+
if (pid === undefined) {
|
|
51
|
+
throw new Error(`Could not start ${command}: no such executable`);
|
|
52
|
+
}
|
|
53
|
+
child.stdin.on('error', (error) => options.onError?.(error));
|
|
54
|
+
child.on('exit', (exitCode) => {
|
|
55
|
+
// The last lines may still be on their way through the pipe; they belong before the exit.
|
|
56
|
+
const report = (): void => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
child.stdout.off('end', report);
|
|
59
|
+
setImmediate(() => options.onExit(exitCode));
|
|
60
|
+
};
|
|
61
|
+
const timer = setTimeout(report, STDOUT_DRAIN_MS);
|
|
62
|
+
if (child.stdout.readableEnded) {
|
|
63
|
+
report();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
child.stdout.once('end', report);
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
pid,
|
|
70
|
+
stdin: {
|
|
71
|
+
write: (chunk) => child.stdin.write(chunk),
|
|
72
|
+
flush: () => undefined,
|
|
73
|
+
end: () => child.stdin.end()
|
|
74
|
+
},
|
|
75
|
+
stdout: Readable.toWeb(child.stdout) as unknown as ReadableStream<Uint8Array>,
|
|
76
|
+
stderr: Readable.toWeb(child.stderr) as unknown as ReadableStream<Uint8Array>,
|
|
77
|
+
kill: (signal) => {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(-pid, signal);
|
|
80
|
+
} catch {
|
|
81
|
+
// Nothing of the group is left to signal.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// After stdin closed, a CLI that is still around is not going to say more.
|
|
88
|
+
const EXIT_GRACE_MS = 3000;
|
|
89
|
+
// What a CLI and everything it started get between the SIGTERM and the SIGKILL.
|
|
90
|
+
const KILL_GRACE_MS = 2000;
|
|
91
|
+
// How long a crash note waits for the last of stderr; a process the CLI started may hold the pipe open.
|
|
92
|
+
const STDERR_DRAIN_MS = 250;
|
|
93
|
+
// A pipe nobody reads is buffered in memory for as long as the CLI lives, so stderr is read into this much and no more.
|
|
94
|
+
const STDERR_TAIL_CHARS = 8 * 1024;
|
|
95
|
+
// What of that tail goes into the note, which the log and every client carry.
|
|
96
|
+
const NOTE_TAIL_LINES = 10;
|
|
97
|
+
const NOTE_TAIL_CHARS = 2000;
|
|
98
|
+
|
|
99
|
+
/* The last few kilobytes a stream carried. */
|
|
100
|
+
export class StreamTail {
|
|
101
|
+
readonly done: Promise<void>;
|
|
102
|
+
private text = '';
|
|
103
|
+
|
|
104
|
+
constructor(stream: ReadableStream<Uint8Array>) {
|
|
105
|
+
this.done = this.read(stream);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* The last lines, short enough for a note; null when nothing but whitespace came. */
|
|
109
|
+
lines(): string | null {
|
|
110
|
+
const lines = this.text
|
|
111
|
+
.split('\n')
|
|
112
|
+
.map((line) => line.trimEnd())
|
|
113
|
+
.filter((line) => line.trim() !== '')
|
|
114
|
+
.slice(-NOTE_TAIL_LINES);
|
|
115
|
+
const tail = lines.join('\n').slice(-NOTE_TAIL_CHARS);
|
|
116
|
+
return tail === '' ? null : tail;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async read(stream: ReadableStream<Uint8Array>): Promise<void> {
|
|
120
|
+
const reader = stream.getReader();
|
|
121
|
+
const decoder = new TextDecoder();
|
|
122
|
+
try {
|
|
123
|
+
while (true) {
|
|
124
|
+
const { value, done } = await reader.read();
|
|
125
|
+
if (done) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
this.text = (this.text + decoder.decode(value, { stream: true })).slice(-STDERR_TAIL_CHARS);
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// The pipe went with the process; what came before it is still the tail.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface ChatChildOptions extends Omit<ChatSpawnOptions, 'onExit'> {
|
|
137
|
+
spawn?: SpawnChatProcess;
|
|
138
|
+
// `stderr` is the tail of what the CLI wrote there, for an exit with an error code only.
|
|
139
|
+
onExit(exitCode: number | null, stderr: string | null): void;
|
|
140
|
+
onError?(error: Error): void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* A spawned CLI and how it ends. Every signal goes to its process group, SIGTERM first and SIGKILL
|
|
145
|
+
* after a grace, and only ever because a person stopped, removed or restarted something.
|
|
146
|
+
*/
|
|
147
|
+
export class ChatChild {
|
|
148
|
+
readonly process: ChatProcess;
|
|
149
|
+
// Settles when the CLI itself exited, not when its report of that was delivered.
|
|
150
|
+
readonly exited: Promise<void>;
|
|
151
|
+
private readonly tail: StreamTail | null;
|
|
152
|
+
private hasExited = false;
|
|
153
|
+
private graceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
154
|
+
private ending: Promise<void> | null = null;
|
|
155
|
+
|
|
156
|
+
constructor(options: ChatChildOptions) {
|
|
157
|
+
let settle: () => void = () => undefined;
|
|
158
|
+
this.exited = new Promise((resolve) => {
|
|
159
|
+
settle = resolve;
|
|
160
|
+
});
|
|
161
|
+
const spawn = options.spawn ?? spawnChatProcess;
|
|
162
|
+
this.process = spawn({
|
|
163
|
+
command: options.command,
|
|
164
|
+
cwd: options.cwd,
|
|
165
|
+
env: options.env,
|
|
166
|
+
...(options.onError ? { onError: options.onError } : {}),
|
|
167
|
+
onExit: (exitCode) => {
|
|
168
|
+
this.hasExited = true;
|
|
169
|
+
this.clearGrace();
|
|
170
|
+
settle();
|
|
171
|
+
void this.report(exitCode, options.onExit);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
this.tail = this.process.stderr ? new StreamTail(this.process.stderr) : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get pid(): number {
|
|
178
|
+
return this.process.pid;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Ends the CLI once the grace is over and it has not left on its own; the caller closed its input. */
|
|
182
|
+
endAfterGrace(): void {
|
|
183
|
+
if (this.hasExited || this.graceTimer !== null || this.ending !== null) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.graceTimer = setTimeout(() => {
|
|
187
|
+
this.graceTimer = null;
|
|
188
|
+
void this.end();
|
|
189
|
+
}, EXIT_GRACE_MS);
|
|
190
|
+
this.graceTimer.unref?.();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* SIGTERM to the group now and SIGKILL after the grace; settles once the CLI exited or was sent the SIGKILL. */
|
|
194
|
+
end(): Promise<void> {
|
|
195
|
+
this.clearGrace();
|
|
196
|
+
if (this.hasExited) {
|
|
197
|
+
return Promise.resolve();
|
|
198
|
+
}
|
|
199
|
+
if (this.ending === null) {
|
|
200
|
+
this.process.kill('SIGTERM');
|
|
201
|
+
this.ending = new Promise((resolve) => {
|
|
202
|
+
// Sent even when the CLI left in time: what it started may still be shutting down, or ignoring the SIGTERM.
|
|
203
|
+
const timer = setTimeout(() => {
|
|
204
|
+
this.process.kill('SIGKILL');
|
|
205
|
+
resolve();
|
|
206
|
+
}, KILL_GRACE_MS);
|
|
207
|
+
timer.unref?.();
|
|
208
|
+
void this.exited.then(resolve);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return this.ending;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private clearGrace(): void {
|
|
215
|
+
if (this.graceTimer !== null) {
|
|
216
|
+
clearTimeout(this.graceTimer);
|
|
217
|
+
this.graceTimer = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async report(exitCode: number | null, onExit: ChatChildOptions['onExit']): Promise<void> {
|
|
222
|
+
if (exitCode === null || exitCode === 0 || this.tail === null) {
|
|
223
|
+
onExit(exitCode, null);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let release: () => void = () => undefined;
|
|
227
|
+
const timer = setTimeout(() => release(), STDERR_DRAIN_MS);
|
|
228
|
+
await Promise.race([
|
|
229
|
+
this.tail.done,
|
|
230
|
+
new Promise<void>((resolve) => {
|
|
231
|
+
release = resolve;
|
|
232
|
+
})
|
|
233
|
+
]);
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
onExit(exitCode, this.tail.lines());
|
|
236
|
+
}
|
|
237
|
+
}
|