alvin-bot 4.18.0 → 4.18.2

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 (74) hide show
  1. package/AEC-PLUGINS-SOURCES.md +53 -0
  2. package/CHANGELOG.md +37 -2
  3. package/DESIGN-SKILLS-SOURCES.md +81 -0
  4. package/bin/cli.js +1 -1
  5. package/dist/providers/claude-sdk-provider.js +24 -0
  6. package/package.json +3 -1
  7. package/test/allowed-users-gate.test.ts +0 -98
  8. package/test/alvin-dispatch.test.ts +0 -220
  9. package/test/async-agent-chunk-flow.test.ts +0 -244
  10. package/test/async-agent-parser-staleness.test.ts +0 -412
  11. package/test/async-agent-parser-streamjson.test.ts +0 -273
  12. package/test/async-agent-parser.test.ts +0 -322
  13. package/test/async-agent-watcher.test.ts +0 -229
  14. package/test/background-bypass-integration.test.ts +0 -443
  15. package/test/background-bypass-stress.test.ts +0 -417
  16. package/test/background-bypass.test.ts +0 -127
  17. package/test/browser-webfetch.test.ts +0 -121
  18. package/test/claude-sdk-provider.test.ts +0 -115
  19. package/test/claude-sdk-tool-use-id.test.ts +0 -180
  20. package/test/console-timestamps.test.ts +0 -98
  21. package/test/cron-progress-ticker.test.ts +0 -76
  22. package/test/cron-restart-resilience.test.ts +0 -191
  23. package/test/cron-run-resolver.test.ts +0 -133
  24. package/test/cron-runjobnow-throw.test.ts +0 -100
  25. package/test/debounce.test.ts +0 -60
  26. package/test/delivery-registry.test.ts +0 -71
  27. package/test/exec-guard-metachars.test.ts +0 -110
  28. package/test/file-permissions.test.ts +0 -130
  29. package/test/i18n.test.ts +0 -108
  30. package/test/list-subagents-merged.test.ts +0 -172
  31. package/test/memory-extractor.test.ts +0 -151
  32. package/test/memory-layers.test.ts +0 -169
  33. package/test/memory-sdk-injection.test.ts +0 -146
  34. package/test/memory-stress-restart.test.ts +0 -337
  35. package/test/multi-session-stress.test.ts +0 -255
  36. package/test/platform-session-key.test.ts +0 -69
  37. package/test/process-manager.test.ts +0 -186
  38. package/test/registry.test.ts +0 -201
  39. package/test/session-pending-background.test.ts +0 -59
  40. package/test/session-persistence.test.ts +0 -195
  41. package/test/slack-progress-ticker.test.ts +0 -123
  42. package/test/slack-slash-command.test.ts +0 -61
  43. package/test/slack-test-connection.test.ts +0 -176
  44. package/test/stress-scenarios.test.ts +0 -356
  45. package/test/stuck-timer.test.ts +0 -116
  46. package/test/subagent-delivery-markdown-fallback.test.ts +0 -147
  47. package/test/subagent-delivery-platform-routing.test.ts +0 -232
  48. package/test/subagent-delivery.test.ts +0 -273
  49. package/test/subagent-final-text.test.ts +0 -132
  50. package/test/subagent-stats.test.ts +0 -119
  51. package/test/subagent-toolset-allowlist.test.ts +0 -146
  52. package/test/subagents-commands.test.ts +0 -64
  53. package/test/subagents-config.test.ts +0 -114
  54. package/test/subagents-depth.test.ts +0 -58
  55. package/test/subagents-inheritance.test.ts +0 -67
  56. package/test/subagents-name-resolver.test.ts +0 -122
  57. package/test/subagents-priority-reject.test.ts +0 -88
  58. package/test/subagents-queue.test.ts +0 -127
  59. package/test/subagents-shutdown.test.ts +0 -126
  60. package/test/subagents-toolset.test.ts +0 -71
  61. package/test/sync-task-timeout.test.ts +0 -153
  62. package/test/system-prompt-background-hint.test.ts +0 -65
  63. package/test/telegram-error-filter.test.ts +0 -85
  64. package/test/telegram-workspace-command.test.ts +0 -78
  65. package/test/timing-safe-bearer.test.ts +0 -65
  66. package/test/watchdog-brake.test.ts +0 -157
  67. package/test/watcher-pending-count.test.ts +0 -228
  68. package/test/watcher-zombie-fix.test.ts +0 -252
  69. package/test/web-server-integration.test.ts +0 -189
  70. package/test/web-server-resilience.test.ts +0 -118
  71. package/test/web-server-shutdown.test.ts +0 -117
  72. package/test/whatsapp-auth-resilience.test.ts +0 -96
  73. package/test/workspaces.test.ts +0 -196
  74. package/vitest.config.ts +0 -17
