devharness 0.9.8 → 0.9.10

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 (161) hide show
  1. package/README.md +16 -1
  2. package/build/block-events.d.ts +3 -5
  3. package/build/block-events.d.ts.map +1 -1
  4. package/build/block-events.js +6 -24
  5. package/build/block-events.js.map +1 -1
  6. package/build/block-events.test.js +10 -7
  7. package/build/block-events.test.js.map +1 -1
  8. package/build/cdp-manager.d.ts +7 -0
  9. package/build/cdp-manager.d.ts.map +1 -1
  10. package/build/cdp-manager.js +9 -0
  11. package/build/cdp-manager.js.map +1 -1
  12. package/build/cli/index.d.ts +26 -0
  13. package/build/cli/index.d.ts.map +1 -0
  14. package/build/cli/index.js +290 -0
  15. package/build/cli/index.js.map +1 -0
  16. package/build/cli/process-tree.d.ts +21 -0
  17. package/build/cli/process-tree.d.ts.map +1 -0
  18. package/build/cli/process-tree.js +51 -0
  19. package/build/cli/process-tree.js.map +1 -0
  20. package/build/cli/session-match.d.ts +37 -0
  21. package/build/cli/session-match.d.ts.map +1 -0
  22. package/build/cli/session-match.js +55 -0
  23. package/build/cli/session-match.js.map +1 -0
  24. package/build/cli/session-match.test.d.ts +17 -0
  25. package/build/cli/session-match.test.d.ts.map +1 -0
  26. package/build/cli/session-match.test.js +125 -0
  27. package/build/cli/session-match.test.js.map +1 -0
  28. package/build/config.d.ts +2 -2
  29. package/build/config.d.ts.map +1 -1
  30. package/build/config.js +2 -1
  31. package/build/config.js.map +1 -1
  32. package/build/config.test.js +2 -2
  33. package/build/config.test.js.map +1 -1
  34. package/build/connection-manager.d.ts.map +1 -1
  35. package/build/connection-manager.js +6 -1
  36. package/build/connection-manager.js.map +1 -1
  37. package/build/dashboard/dashboard-hub.js +2 -2
  38. package/build/index.js +60 -7
  39. package/build/index.js.map +1 -1
  40. package/build/interaction-recorder.js +2 -2
  41. package/build/interaction-recorder.js.map +1 -1
  42. package/build/issue-tracker.test.js +1 -1
  43. package/build/issue-tracker.test.js.map +1 -1
  44. package/build/mcp-supervisor.js +43 -0
  45. package/build/mcp-supervisor.js.map +1 -1
  46. package/build/network-monitor.d.ts +13 -2
  47. package/build/network-monitor.d.ts.map +1 -1
  48. package/build/network-monitor.js +38 -2
  49. package/build/network-monitor.js.map +1 -1
  50. package/build/network-monitor.test.js +50 -0
  51. package/build/network-monitor.test.js.map +1 -1
  52. package/build/persistent-profiles.test.js +2 -2
  53. package/build/persistent-profiles.test.js.map +1 -1
  54. package/build/plugin-session-start-hook.test.d.ts +10 -0
  55. package/build/plugin-session-start-hook.test.d.ts.map +1 -0
  56. package/build/plugin-session-start-hook.test.js +187 -0
  57. package/build/plugin-session-start-hook.test.js.map +1 -0
  58. package/build/runners/docker-compose-runner.js +1 -1
  59. package/build/runners/docker-compose-runner.js.map +1 -1
  60. package/build/runners/docker-runner.js +1 -1
  61. package/build/runners/docker-runner.js.map +1 -1
  62. package/build/runners/types.d.ts +1 -1
  63. package/build/runners/types.d.ts.map +1 -1
  64. package/build/server-manager.d.ts +1 -1
  65. package/build/server-manager.js +3 -3
  66. package/build/server-manager.js.map +1 -1
  67. package/build/server-manager.test.d.ts +1 -1
  68. package/build/server-manager.test.js +1 -1
  69. package/build/server-ownership.test.js +2 -2
  70. package/build/server-ownership.test.js.map +1 -1
  71. package/build/session-endpoint.d.ts +72 -0
  72. package/build/session-endpoint.d.ts.map +1 -0
  73. package/build/session-endpoint.js +220 -0
  74. package/build/session-endpoint.js.map +1 -0
  75. package/build/session-endpoint.test.d.ts +10 -0
  76. package/build/session-endpoint.test.d.ts.map +1 -0
  77. package/build/session-endpoint.test.js +202 -0
  78. package/build/session-endpoint.test.js.map +1 -0
  79. package/build/session-events.d.ts +28 -0
  80. package/build/session-events.d.ts.map +1 -0
  81. package/build/session-events.js +38 -0
  82. package/build/session-events.js.map +1 -0
  83. package/build/session-events.test.d.ts +8 -0
  84. package/build/session-events.test.d.ts.map +1 -0
  85. package/build/session-events.test.js +56 -0
  86. package/build/session-events.test.js.map +1 -0
  87. package/build/session-identity.d.ts +56 -0
  88. package/build/session-identity.d.ts.map +1 -0
  89. package/build/session-identity.js +96 -0
  90. package/build/session-identity.js.map +1 -0
  91. package/build/session-identity.test.d.ts +9 -0
  92. package/build/session-identity.test.d.ts.map +1 -0
  93. package/build/session-identity.test.js +126 -0
  94. package/build/session-identity.test.js.map +1 -0
  95. package/build/session-messages.d.ts +70 -0
  96. package/build/session-messages.d.ts.map +1 -0
  97. package/build/session-messages.js +139 -0
  98. package/build/session-messages.js.map +1 -0
  99. package/build/supervisor/pidfile.test.js +1 -1
  100. package/build/supervisor/pidfile.test.js.map +1 -1
  101. package/build/tool-response.d.ts +40 -1
  102. package/build/tool-response.d.ts.map +1 -1
  103. package/build/tool-response.js.map +1 -1
  104. package/build/tools/config-tools.d.ts +1 -1
  105. package/build/tools/config-tools.js +4 -4
  106. package/build/tools/config-tools.js.map +1 -1
  107. package/build/tools/console-tools.d.ts +4 -0
  108. package/build/tools/console-tools.d.ts.map +1 -1
  109. package/build/tools/console-tools.js +55 -1
  110. package/build/tools/console-tools.js.map +1 -1
  111. package/build/tools/dashboard-tools.d.ts +1 -1
  112. package/build/tools/dashboard-tools.js +2 -2
  113. package/build/tools/dashboard-tools.js.map +1 -1
  114. package/build/tools/inspect-worker-meta.test.d.ts +9 -0
  115. package/build/tools/inspect-worker-meta.test.d.ts.map +1 -0
  116. package/build/tools/inspect-worker-meta.test.js +122 -0
  117. package/build/tools/inspect-worker-meta.test.js.map +1 -0
  118. package/build/tools/inspection-tools.d.ts +12 -5
  119. package/build/tools/inspection-tools.d.ts.map +1 -1
  120. package/build/tools/inspection-tools.js +86 -5
  121. package/build/tools/inspection-tools.js.map +1 -1
  122. package/build/tools/issues-abort-hygiene.test.js +1 -1
  123. package/build/tools/issues-abort-hygiene.test.js.map +1 -1
  124. package/build/tools/issues-tools.d.ts +3 -3
  125. package/build/tools/issues-tools.test.js +1 -1
  126. package/build/tools/issues-tools.test.js.map +1 -1
  127. package/build/tools/message-tools.d.ts +51 -0
  128. package/build/tools/message-tools.d.ts.map +1 -0
  129. package/build/tools/message-tools.js +251 -0
  130. package/build/tools/message-tools.js.map +1 -0
  131. package/build/tools/message-tools.test.d.ts +9 -0
  132. package/build/tools/message-tools.test.d.ts.map +1 -0
  133. package/build/tools/message-tools.test.js +332 -0
  134. package/build/tools/message-tools.test.js.map +1 -0
  135. package/build/tools/network-tools.js +1 -1
  136. package/build/tools/network-tools.js.map +1 -1
  137. package/build/tools/replay-tools.d.ts +3 -3
  138. package/build/tools/replay-tools.js +3 -3
  139. package/build/tools/replay-tools.js.map +1 -1
  140. package/build/tools/server-tools.d.ts +3 -3
  141. package/build/worker-targets.d.ts +75 -0
  142. package/build/worker-targets.d.ts.map +1 -0
  143. package/build/worker-targets.js +171 -0
  144. package/build/worker-targets.js.map +1 -0
  145. package/build/worker-targets.test.d.ts +8 -0
  146. package/build/worker-targets.test.d.ts.map +1 -0
  147. package/build/worker-targets.test.js +118 -0
  148. package/build/worker-targets.test.js.map +1 -0
  149. package/docs/README.md +2 -2
  150. package/docs/automation.md +1 -1
  151. package/docs/installation.md +4 -4
  152. package/docs/instructions.md +26 -15
  153. package/docs/message-system.md +1 -1
  154. package/docs/messages.md +153 -3
  155. package/docs/parser-plugins.md +1 -1
  156. package/docs/replay.md +1 -1
  157. package/docs/troubleshooting.md +3 -3
  158. package/package.json +2 -1
  159. package/plugin/skills/devharness/SKILL.md +36 -8
  160. package/plugin/skills/devharness/references/sequences.md +2 -2
  161. package/plugin/skills/devharness/references/tool-categories.md +10 -3
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Message tool - text between two devharness sessions on this machine.
3
+ *
4
+ * Transport is `src/session-messages.ts`: one append-only JSONL mailbox per
5
+ * session under `~/.devharness/messages/`. Discovery reuses the dashboard
6
+ * hub's session registry, which already holds every live session's short id,
7
+ * pid and project directory.
8
+ */
9
+ import { z } from 'zod';
10
+ import { createTool } from '../validation-helpers.js';
11
+ import { createSuccessResponse, createErrorResponse } from '../messages.js';
12
+ import { getProjectDir } from '../helpers/paths.js';
13
+ import { getSessionInfo } from './dashboard-tools.js';
14
+ import { listSessionRecords } from '../session-endpoint.js';
15
+ import { resolveSessionName } from '../session-identity.js';
16
+ import { appendEvent, getEventStreamPath } from '../session-events.js';
17
+ import { isValidMailboxId, getMailboxPath, getMessagesDir, listMailboxIds, readMailbox, readCursor, writeCursor, sendMessage, waitForMailbox, } from '../session-messages.js';
18
+ const messageSchema = z.object({
19
+ action: z.enum(['sessions', 'send', 'read', 'reply'])
20
+ .describe('sessions (list reachable sessions and this one\'s mailbox path), send (write to another session), read (take new messages from this session\'s mailbox), reply (answer a message by id)'),
21
+ to: z.string().optional()
22
+ .describe('send: recipient mailbox id, as reported by action sessions'),
23
+ text: z.string().optional()
24
+ .describe('send/reply: the message body'),
25
+ replyTo: z.string().optional()
26
+ .describe('reply: id of the message being answered - the reply goes to its sender'),
27
+ waitForReplyMs: z.number().int().positive().max(300000).optional()
28
+ .describe('send/reply: hold the call open until a message arrives in this session\'s mailbox, or this many ms elapse (max 300000). Returns on any arrival, not only a tagged reply.'),
29
+ pollIntervalMs: z.number().int().min(100).max(5000).optional()
30
+ .describe('send/reply: interval between mailbox checks while waiting (default 500)'),
31
+ }).strict();
32
+ const DEFAULT_POLL_INTERVAL_MS = 500;
33
+ function buildMeta(action, self, extra) {
34
+ return {
35
+ tool: 'message',
36
+ action,
37
+ timestamp: Date.now(),
38
+ message: { action, self, ...extra },
39
+ };
40
+ }
41
+ /** One line per message, newest last, for the rendered response. */
42
+ function formatMessages(messages) {
43
+ return messages
44
+ .map(m => `- [${m.id.slice(0, 8)}] from ${m.from}${m.replyTo ? ` (reply to ${m.replyTo.slice(0, 8)})` : ''} at ${m.at}\n ${m.text}`)
45
+ .join('\n');
46
+ }
47
+ export function createMessageTools() {
48
+ return {
49
+ message: createTool('Text between two devharness sessions on this machine. Actions: sessions (who is reachable), send (write to another session, optionally holding the call open for a reply), read (take new messages), reply (answer a message by id).', messageSchema, async (args, abortSignal) => {
50
+ const self = resolveSessionName(getSessionInfo()?.shortId);
51
+ const pollIntervalMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
52
+ switch (args.action) {
53
+ // ---- sessions ------------------------------------------------------
54
+ case 'sessions': {
55
+ // The endpoint records are written by the live child and swept by
56
+ // a pid check, so they carry both identity and liveness. The
57
+ // dashboard hub holds the same sessions but prunes only on a clean
58
+ // socket close, which reports a replaced child as still running.
59
+ const records = listSessionRecords();
60
+ const mailboxIds = await listMailboxIds();
61
+ const rows = [];
62
+ const seen = new Set();
63
+ for (const record of records) {
64
+ const id = record.shortId ?? `pid-${record.pid}`;
65
+ if (seen.has(id))
66
+ continue;
67
+ seen.add(id);
68
+ rows.push({ id, pid: record.pid, cwd: record.cwd, live: true, self: id === self });
69
+ }
70
+ // A mailbox with no session behind it is one that has exited.
71
+ // Listing it keeps a reply to a dead session visible, not silent.
72
+ for (const id of mailboxIds) {
73
+ if (seen.has(id))
74
+ continue;
75
+ seen.add(id);
76
+ rows.push({ id, live: false, self: id === self });
77
+ }
78
+ if (!seen.has(self)) {
79
+ rows.push({ id: self, pid: process.pid, live: true, self: true });
80
+ }
81
+ const response = createSuccessResponse('MESSAGE_SESSIONS', {
82
+ self,
83
+ mailboxPath: getMailboxPath(self),
84
+ messagesDir: getMessagesDir(),
85
+ eventStreamPath: getEventStreamPath(self),
86
+ sessionList: rows
87
+ .map(r => `- ${r.id}${r.self ? ' (this session)' : ''} - ${r.live ? 'live' : 'no process'}${r.cwd ? ` - ${r.cwd}` : ''}`)
88
+ .join('\n'),
89
+ count: rows.length,
90
+ });
91
+ return { ...response, _meta: buildMeta('sessions', self, { sessions: rows }) };
92
+ }
93
+ // ---- send / reply --------------------------------------------------
94
+ case 'send':
95
+ case 'reply': {
96
+ if (!args.text) {
97
+ return createErrorResponse('MESSAGE_INVALID_ARGS', {
98
+ message: `message({ action: '${args.action}' }) requires text.`,
99
+ });
100
+ }
101
+ let target;
102
+ let replyTo;
103
+ if (args.action === 'reply') {
104
+ if (!args.replyTo) {
105
+ return createErrorResponse('MESSAGE_INVALID_ARGS', {
106
+ message: "message({ action: 'reply' }) requires replyTo - the id of the message being answered.",
107
+ });
108
+ }
109
+ const own = await readMailbox(self);
110
+ const original = own.find(m => m.id === args.replyTo || m.id.startsWith(args.replyTo));
111
+ if (!original) {
112
+ return createErrorResponse('MESSAGE_TARGET_UNKNOWN', {
113
+ target: args.replyTo,
114
+ reason: `No message with id "${args.replyTo}" in this session's mailbox`,
115
+ self,
116
+ });
117
+ }
118
+ target = original.from;
119
+ replyTo = original.id;
120
+ }
121
+ else {
122
+ if (!args.to) {
123
+ return createErrorResponse('MESSAGE_INVALID_ARGS', {
124
+ message: "message({ action: 'send' }) requires to - a mailbox id from message({ action: 'sessions' }).",
125
+ });
126
+ }
127
+ target = args.to;
128
+ }
129
+ if (!isValidMailboxId(target)) {
130
+ return createErrorResponse('MESSAGE_TARGET_UNKNOWN', {
131
+ target,
132
+ reason: 'Not a valid mailbox id (letters, digits, dot, dash, underscore; 64 chars max)',
133
+ self,
134
+ });
135
+ }
136
+ if (target === self) {
137
+ return createErrorResponse('MESSAGE_INVALID_ARGS', {
138
+ message: `"${target}" is this session's own mailbox - a message to it would never reach another agent.`,
139
+ });
140
+ }
141
+ // The line count is taken BEFORE the append: a reply that lands
142
+ // between the append and the first poll sits past this offset and
143
+ // is returned, rather than being counted as already-read.
144
+ const fromLine = args.waitForReplyMs ? (await readMailbox(self)).length : 0;
145
+ // Where `read` had got to. A mailbox with unread lines sits behind
146
+ // fromLine, and advancing the cursor past them would drop them.
147
+ const cursorBefore = args.waitForReplyMs ? await readCursor(self) : 0;
148
+ const sent = await sendMessage({
149
+ from: self,
150
+ fromCwd: getProjectDir(),
151
+ to: target,
152
+ text: args.text,
153
+ ...(replyTo ? { replyTo } : {}),
154
+ });
155
+ // The recipient's Monitor watches its event stream, not its
156
+ // mailbox, so the arrival is announced there.
157
+ await appendEvent(target, 'message', {
158
+ from: self,
159
+ id: sent.id,
160
+ detail: `Message from ${self}: ${args.text.slice(0, 120)}`,
161
+ resolve: "message({ action: 'read' })",
162
+ });
163
+ // A typo mints a mailbox nothing ever reads, and nothing removes
164
+ // one, so the sessions listing grows a fictional peer per slip.
165
+ // Refusing would block a genuine first message to a session whose
166
+ // server is suspended, which has neither record nor mailbox, so the
167
+ // state is reported rather than enforced.
168
+ const targetListening = listSessionRecords()
169
+ .some(r => (r.shortId ?? `pid-${r.pid}`) === target);
170
+ if (!args.waitForReplyMs) {
171
+ const response = createSuccessResponse('MESSAGE_SENT', {
172
+ to: target,
173
+ id: sent.id,
174
+ shortId: sent.id.slice(0, 8),
175
+ mailboxPath: getMailboxPath(target),
176
+ deliveryNote: targetListening
177
+ ? ''
178
+ : `\n\n**No session is listening as \`${target}\` right now.** It reads this when it next starts, or the id is a typo and nothing ever will - check \`message({ action: 'sessions' })\`.`,
179
+ });
180
+ return { ...response, _meta: buildMeta(args.action, self, { sent, targetListening }) };
181
+ }
182
+ const waitStart = Date.now();
183
+ const received = await waitForMailbox(self, fromLine, {
184
+ timeoutMs: args.waitForReplyMs,
185
+ pollIntervalMs,
186
+ abortSignal,
187
+ });
188
+ const waitElapsedMs = Date.now() - waitStart;
189
+ if (received.length === 0) {
190
+ return {
191
+ ...createErrorResponse('MESSAGE_REPLY_TIMEOUT', {
192
+ to: target,
193
+ timeoutMs: args.waitForReplyMs,
194
+ id: sent.id.slice(0, 8),
195
+ self,
196
+ mailboxPath: getMailboxPath(self),
197
+ }),
198
+ _meta: buildMeta(args.action, self, { sent, waitTimedOut: true, waitElapsedMs }),
199
+ };
200
+ }
201
+ // The wait consumed these lines, so the cursor moves past them and
202
+ // a later `read` does not hand back the same messages - but only
203
+ // when `read` had already caught up. With unread lines behind
204
+ // fromLine there is no cursor value that means "those unread, these
205
+ // read", so the cursor stays put and `read` returns the backlog
206
+ // together with what the wait already showed. Repeating a message
207
+ // is recoverable; losing one is not.
208
+ if (cursorBefore >= fromLine) {
209
+ await writeCursor(self, fromLine + received.length);
210
+ }
211
+ const response = createSuccessResponse('MESSAGE_REPLY_RECEIVED', {
212
+ to: target,
213
+ count: received.length,
214
+ elapsedMs: waitElapsedMs,
215
+ messageList: formatMessages(received),
216
+ // A cursor cannot express "those unread, these read", so with a
217
+ // backlog behind fromLine it stays put and `read` hands these
218
+ // back a second time. Acting on them twice is the failure that
219
+ // silence here would cause.
220
+ repeatNote: cursorBefore < fromLine
221
+ ? `\n\n**Unread messages sit behind these**, so the cursor did not move: \`message({ action: 'read' })\` returns the backlog AND repeats the message(s) above once. Treat a repeat as already handled.`
222
+ : '',
223
+ });
224
+ return {
225
+ ...response,
226
+ _meta: buildMeta(args.action, self, { sent, received, waitTimedOut: false, waitElapsedMs }),
227
+ };
228
+ }
229
+ // ---- read ----------------------------------------------------------
230
+ case 'read': {
231
+ const all = await readMailbox(self);
232
+ const cursor = await readCursor(self);
233
+ // A mailbox deleted while a cursor survives would slice past the
234
+ // end and return nothing forever; clamp to the current length.
235
+ const from = Math.min(cursor, all.length);
236
+ const fresh = all.slice(from);
237
+ await writeCursor(self, all.length);
238
+ const response = createSuccessResponse('MESSAGE_INBOX', {
239
+ self,
240
+ count: fresh.length,
241
+ total: all.length,
242
+ mailboxPath: getMailboxPath(self),
243
+ messageList: fresh.length ? formatMessages(fresh) : '(nothing new)',
244
+ });
245
+ return { ...response, _meta: buildMeta('read', self, { received: fresh }) };
246
+ }
247
+ }
248
+ }),
249
+ };
250
+ }
251
+ //# sourceMappingURL=message-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-tools.js","sourceRoot":"","sources":["../../src/tools/message-tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tests for the `message` tool and its mailbox transport.
3
+ *
4
+ * Every case runs against a temporary DEVHARNESS_DIR, so the mailboxes are the
5
+ * real files the tool writes in production rather than a mock of them - the
6
+ * cursor and the blocking wait are both defined by what is on disk.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=message-tools.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-tools.test.d.ts","sourceRoot":"","sources":["../../src/tools/message-tools.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Tests for the `message` tool and its mailbox transport.
3
+ *
4
+ * Every case runs against a temporary DEVHARNESS_DIR, so the mailboxes are the
5
+ * real files the tool writes in production rather than a mock of them - the
6
+ * cursor and the blocking wait are both defined by what is on disk.
7
+ */
8
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
9
+ import { mkdtempSync, rmSync, readFileSync, existsSync } from 'fs';
10
+ import { tmpdir } from 'os';
11
+ import { join } from 'path';
12
+ import { initializePaths } from '../helpers/paths.js';
13
+ import { setSessionInfo } from './dashboard-tools.js';
14
+ import { createMessageTools } from './message-tools.js';
15
+ import { isValidMailboxId, getMailboxPath, readMailbox, readCursor, sendMessage, waitForMailbox, } from '../session-messages.js';
16
+ let dir;
17
+ let previousDir;
18
+ let previousSessionId;
19
+ function asSession(shortId) {
20
+ setSessionInfo({
21
+ sessionId: `${shortId}-full-uuid`,
22
+ shortId,
23
+ sessionFile: `/tmp/${shortId}.jsonl`,
24
+ detectedAt: Date.now(),
25
+ });
26
+ }
27
+ const { message } = createMessageTools();
28
+ beforeEach(() => {
29
+ previousDir = process.env.DEVHARNESS_DIR;
30
+ previousSessionId = process.env.CLAUDE_CODE_SESSION_ID;
31
+ dir = mkdtempSync(join(tmpdir(), 'devharness-messages-'));
32
+ process.env.DEVHARNESS_DIR = dir;
33
+ // The real value on the machine running the suite would otherwise decide
34
+ // every mailbox id below.
35
+ process.env.CLAUDE_CODE_SESSION_ID = 'aaaaaaaa-1111-2222-3333-444444444444';
36
+ initializePaths();
37
+ asSession('aaaaaaaa');
38
+ });
39
+ afterEach(() => {
40
+ setSessionInfo(null);
41
+ if (previousDir === undefined)
42
+ delete process.env.DEVHARNESS_DIR;
43
+ else
44
+ process.env.DEVHARNESS_DIR = previousDir;
45
+ if (previousSessionId === undefined)
46
+ delete process.env.CLAUDE_CODE_SESSION_ID;
47
+ else
48
+ process.env.CLAUDE_CODE_SESSION_ID = previousSessionId;
49
+ initializePaths();
50
+ rmSync(dir, { recursive: true, force: true });
51
+ });
52
+ // ---------------------------------------------------------------------------
53
+ // mailbox ids
54
+ // ---------------------------------------------------------------------------
55
+ describe('isValidMailboxId', () => {
56
+ it('accepts the short-id and pid forms the tool produces', () => {
57
+ expect(isValidMailboxId('aaaaaaaa')).toBe(true);
58
+ expect(isValidMailboxId('pid-12345')).toBe(true);
59
+ });
60
+ it('rejects ids that would write outside the messages directory', () => {
61
+ expect(isValidMailboxId('../escape')).toBe(false);
62
+ expect(isValidMailboxId('a/b')).toBe(false);
63
+ expect(isValidMailboxId('..')).toBe(false);
64
+ expect(isValidMailboxId('')).toBe(false);
65
+ });
66
+ });
67
+ // ---------------------------------------------------------------------------
68
+ // transport
69
+ // ---------------------------------------------------------------------------
70
+ describe('mailbox transport', () => {
71
+ it('appends one JSON line per message to the recipient', async () => {
72
+ await sendMessage({ from: 'aaaaaaaa', to: 'bbbbbbbb', text: 'first' });
73
+ await sendMessage({ from: 'aaaaaaaa', to: 'bbbbbbbb', text: 'second' });
74
+ const raw = readFileSync(getMailboxPath('bbbbbbbb'), 'utf-8');
75
+ expect(raw.trimEnd().split('\n')).toHaveLength(2);
76
+ const messages = await readMailbox('bbbbbbbb');
77
+ expect(messages.map(m => m.text)).toEqual(['first', 'second']);
78
+ expect(messages[0].from).toBe('aaaaaaaa');
79
+ expect(messages[0].to).toBe('bbbbbbbb');
80
+ });
81
+ it('skips a torn line instead of losing every message after it', async () => {
82
+ await sendMessage({ from: 'aaaaaaaa', to: 'bbbbbbbb', text: 'good' });
83
+ const { appendFileSync } = await import('fs');
84
+ appendFileSync(getMailboxPath('bbbbbbbb'), '{not json\n');
85
+ await sendMessage({ from: 'aaaaaaaa', to: 'bbbbbbbb', text: 'after' });
86
+ const messages = await readMailbox('bbbbbbbb');
87
+ expect(messages.map(m => m.text)).toEqual(['good', 'after']);
88
+ });
89
+ it('reads an absent mailbox as empty', async () => {
90
+ expect(await readMailbox('nobody')).toEqual([]);
91
+ expect(existsSync(getMailboxPath('nobody'))).toBe(false);
92
+ });
93
+ });
94
+ // ---------------------------------------------------------------------------
95
+ // waitForMailbox
96
+ // ---------------------------------------------------------------------------
97
+ describe('waitForMailbox', () => {
98
+ it('returns the messages past the offset once one lands', async () => {
99
+ const pending = waitForMailbox('aaaaaaaa', 0, { timeoutMs: 5000, pollIntervalMs: 25 });
100
+ setTimeout(() => { void sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'here' }); }, 50);
101
+ const received = await pending;
102
+ expect(received.map(m => m.text)).toEqual(['here']);
103
+ });
104
+ it('returns empty when the timeout elapses with nothing new', async () => {
105
+ const received = await waitForMailbox('aaaaaaaa', 0, { timeoutMs: 150, pollIntervalMs: 25 });
106
+ expect(received).toEqual([]);
107
+ });
108
+ it('ignores messages at or before the offset', async () => {
109
+ await sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'old' });
110
+ const received = await waitForMailbox('aaaaaaaa', 1, { timeoutMs: 150, pollIntervalMs: 25 });
111
+ expect(received).toEqual([]);
112
+ });
113
+ it('throws an abort-shaped error when the signal aborts mid-wait', async () => {
114
+ const controller = new AbortController();
115
+ const pending = waitForMailbox('aaaaaaaa', 0, {
116
+ timeoutMs: 5000,
117
+ pollIntervalMs: 25,
118
+ abortSignal: controller.signal,
119
+ });
120
+ setTimeout(() => controller.abort(), 40);
121
+ await expect(pending).rejects.toMatchObject({ name: 'AbortError' });
122
+ });
123
+ });
124
+ // ---------------------------------------------------------------------------
125
+ // the tool
126
+ // ---------------------------------------------------------------------------
127
+ describe('message send', () => {
128
+ it('writes to the target mailbox and reports the id', async () => {
129
+ const res = await message.handler({ action: 'send', to: 'bbbbbbbb', text: 'hello' });
130
+ expect(res.isError).toBeFalsy();
131
+ expect(res._meta.message.sent.to).toBe('bbbbbbbb');
132
+ expect(res._meta.message.sent.from).toBe('aaaaaaaa');
133
+ expect(res._meta.message.self).toBe('aaaaaaaa');
134
+ const delivered = await readMailbox('bbbbbbbb');
135
+ expect(delivered.map(m => m.text)).toEqual(['hello']);
136
+ });
137
+ it('refuses a message addressed to this session', async () => {
138
+ const res = await message.handler({ action: 'send', to: 'aaaaaaaa', text: 'hello' });
139
+ expect(res.isError).toBe(true);
140
+ expect(res._errorId).toBe('MESSAGE_INVALID_ARGS');
141
+ });
142
+ it('refuses an id that would escape the messages directory', async () => {
143
+ const res = await message.handler({ action: 'send', to: '../escape', text: 'hello' });
144
+ expect(res.isError).toBe(true);
145
+ expect(res._errorId).toBe('MESSAGE_TARGET_UNKNOWN');
146
+ });
147
+ it('requires text', async () => {
148
+ const res = await message.handler({ action: 'send', to: 'bbbbbbbb' });
149
+ expect(res.isError).toBe(true);
150
+ expect(res._errorId).toBe('MESSAGE_INVALID_ARGS');
151
+ });
152
+ });
153
+ describe('message send with waitForReplyMs', () => {
154
+ it('returns once anything lands in this session\'s mailbox', async () => {
155
+ const pending = message.handler({
156
+ action: 'send', to: 'bbbbbbbb', text: 'ping', waitForReplyMs: 5000, pollIntervalMs: 25,
157
+ });
158
+ setTimeout(() => { void sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'pong' }); }, 50);
159
+ const res = await pending;
160
+ expect(res.isError).toBeFalsy();
161
+ expect(res._meta.message.waitTimedOut).toBe(false);
162
+ expect(res._meta.message.received.map((m) => m.text)).toEqual(['pong']);
163
+ });
164
+ it('fails with MESSAGE_REPLY_TIMEOUT when nothing answers', async () => {
165
+ const res = await message.handler({
166
+ action: 'send', to: 'bbbbbbbb', text: 'ping', waitForReplyMs: 150, pollIntervalMs: 25,
167
+ });
168
+ expect(res.isError).toBe(true);
169
+ expect(res._errorId).toBe('MESSAGE_REPLY_TIMEOUT');
170
+ expect(res._meta.message.waitTimedOut).toBe(true);
171
+ });
172
+ it('leaves a message that arrived before the send unconsumed by the wait', async () => {
173
+ await sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'earlier' });
174
+ const res = await message.handler({
175
+ action: 'send', to: 'bbbbbbbb', text: 'ping', waitForReplyMs: 150, pollIntervalMs: 25,
176
+ });
177
+ expect(res._errorId).toBe('MESSAGE_REPLY_TIMEOUT');
178
+ const inbox = await message.handler({ action: 'read' });
179
+ expect(inbox._meta.message.received.map((m) => m.text)).toEqual(['earlier']);
180
+ });
181
+ it('keeps an unread backlog readable when the wait succeeds', async () => {
182
+ await sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'earlier-unread' });
183
+ const pending = message.handler({
184
+ action: 'send', to: 'bbbbbbbb', text: 'ping', waitForReplyMs: 5000, pollIntervalMs: 25,
185
+ });
186
+ setTimeout(() => { void sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'pong' }); }, 50);
187
+ expect((await pending).isError).toBeFalsy();
188
+ // The backlog is what must survive; the waited message repeating is the
189
+ // price of a cursor that cannot express a hole.
190
+ const inbox = await message.handler({ action: 'read' });
191
+ expect(inbox._meta.message.received.map((m) => m.text)).toEqual(['earlier-unread', 'pong']);
192
+ // Exactly once - a second read is empty, not another repeat.
193
+ const again = await message.handler({ action: 'read' });
194
+ expect(again._meta.message.received).toEqual([]);
195
+ });
196
+ it('says in the response that the waited message will come back once', async () => {
197
+ await sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'earlier-unread' });
198
+ const pending = message.handler({
199
+ action: 'send', to: 'bbbbbbbb', text: 'ping', waitForReplyMs: 5000, pollIntervalMs: 25,
200
+ });
201
+ setTimeout(() => { void sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'pong' }); }, 50);
202
+ const res = await pending;
203
+ expect(res.content[0].text).toContain('repeats the message');
204
+ });
205
+ it('says nothing about repeats when the mailbox was already caught up', async () => {
206
+ const pending = message.handler({
207
+ action: 'send', to: 'bbbbbbbb', text: 'ping', waitForReplyMs: 5000, pollIntervalMs: 25,
208
+ });
209
+ setTimeout(() => { void sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'pong' }); }, 50);
210
+ const res = await pending;
211
+ expect(res.content[0].text).not.toContain('repeats the message');
212
+ });
213
+ it('advances the cursor past what the wait returned', async () => {
214
+ const pending = message.handler({
215
+ action: 'send', to: 'bbbbbbbb', text: 'ping', waitForReplyMs: 5000, pollIntervalMs: 25,
216
+ });
217
+ setTimeout(() => { void sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'pong' }); }, 50);
218
+ await pending;
219
+ expect(await readCursor('aaaaaaaa')).toBe(1);
220
+ const inbox = await message.handler({ action: 'read' });
221
+ expect(inbox._meta.message.received).toEqual([]);
222
+ });
223
+ });
224
+ describe('waitForReplyMs shorter than one poll interval', () => {
225
+ it('still polls, rather than timing out before the first check', async () => {
226
+ const pending = message.handler({
227
+ action: 'send', to: 'bbbbbbbb', text: 'ping', waitForReplyMs: 400, pollIntervalMs: 500,
228
+ });
229
+ setTimeout(() => { void sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'pong' }); }, 100);
230
+ const res = await pending;
231
+ expect(res.isError).toBeFalsy();
232
+ expect(res._meta.message.received.map((m) => m.text)).toEqual(['pong']);
233
+ });
234
+ });
235
+ describe('message read', () => {
236
+ it('returns each message once', async () => {
237
+ await sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'one' });
238
+ await sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'two' });
239
+ const first = await message.handler({ action: 'read' });
240
+ expect(first._meta.message.received.map((m) => m.text)).toEqual(['one', 'two']);
241
+ const second = await message.handler({ action: 'read' });
242
+ expect(second._meta.message.received).toEqual([]);
243
+ await sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'three' });
244
+ const third = await message.handler({ action: 'read' });
245
+ expect(third._meta.message.received.map((m) => m.text)).toEqual(['three']);
246
+ });
247
+ it('returns nothing for a session that has received nothing', async () => {
248
+ const res = await message.handler({ action: 'read' });
249
+ expect(res.isError).toBeFalsy();
250
+ expect(res._meta.message.received).toEqual([]);
251
+ });
252
+ });
253
+ describe('message reply', () => {
254
+ it('routes back to the sender of the message being answered', async () => {
255
+ const incoming = await sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'question' });
256
+ await message.handler({ action: 'read' });
257
+ const res = await message.handler({ action: 'reply', replyTo: incoming.id, text: 'answer' });
258
+ expect(res.isError).toBeFalsy();
259
+ const theirs = await readMailbox('bbbbbbbb');
260
+ expect(theirs.map(m => m.text)).toEqual(['answer']);
261
+ expect(theirs[0].replyTo).toBe(incoming.id);
262
+ expect(theirs[0].from).toBe('aaaaaaaa');
263
+ });
264
+ it('accepts the abbreviated id the response prints', async () => {
265
+ const incoming = await sendMessage({ from: 'bbbbbbbb', to: 'aaaaaaaa', text: 'question' });
266
+ const res = await message.handler({
267
+ action: 'reply', replyTo: incoming.id.slice(0, 8), text: 'answer',
268
+ });
269
+ expect(res.isError).toBeFalsy();
270
+ expect((await readMailbox('bbbbbbbb'))[0].replyTo).toBe(incoming.id);
271
+ });
272
+ it('fails when no message in this mailbox carries that id', async () => {
273
+ const res = await message.handler({ action: 'reply', replyTo: 'deadbeef', text: 'answer' });
274
+ expect(res.isError).toBe(true);
275
+ expect(res._errorId).toBe('MESSAGE_TARGET_UNKNOWN');
276
+ });
277
+ it('requires replyTo', async () => {
278
+ const res = await message.handler({ action: 'reply', text: 'answer' });
279
+ expect(res.isError).toBe(true);
280
+ expect(res._errorId).toBe('MESSAGE_INVALID_ARGS');
281
+ });
282
+ });
283
+ describe('message sessions', () => {
284
+ it('lists this session and every mailbox on disk when the hub is down', async () => {
285
+ await sendMessage({ from: 'aaaaaaaa', to: 'bbbbbbbb', text: 'hello' });
286
+ const res = await message.handler({ action: 'sessions' });
287
+ expect(res.isError).toBeFalsy();
288
+ const ids = res._meta.message.sessions.map((s) => s.id).sort();
289
+ expect(ids).toEqual(['aaaaaaaa', 'bbbbbbbb']);
290
+ const self = res._meta.message.sessions.find((s) => s.self);
291
+ expect(self.id).toBe('aaaaaaaa');
292
+ expect(res._meta.message.sessions.find((s) => s.id === 'bbbbbbbb').live).toBe(false);
293
+ });
294
+ });
295
+ describe('mailbox identity', () => {
296
+ it('uses the session id from the environment, with no detection needed', async () => {
297
+ setSessionInfo(null);
298
+ const res = await message.handler({ action: 'read' });
299
+ expect(res._meta.message.self).toBe('aaaaaaaa');
300
+ });
301
+ it('keeps the same id when the child pid changes, which a rebuild does', async () => {
302
+ const before = (await message.handler({ action: 'read' }))._meta.message.self;
303
+ setSessionInfo(null); // the detector's result is lost with the old child
304
+ const after = (await message.handler({ action: 'read' }))._meta.message.self;
305
+ expect(after).toBe(before);
306
+ expect(after).not.toContain(String(process.pid));
307
+ });
308
+ it('prefers the environment over a detector result that disagrees', async () => {
309
+ asSession('zzzzzzzz');
310
+ const res = await message.handler({ action: 'read' });
311
+ expect(res._meta.message.self).toBe('aaaaaaaa');
312
+ });
313
+ it('falls back to the detector when the environment carries no session id', async () => {
314
+ delete process.env.CLAUDE_CODE_SESSION_ID;
315
+ asSession('zzzzzzzz');
316
+ const res = await message.handler({ action: 'read' });
317
+ expect(res._meta.message.self).toBe('zzzzzzzz');
318
+ });
319
+ it('falls back to the pid form when neither source has an id', async () => {
320
+ delete process.env.CLAUDE_CODE_SESSION_ID;
321
+ setSessionInfo(null);
322
+ const res = await message.handler({ action: 'read' });
323
+ expect(res._meta.message.self).toBe(`pid-${process.pid}`);
324
+ });
325
+ it('ignores an environment value that could escape the messages directory', async () => {
326
+ process.env.CLAUDE_CODE_SESSION_ID = '../../escape';
327
+ setSessionInfo(null);
328
+ const res = await message.handler({ action: 'read' });
329
+ expect(res._meta.message.self).toBe(`pid-${process.pid}`);
330
+ });
331
+ });
332
+ //# sourceMappingURL=message-tools.test.js.map