@@ -1,273 +0,0 @@
1
- /**
2
- * v4.13 — parseOutputFileStatus support for `claude -p --output-format stream-json`.
3
- *
4
- * The SDK's built-in Task tool writes its sub-agent output in one JSONL
5
- * format (events with `message.stop_reason: "end_turn"`). The new v4.13
6
- * dispatch mechanism spawns `claude -p --output-format stream-json`
7
- * which writes a DIFFERENT format:
8
- *
9
- * - Assistant messages have `message.stop_reason: null` (streaming shape)
10
- * - A final `{"type":"result","subtype":"success","stop_reason":"end_turn",...}`
11
- * event marks completion explicitly
12
- * - `result.duration_ms`, `total_cost_usd`, `num_turns`, `usage`
13
- * are the authoritative completion signals
14
- *
15
- * The parser must recognize BOTH formats. v4.13 adds detection for the
16
- * result-event format while preserving backward compat with the existing
17
- * SDK-internal format (tested in the sibling test files).
18
- */
19
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
20
- import fs from "fs";
21
- import os from "os";
22
- import { resolve } from "path";
23
- import { parseOutputFileStatus } from "../src/services/async-agent-parser.js";
24
-
25
- const TMP_BASE = resolve(
26
- os.tmpdir(),
27
- `alvin-parser-streamjson-${process.pid}`,
28
- );
29
-
30
- beforeEach(() => {
31
- fs.mkdirSync(TMP_BASE, { recursive: true });
32
- });
33
- afterEach(() => {
34
- try {
35
- fs.rmSync(TMP_BASE, { recursive: true, force: true });
36
- } catch {
37
- /* ignore */
38
- }
39
- });
40
-
41
- describe("parseOutputFileStatus — stream-json format (v4.13)", () => {
42
- it("returns 'completed' when final event is type:result + subtype:success", async () => {
43
- const path = resolve(TMP_BASE, "stream-success.jsonl");
44
- const lines = [
45
- { type: "system", subtype: "init", session_id: "s1" },
46
- {
47
- type: "assistant",
48
- message: {
49
- role: "assistant",
50
- content: [{ type: "text", text: "The answer is 42." }],
51
- stop_reason: null, // streaming shape — NOT end_turn yet
52
- },
53
- session_id: "s1",
54
- },
55
- {
56
- type: "result",
57
- subtype: "success",
58
- stop_reason: "end_turn",
59
- session_id: "s1",
60
- total_cost_usd: 0.01,
61
- duration_ms: 500,
62
- usage: { input_tokens: 10, output_tokens: 5 },
63
- result: "The answer is 42.",
64
- },
65
- ];
66
- fs.writeFileSync(
67
- path,
68
- lines.map((l) => JSON.stringify(l)).join("\n") + "\n",
69
- "utf-8",
70
- );
71
-
72
- const status = await parseOutputFileStatus(path);
73
- expect(status.state).toBe("completed");
74
- if (status.state === "completed") {
75
- expect(status.output).toContain("The answer is 42.");
76
- expect(status.output).not.toMatch(/interrupted|partial/i);
77
- }
78
- });
79
-
80
- it("extracts tokens from result.usage when using stream-json format", async () => {
81
- const path = resolve(TMP_BASE, "stream-tokens.jsonl");
82
- const lines = [
83
- {
84
- type: "assistant",
85
- message: {
86
- content: [{ type: "text", text: "x" }],
87
- stop_reason: null,
88
- },
89
- },
90
- {
91
- type: "result",
92
- subtype: "success",
93
- stop_reason: "end_turn",
94
- usage: { input_tokens: 1234, output_tokens: 567 },
95
- },
96
- ];
97
- fs.writeFileSync(
98
- path,
99
- lines.map((l) => JSON.stringify(l)).join("\n") + "\n",
100
- "utf-8",
101
- );
102
- const status = await parseOutputFileStatus(path);
103
- expect(status.state).toBe("completed");
104
- if (status.state === "completed") {
105
- expect(status.tokensUsed).toEqual({ input: 1234, output: 567 });
106
- }
107
- });
108
-
109
- it("recognises 'failed' state when result.is_error is true", async () => {
110
- const path = resolve(TMP_BASE, "stream-failed.jsonl");
111
- const lines = [
112
- {
113
- type: "assistant",
114
- message: {
115
- content: [{ type: "text", text: "I tried..." }],
116
- stop_reason: null,
117
- },
118
- },
119
- {
120
- type: "result",
121
- subtype: "error_max_turns",
122
- is_error: true,
123
- stop_reason: "max_turns",
124
- },
125
- ];
126
- fs.writeFileSync(
127
- path,
128
- lines.map((l) => JSON.stringify(l)).join("\n") + "\n",
129
- "utf-8",
130
- );
131
- const status = await parseOutputFileStatus(path);
132
- // With an is_error result + text content, we still deliver the text
133
- // as completed (better to give the user SOMETHING than nothing).
134
- // The delivery layer can annotate differently if it chooses.
135
- expect(status.state).toBe("completed");
136
- if (status.state === "completed") {
137
- expect(status.output).toContain("I tried...");
138
- }
139
- });
140
-
141
- it("returns 'running' when stream-json events are present but no result yet", async () => {
142
- const path = resolve(TMP_BASE, "stream-running.jsonl");
143
- const lines = [
144
- { type: "system", subtype: "init", session_id: "s1" },
145
- {
146
- type: "assistant",
147
- message: {
148
- content: [{ type: "text", text: "Thinking..." }],
149
- stop_reason: null,
150
- },
151
- },
152
- {
153
- type: "assistant",
154
- message: {
155
- content: [{ type: "tool_use", name: "Bash", input: {} }],
156
- stop_reason: null,
157
- },
158
- },
159
- ];
160
- fs.writeFileSync(
161
- path,
162
- lines.map((l) => JSON.stringify(l)).join("\n") + "\n",
163
- "utf-8",
164
- );
165
- const status = await parseOutputFileStatus(path);
166
- expect(status.state).toBe("running");
167
- });
168
-
169
- it("aggregates text from ALL assistant messages when result arrives", async () => {
170
- const path = resolve(TMP_BASE, "stream-multi-text.jsonl");
171
- const lines = [
172
- {
173
- type: "assistant",
174
- message: {
175
- content: [{ type: "text", text: "First thought." }],
176
- stop_reason: null,
177
- },
178
- },
179
- {
180
- type: "user",
181
- message: { content: [{ type: "tool_result", content: "ok" }] },
182
- },
183
- {
184
- type: "assistant",
185
- message: {
186
- content: [{ type: "text", text: "Continuing..." }],
187
- stop_reason: null,
188
- },
189
- },
190
- {
191
- type: "user",
192
- message: { content: [{ type: "tool_result", content: "ok" }] },
193
- },
194
- {
195
- type: "assistant",
196
- message: {
197
- content: [{ type: "text", text: "Final answer." }],
198
- stop_reason: null,
199
- },
200
- },
201
- { type: "result", subtype: "success", stop_reason: "end_turn" },
202
- ];
203
- fs.writeFileSync(
204
- path,
205
- lines.map((l) => JSON.stringify(l)).join("\n") + "\n",
206
- "utf-8",
207
- );
208
- const status = await parseOutputFileStatus(path);
209
- expect(status.state).toBe("completed");
210
- if (status.state === "completed") {
211
- // All three text blocks must be present
212
- expect(status.output).toContain("First thought");
213
- expect(status.output).toContain("Continuing");
214
- expect(status.output).toContain("Final answer");
215
- }
216
- });
217
-
218
- it("prefers result.result field as authoritative output when available", async () => {
219
- // The stream-json's result event has a `result` field with the
220
- // already-concatenated final answer. Use it directly when present
221
- // (more accurate than re-aggregating from streaming chunks).
222
- const path = resolve(TMP_BASE, "stream-result-field.jsonl");
223
- const lines = [
224
- {
225
- type: "assistant",
226
- message: {
227
- content: [{ type: "text", text: "Intermediate chunk" }],
228
- stop_reason: null,
229
- },
230
- },
231
- {
232
- type: "result",
233
- subtype: "success",
234
- stop_reason: "end_turn",
235
- result: "FINAL AUTHORITATIVE ANSWER",
236
- },
237
- ];
238
- fs.writeFileSync(
239
- path,
240
- lines.map((l) => JSON.stringify(l)).join("\n") + "\n",
241
- "utf-8",
242
- );
243
- const status = await parseOutputFileStatus(path);
244
- expect(status.state).toBe("completed");
245
- if (status.state === "completed") {
246
- expect(status.output).toContain("FINAL AUTHORITATIVE ANSWER");
247
- }
248
- });
249
-
250
- it("handles result event with only partial fields (defensive)", async () => {
251
- const path = resolve(TMP_BASE, "stream-result-minimal.jsonl");
252
- const lines = [
253
- {
254
- type: "assistant",
255
- message: {
256
- content: [{ type: "text", text: "Some output" }],
257
- stop_reason: null,
258
- },
259
- },
260
- { type: "result" }, // no subtype, no result field, no usage
261
- ];
262
- fs.writeFileSync(
263
- path,
264
- lines.map((l) => JSON.stringify(l)).join("\n") + "\n",
265
- "utf-8",
266
- );
267
- const status = await parseOutputFileStatus(path);
268
- expect(status.state).toBe("completed");
269
- if (status.state === "completed") {
270
- expect(status.output).toContain("Some output");
271
- }
272
- });
273
- });
@@ -1,322 +0,0 @@
1
- /**
2
- * Fix #17 (Stage 2) — async-agent-parser unit tests.
3
- *
4
- * Two pure helpers:
5
- * parseAsyncLaunchedToolResult(text) → { agentId, outputFile } | null
6
- * parseOutputFileStatus(path) → { state: "running"|"completed"|"failed"|"missing" }
7
- *
8
- * Format details captured from the live SDK probe in
9
- * docs/superpowers/specs/sdk-async-agent-outputfile-format.md
10
- */
11
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
12
- import fs from "fs";
13
- import os from "os";
14
- import { resolve } from "path";
15
- import {
16
- parseAsyncLaunchedToolResult,
17
- parseOutputFileStatus,
18
- } from "../src/services/async-agent-parser.js";
19
-
20
- describe("parseAsyncLaunchedToolResult — plain text format (Stage 2)", () => {
21
- it("extracts agentId and output_file from the real SDK tool-result text", () => {
22
- const text = `Async agent launched successfully.
23
- agentId: a9e9c5913b2faec71 (internal ID - do not mention to user. Use SendMessage with to: 'a9e9c5913b2faec71' to continue this agent.)
24
- The agent is working in the background. You will be notified automatically when it completes.
25
- Do not duplicate this agent's work — avoid working with the same files or topics it is using.
26
- output_file: /private/tmp/claude-502/-Users-alvin-de-Projects-alvin-bot/abc/tasks/a9e9c5913b2faec71.output
27
- If asked, you can check progress before completion by using Read or Bash tail on the output file.`;
28
-
29
- const info = parseAsyncLaunchedToolResult(text);
30
- expect(info).not.toBeNull();
31
- expect(info?.agentId).toBe("a9e9c5913b2faec71");
32
- expect(info?.outputFile).toBe(
33
- "/private/tmp/claude-502/-Users-alvin-de-Projects-alvin-bot/abc/tasks/a9e9c5913b2faec71.output",
34
- );
35
- });
36
-
37
- it("returns null for ordinary tool result text (e.g. Read output)", () => {
38
- expect(parseAsyncLaunchedToolResult("file contents here")).toBeNull();
39
- });
40
-
41
- it("returns null for an empty string", () => {
42
- expect(parseAsyncLaunchedToolResult("")).toBeNull();
43
- });
44
-
45
- it("returns null when the marker line is missing", () => {
46
- expect(
47
- parseAsyncLaunchedToolResult("agentId: x\noutput_file: /tmp/a"),
48
- ).toBeNull();
49
- });
50
-
51
- it("returns null when output_file line is missing", () => {
52
- const text =
53
- "Async agent launched successfully.\nagentId: abc123\nMore prose";
54
- expect(parseAsyncLaunchedToolResult(text)).toBeNull();
55
- });
56
-
57
- it("returns null when agentId line is missing", () => {
58
- const text =
59
- "Async agent launched successfully.\noutput_file: /tmp/a\nMore prose";
60
- expect(parseAsyncLaunchedToolResult(text)).toBeNull();
61
- });
62
-
63
- it("trims whitespace around extracted values", () => {
64
- const text = `Async agent launched successfully.
65
- agentId: abc-with-spaces (something)
66
- output_file: /tmp/path with spaces.output `;
67
- const info = parseAsyncLaunchedToolResult(text);
68
- expect(info?.agentId).toBe("abc-with-spaces");
69
- // Path can contain spaces — we just trim leading/trailing
70
- expect(info?.outputFile).toBe("/tmp/path with spaces.output");
71
- });
72
-
73
- it("handles input that is an array of content blocks (Anthropic SDK shape)", () => {
74
- const blocks = [
75
- { type: "text", text: "Async agent launched successfully.\nagentId: id1\noutput_file: /tmp/o1\n" },
76
- ];
77
- const info = parseAsyncLaunchedToolResult(blocks);
78
- expect(info?.agentId).toBe("id1");
79
- expect(info?.outputFile).toBe("/tmp/o1");
80
- });
81
-
82
- it("handles non-string input gracefully", () => {
83
- expect(parseAsyncLaunchedToolResult(null)).toBeNull();
84
- expect(parseAsyncLaunchedToolResult(undefined)).toBeNull();
85
- expect(parseAsyncLaunchedToolResult(42 as unknown as string)).toBeNull();
86
- });
87
- });
88
-
89
- const TMP_BASE = resolve(os.tmpdir(), `alvin-parser-${process.pid}`);
90
-
91
- beforeEach(() => {
92
- fs.mkdirSync(TMP_BASE, { recursive: true });
93
- });
94
-
95
- afterEach(() => {
96
- try {
97
- fs.rmSync(TMP_BASE, { recursive: true, force: true });
98
- } catch { /* ignore */ }
99
- });
100
-
101
- async function writeJsonl(name: string, lines: object[]): Promise<string> {
102
- const path = resolve(TMP_BASE, name);
103
- fs.writeFileSync(
104
- path,
105
- lines.map((l) => JSON.stringify(l)).join("\n") + "\n",
106
- "utf-8",
107
- );
108
- return path;
109
- }
110
-
111
- describe("parseOutputFileStatus — JSONL completion detection (Stage 2)", () => {
112
- it("returns 'missing' when the file doesn't exist", async () => {
113
- const status = await parseOutputFileStatus(`${TMP_BASE}/nonexistent.jsonl`);
114
- expect(status.state).toBe("missing");
115
- });
116
-
117
- it("returns 'missing' for an empty file", async () => {
118
- const path = resolve(TMP_BASE, "empty.jsonl");
119
- fs.writeFileSync(path, "", "utf-8");
120
- const status = await parseOutputFileStatus(path);
121
- expect(status.state).toBe("missing");
122
- });
123
-
124
- it("returns 'running' when the file has events but no end_turn", async () => {
125
- const path = await writeJsonl("running.jsonl", [
126
- {
127
- type: "user",
128
- isSidechain: true,
129
- agentId: "x",
130
- message: { role: "user", content: "do the thing" },
131
- },
132
- {
133
- type: "assistant",
134
- isSidechain: true,
135
- agentId: "x",
136
- message: {
137
- role: "assistant",
138
- content: [{ type: "tool_use", name: "Bash", input: { command: "ls" } }],
139
- stop_reason: "tool_use",
140
- },
141
- },
142
- ]);
143
- const status = await parseOutputFileStatus(path);
144
- expect(status.state).toBe("running");
145
- });
146
-
147
- it("returns 'completed' with the final text when stop_reason is end_turn", async () => {
148
- const path = await writeJsonl("completed.jsonl", [
149
- {
150
- type: "user",
151
- isSidechain: true,
152
- agentId: "x",
153
- message: { role: "user", content: "p" },
154
- },
155
- {
156
- type: "assistant",
157
- isSidechain: true,
158
- agentId: "x",
159
- message: {
160
- role: "assistant",
161
- content: [{ type: "text", text: "Final report: it works!" }],
162
- stop_reason: "end_turn",
163
- usage: { input_tokens: 100, output_tokens: 50 },
164
- },
165
- },
166
- ]);
167
- const status = await parseOutputFileStatus(path);
168
- expect(status.state).toBe("completed");
169
- if (status.state === "completed") {
170
- expect(status.output).toContain("Final report: it works!");
171
- expect(status.tokensUsed).toEqual({ input: 100, output: 50 });
172
- }
173
- });
174
-
175
- it("concatenates multiple text blocks in the final assistant message", async () => {
176
- const path = await writeJsonl("multi-block.jsonl", [
177
- {
178
- type: "assistant",
179
- isSidechain: true,
180
- agentId: "x",
181
- message: {
182
- role: "assistant",
183
- content: [
184
- { type: "thinking", text: "let me think" },
185
- { type: "text", text: "Part one." },
186
- { type: "text", text: "Part two." },
187
- ],
188
- stop_reason: "end_turn",
189
- },
190
- },
191
- ]);
192
- const status = await parseOutputFileStatus(path);
193
- expect(status.state).toBe("completed");
194
- if (status.state === "completed") {
195
- expect(status.output).toBe("Part one.\n\nPart two.");
196
- // thinking blocks are NOT included
197
- expect(status.output).not.toContain("let me think");
198
- }
199
- });
200
-
201
- it("ignores assistant messages with stop_reason !== end_turn (still running)", async () => {
202
- const path = await writeJsonl("intermediate.jsonl", [
203
- {
204
- type: "assistant",
205
- isSidechain: true,
206
- agentId: "x",
207
- message: {
208
- role: "assistant",
209
- content: [{ type: "text", text: "checking..." }],
210
- stop_reason: "tool_use",
211
- },
212
- },
213
- ]);
214
- const status = await parseOutputFileStatus(path);
215
- expect(status.state).toBe("running");
216
- });
217
-
218
- it("uses the LAST end_turn assistant message when there are multiple turns", async () => {
219
- const path = await writeJsonl("multi-turn.jsonl", [
220
- {
221
- type: "assistant",
222
- agentId: "x",
223
- message: {
224
- content: [{ type: "text", text: "first answer" }],
225
- stop_reason: "end_turn",
226
- },
227
- },
228
- {
229
- type: "user",
230
- agentId: "x",
231
- message: { content: [{ type: "tool_result", content: "..." }] },
232
- },
233
- {
234
- type: "assistant",
235
- agentId: "x",
236
- message: {
237
- content: [{ type: "text", text: "second and final answer" }],
238
- stop_reason: "end_turn",
239
- },
240
- },
241
- ]);
242
- const status = await parseOutputFileStatus(path);
243
- expect(status.state).toBe("completed");
244
- if (status.state === "completed") {
245
- expect(status.output).toBe("second and final answer");
246
- }
247
- });
248
-
249
- it("survives partial final lines (mid-write)", async () => {
250
- const path = resolve(TMP_BASE, "partial.jsonl");
251
- fs.writeFileSync(
252
- path,
253
- JSON.stringify({
254
- type: "assistant",
255
- agentId: "x",
256
- message: {
257
- content: [{ type: "text", text: "checking" }],
258
- stop_reason: "tool_use",
259
- },
260
- }) +
261
- "\n" +
262
- '{"type":"assistant","agentId":"x","mes',
263
- "utf-8",
264
- );
265
- const status = await parseOutputFileStatus(path);
266
- // Partial line is ignored; only the complete event counts
267
- expect(status.state).toBe("running");
268
- });
269
-
270
- it("survives unparseable lines (skip them, keep checking)", async () => {
271
- const path = resolve(TMP_BASE, "garbage.jsonl");
272
- fs.writeFileSync(
273
- path,
274
- "garbage line\n" +
275
- JSON.stringify({
276
- type: "assistant",
277
- agentId: "x",
278
- message: {
279
- content: [{ type: "text", text: "the answer" }],
280
- stop_reason: "end_turn",
281
- },
282
- }) +
283
- "\n",
284
- "utf-8",
285
- );
286
- const status = await parseOutputFileStatus(path);
287
- expect(status.state).toBe("completed");
288
- if (status.state === "completed") {
289
- expect(status.output).toBe("the answer");
290
- }
291
- });
292
-
293
- it("only tail-reads large files (does not load entire content into memory)", async () => {
294
- const path = resolve(TMP_BASE, "huge.jsonl");
295
- // Write a 200KB padding stream of 'running' events, then an end_turn
296
- const padding = JSON.stringify({
297
- type: "assistant",
298
- agentId: "x",
299
- message: { content: [{ type: "text", text: "x".repeat(500) }], stop_reason: "tool_use" },
300
- });
301
- let buf = "";
302
- for (let i = 0; i < 200; i++) buf += padding + "\n";
303
- buf +=
304
- JSON.stringify({
305
- type: "assistant",
306
- agentId: "x",
307
- message: {
308
- content: [{ type: "text", text: "FINAL" }],
309
- stop_reason: "end_turn",
310
- },
311
- }) + "\n";
312
- fs.writeFileSync(path, buf, "utf-8");
313
- expect(fs.statSync(path).size).toBeGreaterThan(100_000);
314
-
315
- const status = await parseOutputFileStatus(path, { maxTailBytes: 8192 });
316
- // Tail should still find the last end_turn
317
- expect(status.state).toBe("completed");
318
- if (status.state === "completed") {
319
- expect(status.output).toBe("FINAL");
320
- }
321
- });
322
- });