@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3

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 (120) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +636 -613
  4. package/apps/agent-terminal/app.js +125 -65
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +171 -170
  7. package/apps/agent-terminal/panel-host-proof.html +152 -152
  8. package/apps/agent-terminal/panel-proof-content.html +41 -41
  9. package/apps/agent-terminal/right-workspace-dock.css +187 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +623 -519
  11. package/apps/ai-profile-settings-host/index.html +25 -25
  12. package/apps/ai-profile-settings-host/page.css +1 -1
  13. package/apps/ai-profile-settings-host/page.js +113 -113
  14. package/apps/minimal-test-host/README.md +15 -15
  15. package/apps/minimal-test-host/host.css +2 -2
  16. package/apps/minimal-test-host/host.js +35 -35
  17. package/apps/minimal-test-host/index.html +58 -58
  18. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  19. package/docs/sdk/FILE_LAYOUT.md +31 -31
  20. package/docs/sdk/INTEGRATION.md +126 -126
  21. package/docs/sdk/README.md +21 -21
  22. package/docs/sdk/SESSION_EVENTS.md +13 -13
  23. package/docs/sdk/openapi.yaml +8 -1
  24. package/package.json +1 -1
  25. package/packages/client/README.md +29 -29
  26. package/packages/client/index.d.ts +122 -122
  27. package/packages/client/index.js +260 -260
  28. package/packages/client/package.json +2 -2
  29. package/packages/contracts/README.md +9 -9
  30. package/packages/contracts/index.d.ts +519 -517
  31. package/packages/contracts/index.js +312 -312
  32. package/packages/contracts/package.json +1 -1
  33. package/packages/core/README.md +8 -8
  34. package/packages/core/index.d.ts +76 -76
  35. package/packages/core/index.js +222 -222
  36. package/packages/core/package.json +2 -2
  37. package/packages/server/README.md +17 -17
  38. package/packages/server/bin/agent-terminal-server.js +42 -42
  39. package/packages/server/bin/claude-workbench.js +225 -225
  40. package/packages/server/index.d.ts +171 -171
  41. package/packages/server/index.js +230 -230
  42. package/packages/server/lib/agent-discovery.js +133 -133
  43. package/packages/server/lib/agent-registry.js +143 -143
  44. package/packages/server/lib/agent-spawn-env.js +17 -17
  45. package/packages/server/lib/attachment-store.js +78 -78
  46. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  47. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  48. package/packages/server/lib/capture-settings-store.js +35 -35
  49. package/packages/server/lib/claude-history-recovery.js +87 -87
  50. package/packages/server/lib/cli-session-recovery.js +28 -28
  51. package/packages/server/lib/context-resolver.js +55 -55
  52. package/packages/server/lib/conversation-capture.js +51 -51
  53. package/packages/server/lib/delegation-phase-store.js +357 -357
  54. package/packages/server/lib/delegation-store.js +474 -468
  55. package/packages/server/lib/diagnostics.js +32 -32
  56. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  57. package/packages/server/lib/drivers/codex-driver.js +1442 -1442
  58. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  59. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  60. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  61. package/packages/server/lib/event-stream.js +138 -138
  62. package/packages/server/lib/folder-picker.js +623 -623
  63. package/packages/server/lib/http-handler.js +339 -339
  64. package/packages/server/lib/json-file.js +26 -26
  65. package/packages/server/lib/knowledge-context.js +33 -33
  66. package/packages/server/lib/model-detection.js +295 -295
  67. package/packages/server/lib/parent-context.js +44 -0
  68. package/packages/server/lib/profile-store.js +283 -283
  69. package/packages/server/lib/project-store.js +102 -102
  70. package/packages/server/lib/runtime-loader.js +120 -120
  71. package/packages/server/lib/session-archive-store.js +53 -53
  72. package/packages/server/lib/session-history.js +612 -612
  73. package/packages/server/lib/system-doctor.js +125 -125
  74. package/packages/server/lib/ui-preferences-store.js +48 -48
  75. package/packages/server/lib/workbench-service.js +1696 -1675
  76. package/packages/server/package.json +2 -2
  77. package/packages/ui/README.md +30 -30
  78. package/packages/ui/ai-profile-settings.d.ts +45 -45
  79. package/packages/ui/ai-profile-settings.js +283 -283
  80. package/packages/ui/claude-terminal.d.ts +31 -31
  81. package/packages/ui/claude-terminal.js +343 -343
  82. package/packages/ui/index.d.ts +18 -18
  83. package/packages/ui/index.js +51 -51
  84. package/packages/ui/package.json +2 -2
  85. package/packages/ui/theme.css +13 -13
  86. package/src/assets/claude-prompts.json +21 -21
  87. package/src/backend/http/ai-profile-routes.js +61 -61
  88. package/src/backend/http/claude-routes.js +170 -170
  89. package/src/backend/http/node-helpers.js +45 -45
  90. package/src/backend/index.js +21 -21
  91. package/src/backend/lib/ai-adapter.js +144 -144
  92. package/src/backend/lib/ai-profile-store.js +259 -259
  93. package/src/backend/lib/ai-vendor-presets.js +83 -83
  94. package/src/backend/lib/ai-workspace.js +140 -140
  95. package/src/backend/lib/automation-config.js +253 -253
  96. package/src/backend/lib/claude-cli-runner.js +2392 -2392
  97. package/src/backend/lib/data-dir.js +169 -169
  98. package/src/backend/lib/llm-client.js +176 -176
  99. package/src/backend/lib/model-context-windows.js +82 -82
  100. package/src/backend/lib/native-task-control.js +217 -217
  101. package/src/backend/lib/native-task-history-index.js +475 -475
  102. package/src/backend/lib/native-task-history-reader.js +526 -526
  103. package/src/backend/lib/native-task-messages.js +457 -457
  104. package/src/backend/lib/native-task-registry.js +325 -325
  105. package/src/backend/lib/native-task-runtime.js +242 -242
  106. package/src/backend/lib/native-task-trace.js +440 -440
  107. package/src/backend/lib/native-task-transcript.js +393 -393
  108. package/src/backend/lib/prompt-registry.js +166 -166
  109. package/src/backend/lib/runtime-config.js +121 -121
  110. package/src/backend/lib/sdk-session-stream.js +143 -143
  111. package/src/backend/lib/sdk-turn-stream.js +16 -16
  112. package/src/backend/lib/store-scope.js +131 -131
  113. package/src/backend/lib/task-identity.js +104 -104
  114. package/src/backend/lib/task-state-projection.js +58 -58
  115. package/src/index.js +7 -7
  116. package/src/sdk-wave-a.ts +1 -1
  117. package/src/sdk-wave-b.ts +1 -1
  118. package/src/sdk-wave-c.ts +1 -1
  119. package/src/sdk-wave-d.ts +1 -1
  120. package/src/sdk-wave-e.ts +1 -1
@@ -1,393 +1,393 @@
1
- 'use strict';
2
-
3
- // Native task transcript collector - the read side of native task history.
4
- // When a bound subagent reaches a real terminal signal (a SubagentStop hook
5
- // trace, a terminal SDK task status, or the main reply ending while an agent
6
- // task is already terminal), the collector performs ONE public SDK read
7
- // (getSubagentMessages) for that agent, normalizes the snapshot through the
8
- // redaction boundary (native-task-messages + the trace module's credential
9
- // masking), and publishes it as claude/native-task-transcript facts through
10
- // the SAME durable publish gate as every other native fact. It has no
11
- // authority of its own: the agent identity comes exclusively from the
12
- // registry's trusted bindings, the read uses only the installed SDK's public
13
- // entry point with the query's real cwd and the process's inherited config
14
- // root, and dedup keys/cursors are consumed only after the gate proves the
15
- // fact durable. Every scheduled read freezes its notification-time Service
16
- // owner and query context: a later query or a whole Service transfer can
17
- // never redirect, republish or un-drop it. No polling, no private transcript
18
- // parsing, no worker, no process.env mutation, no parent abort on read
19
- // failure.
20
-
21
- const { normalizeTaskMessages } = require('./native-task-messages');
22
- const { createSecretRedactor } = require('./native-task-trace');
23
- const { NATIVE_TASK_HISTORY_READER_LIMITS } = require('./native-task-history-reader');
24
-
25
- const SDK_PAGE_LIMIT = 1000;
26
- const MAX_READ_PAGES = 20;
27
- const CHUNK_BYTES = 256 * 1024;
28
- const CHUNK_ENVELOPE_HEADROOM = 2048;
29
- const MAX_RETRIES = 2;
30
- const RETRY_DELAYS_MS = [250, 1000];
31
- const TERMINAL_STATUSES = new Set(['completed', 'failed', 'stopped']);
32
- // A built chunk must stay servable by the history reader: the reader refuses
33
- // (HTTP 413 ROW_TOO_LARGE) any single row above its page byte cap, so a fact
34
- // that big could never page back. The headroom covers the journal row
35
- // envelope (sequence stamping and framing) wrapped around the event.
36
- const READ_ROW_HEADROOM = 4096;
37
- const MAX_SERVABLE_EVENT_BYTES = Math.max(0, NATIVE_TASK_HISTORY_READER_LIMITS.maxPageBytes - READ_ROW_HEADROOM);
38
-
39
- // Fixed, content-free reason codes for the explicit read-state facts. The
40
- // marker events carry no SDK error text and no paths.
41
- const READ_REASONS = {
42
- apiAbsent: 'NATIVE_TASK_TRANSCRIPT_API_ABSENT',
43
- configRootMismatch: 'NATIVE_TASK_TRANSCRIPT_CONFIG_ROOT_MISMATCH',
44
- cwdMismatch: 'NATIVE_TASK_TRANSCRIPT_CWD_MISMATCH',
45
- readFailed: 'NATIVE_TASK_TRANSCRIPT_READ_FAILED',
46
- redactFailed: 'NATIVE_TASK_TRANSCRIPT_REDACT_FAILED',
47
- normalizeRefused: 'NATIVE_TASK_TRANSCRIPT_NORMALIZE_REFUSED',
48
- entryOversize: 'NATIVE_TASK_TRANSCRIPT_ENTRY_OVERSIZE',
49
- };
50
-
51
- function byteLength(value) { return Buffer.byteLength(JSON.stringify(value)); }
52
-
53
- // Precise Service ownership identity: the runtime epoch and the committer
54
- // closure the read was initiated under. "Some binding currently exists" is
55
- // not an owner - only the exact same epoch AND the exact same committer
56
- // reference (or the very same identity object) counts. A null on either
57
- // side never matches a live binding on the other.
58
- function sameOwner(captured, current) {
59
- if (captured === current) return true;
60
- if (!captured || !current) return false;
61
- return typeof captured.committer === 'function' && typeof current.committer === 'function'
62
- && captured.epoch === current.epoch && captured.committer === current.committer;
63
- }
64
-
65
- function createNativeTaskTranscriptCollector({ isLive, getRuntime, isBound, emit, sdkImport, redactSource, queryContext, ownerIdentity } = {}) {
66
- if (typeof isLive !== 'function' || typeof getRuntime !== 'function' || typeof isBound !== 'function'
67
- || typeof emit !== 'function' || typeof sdkImport !== 'function' || typeof redactSource !== 'function'
68
- || typeof queryContext !== 'function') {
69
- throw new Error('createNativeTaskTranscriptCollector requires live/runtime/binding/emit/sdk/redaction/context accessors');
70
- }
71
- if (ownerIdentity !== undefined && typeof ownerIdentity !== 'function') {
72
- throw new Error('createNativeTaskTranscriptCollector requires the owner identity accessor to be a function when provided');
73
- }
74
- // Per-task publish state. The dedup sets are consumed ONLY after the gate
75
- // accepted a chunk: a refused fact never marks content as published.
76
- const tasks = new Map();
77
- function taskState(taskKey) {
78
- let entry = tasks.get(taskKey);
79
- if (!entry) {
80
- entry = { publishedUuids: new Set(), publishedStepIds: new Set(), publishedModelIds: new Set(), markerReasons: new Set(), emptyPublished: false, inFlight: false, rerun: null, retries: 0 };
81
- tasks.set(taskKey, entry);
82
- }
83
- return entry;
84
- }
85
-
86
- // Trusted lookup: only bindings the registry itself established.
87
- function bindingFor(sdkSessionId, taskId) {
88
- const runtime = getRuntime();
89
- if (!runtime || typeof runtime.agentTranscriptBindings !== 'function') return null;
90
- try {
91
- const bindings = runtime.agentTranscriptBindings();
92
- return bindings.find(binding => binding.sdkSessionId === sdkSessionId && binding.taskId === taskId) || null;
93
- } catch {
94
- return null;
95
- }
96
- }
97
- function terminalBindings(sdkSessionId) {
98
- const runtime = getRuntime();
99
- if (!runtime || typeof runtime.agentTranscriptBindings !== 'function') return [];
100
- try {
101
- return runtime.agentTranscriptBindings().filter(binding => (!sdkSessionId || binding.sdkSessionId === sdkSessionId)
102
- && TERMINAL_STATUSES.has(String(binding.status || '')));
103
- } catch {
104
- return [];
105
- }
106
- }
107
-
108
- // Everything a scheduled read needs is frozen at NOTIFICATION time: the
109
- // precise Service owner, the query context and the redaction source of the
110
- // transport that produced the trigger. A later query (or a whole Service
111
- // transfer) can never rewrite what an already-scheduled read uses; a
112
- // collector that cannot even read one of these captures nothing instead of
113
- // guessing.
114
- function captureFrame(capture) {
115
- const frame = { capture, owner: null, context: null, secrets: null, secretsFailed: false };
116
- try { frame.owner = typeof ownerIdentity === 'function' ? ownerIdentity() : null; } catch { frame.owner = null; }
117
- try { frame.context = queryContext(); } catch { frame.context = null; }
118
- try { frame.secrets = redactSource(); } catch { frame.secrets = null; frame.secretsFailed = true; }
119
- return frame;
120
- }
121
- function currentOwner() {
122
- try { return typeof ownerIdentity === 'function' ? ownerIdentity() : null; } catch { return null; }
123
- }
124
- function ownerValid(frame) { return sameOwner(frame.owner, currentOwner()); }
125
- // The redactor reads only the frame-frozen secrets; a source that failed
126
- // at capture time keeps failing here so the normalizer fails closed.
127
- function frameRedactor(frame) {
128
- return createSecretRedactor(() => {
129
- if (frame.secretsFailed) throw new Error('redaction source unavailable at capture');
130
- return frame.secrets;
131
- });
132
- }
133
-
134
- function schedule(sdkSessionId, taskId, capture) {
135
- if (!isLive()) return;
136
- const binding = bindingFor(sdkSessionId, taskId);
137
- if (!binding) return;
138
- const frame = captureFrame(capture);
139
- const taskKey = JSON.stringify([sdkSessionId, taskId]);
140
- const entry = taskState(taskKey);
141
- if (entry.inFlight) { entry.rerun = frame; return; }
142
- entry.inFlight = true;
143
- void run(taskKey, binding, frame).catch(() => {
144
- const current = tasks.get(taskKey);
145
- if (current) { current.inFlight = false; current.rerun = null; }
146
- });
147
- }
148
-
149
- async function run(taskKey, initialBinding, frame) {
150
- const entry = tasks.get(taskKey);
151
- try {
152
- await attempt(taskKey, initialBinding, frame);
153
- } finally {
154
- const rerunFrame = entry.rerun;
155
- entry.rerun = null;
156
- if (rerunFrame && isLive()) {
157
- // The queued rerun keeps the notification-time frame it was
158
- // requested under: after an ownership transfer it is a stale request
159
- // and stops before any read or publish, while a same-owner repeat
160
- // re-reads normally.
161
- const binding = bindingFor(initialBinding.sdkSessionId, initialBinding.taskId);
162
- if (binding) {
163
- void run(taskKey, binding, rerunFrame).catch(() => {
164
- const current = tasks.get(taskKey);
165
- if (current) { current.inFlight = false; current.rerun = null; }
166
- });
167
- return;
168
- }
169
- }
170
- entry.inFlight = false;
171
- }
172
- }
173
-
174
- // A full snapshot read: bounded explicit pages off the public API until a
175
- // short page (complete) or the read budget (honestly incomplete). The
176
- // query context is the frame-frozen one, never the current holder.
177
- async function readSnapshot(binding, frame) {
178
- const sdk = await sdkImport();
179
- if (!sdk || typeof sdk.getSubagentMessages !== 'function') return { status: 'unavailable', reason: READ_REASONS.apiAbsent };
180
- const context = frame.context;
181
- if (!context || typeof context.cwd !== 'string' || context.cwd === '') return { status: 'unavailable', reason: READ_REASONS.cwdMismatch };
182
- if ((process.env.CLAUDE_CONFIG_DIR || null) !== (context.configRoot || null)) {
183
- return { status: 'unavailable', reason: READ_REASONS.configRootMismatch };
184
- }
185
- const live = bindingFor(binding.sdkSessionId, binding.taskId);
186
- if (!live) return { status: 'stale' };
187
- const messages = [];
188
- let complete = false;
189
- try {
190
- for (let page = 0; page < MAX_READ_PAGES; page += 1) {
191
- const slice = await sdk.getSubagentMessages(binding.sdkSessionId, binding.taskId, {
192
- dir: context.cwd, limit: SDK_PAGE_LIMIT, offset: messages.length,
193
- });
194
- if (!Array.isArray(slice)) throw new Error('non-array page');
195
- messages.push(...slice);
196
- if (slice.length < SDK_PAGE_LIMIT) { complete = true; break; }
197
- }
198
- } catch {
199
- return { status: 'retry' };
200
- }
201
- return { status: 'ok', messages, complete, cwd: context.cwd };
202
- }
203
-
204
- function markerEvent(binding, scope, capture, readStatus, reason) {
205
- return {
206
- type: 'claude/native-task-transcript',
207
- source: 'sdk-public-read',
208
- scope: { storeScope: scope.storeScope, contextId: scope.contextId, parentSessionId: scope.parentSessionId },
209
- sdkSessionId: binding.sdkSessionId,
210
- taskId: binding.taskId,
211
- taskRef: binding.taskRef ? { ...binding.taskRef } : null,
212
- ownerRunId: binding.ownerRunId || null,
213
- capture,
214
- read: { status: readStatus, reason, at: new Date().toISOString() },
215
- entries: [],
216
- toolSteps: [],
217
- usage: { records: [], summary: null },
218
- diagnostics: [],
219
- chunk: { index: 0, count: 0, complete: false },
220
- };
221
- }
222
-
223
- async function attempt(taskKey, binding, frame) {
224
- const entry = tasks.get(taskKey);
225
- // Every read, retry and rerun starts by re-proving the precise Service
226
- // owner it was initiated under; a transferred or unbound owner stops
227
- // here without publishing and without consuming anything.
228
- if (!ownerValid(frame)) return;
229
- let read;
230
- try {
231
- read = await readSnapshot(binding, frame);
232
- } catch {
233
- read = { status: 'retry' };
234
- }
235
- if (read.status === 'stale') return;
236
- if (read.status === 'retry') {
237
- if (entry.retries < MAX_RETRIES && isLive() && ownerValid(frame)) {
238
- entry.retries += 1;
239
- await new Promise(resolve => setTimeout(resolve, RETRY_DELAYS_MS[Math.min(entry.retries - 1, RETRY_DELAYS_MS.length - 1)]));
240
- if (!isLive() || !ownerValid(frame)) { entry.retries = 0; return; }
241
- return attempt(taskKey, binding, frame);
242
- }
243
- entry.retries = 0;
244
- await publishMarker(taskKey, binding, frame, 'failed', READ_REASONS.readFailed);
245
- return;
246
- }
247
- entry.retries = 0;
248
- if (read.status === 'unavailable') {
249
- await publishMarker(taskKey, binding, frame, 'unavailable', read.reason);
250
- return;
251
- }
252
- let normalized;
253
- try {
254
- normalized = normalizeTaskMessages(read.messages, { redact: frameRedactor(frame) });
255
- } catch (error) {
256
- const reason = error && (error.code === 'NATIVE_TASK_MESSAGES_REDACT_REQUIRED' || error.code === 'NATIVE_TASK_MESSAGES_REDACT_FAILED')
257
- ? READ_REASONS.redactFailed
258
- : READ_REASONS.normalizeRefused;
259
- await publishMarker(taskKey, binding, frame, 'failed', reason);
260
- return;
261
- }
262
- await publishChunks(taskKey, binding, frame, normalized, read.complete);
263
- }
264
-
265
- async function publishMarker(taskKey, binding, frame, readStatus, reason) {
266
- const entry = tasks.get(taskKey);
267
- if (entry.markerReasons.has(reason)) return;
268
- const runtime = getRuntime();
269
- const scope = runtime && typeof runtime.scope === 'function' ? runtime.scope() : null;
270
- if (!scope || !isLive() || !isBound() || !ownerValid(frame)) return;
271
- const event = markerEvent(binding, scope, frame.capture, readStatus, reason);
272
- if (emit(event) === true) entry.markerReasons.add(reason);
273
- }
274
-
275
- // Delta chunks: entries never published before, grouped under the byte
276
- // budget, with the steps/usage records whose first contributing entry sits
277
- // in the group. Cursor sets are consumed only after the gate accepted the
278
- // chunk; a refusal stops the whole publish with nothing consumed.
279
- async function publishChunks(taskKey, binding, frame, normalized, snapshotComplete) {
280
- const entry = tasks.get(taskKey);
281
- const runtime = getRuntime();
282
- const scope = runtime && typeof runtime.scope === 'function' ? runtime.scope() : null;
283
- if (!scope || !isLive() || !isBound() || !ownerValid(frame)) return;
284
- const deltaEntries = normalized.entries.filter(item => !entry.publishedUuids.has(item.uuid));
285
- if (deltaEntries.length === 0) {
286
- // An empty first read is an honest fact: the SDK published no messages
287
- // for this agent yet the binding is real and terminal. Published once
288
- // and only before any content ever landed; later still-empty reads add
289
- // nothing.
290
- if (entry.publishedUuids.size > 0 || entry.emptyPublished) return;
291
- const event = baseEvent(binding, scope, frame.capture, normalized, snapshotComplete);
292
- event.chunk = { index: 0, count: 1, complete: snapshotComplete };
293
- if (emit(event) === true) entry.emptyPublished = true;
294
- return;
295
- }
296
- const groups = [];
297
- let current = [];
298
- let currentBytes = CHUNK_ENVELOPE_HEADROOM;
299
- for (const item of deltaEntries) {
300
- const size = byteLength(item);
301
- if (current.length > 0 && currentBytes + size > CHUNK_BYTES) {
302
- groups.push(current);
303
- current = [];
304
- currentBytes = CHUNK_ENVELOPE_HEADROOM;
305
- }
306
- current.push(item);
307
- currentBytes += size;
308
- }
309
- if (current.length > 0) groups.push(current);
310
- const groupUuids = groups.map(group => new Set(group.map(item => item.uuid)));
311
- let chunkIndex = 0;
312
- for (const group of groups) {
313
- if (!isLive() || !isBound() || !ownerValid(frame)) return;
314
- const uuids = groupUuids[chunkIndex];
315
- const steps = normalized.toolSteps.filter(step => !entry.publishedStepIds.has(step.toolUseId)
316
- && stepBelongsToGroup(step, uuids));
317
- const records = normalized.usageRecords.filter(record => !entry.publishedModelIds.has(record.modelMessageId || 'noid:' + record.entryUuids[0])
318
- && record.entryUuids.some(uuid => uuids.has(uuid)));
319
- const event = baseEvent(binding, scope, frame.capture, normalized, snapshotComplete);
320
- event.entries = group;
321
- event.toolSteps = steps;
322
- event.usage = { records, summary: chunkIndex === groups.length - 1 && snapshotComplete ? normalized.usageSummary : null };
323
- event.diagnostics = chunkIndex === groups.length - 1 ? normalized.diagnostics : [];
324
- event.chunk = { index: chunkIndex, count: groups.length, complete: chunkIndex === groups.length - 1 && snapshotComplete };
325
- // A chunk the history reader could never serve - its page byte cap
326
- // refuses a row this large, so the fact could never page back - must
327
- // not land as a fake success: one explicit unavailable marker is the
328
- // honest outcome, no cursor is consumed and publishing stops.
329
- if (byteLength(event) > MAX_SERVABLE_EVENT_BYTES) {
330
- await publishMarker(taskKey, binding, frame, 'unavailable', READ_REASONS.entryOversize);
331
- return;
332
- }
333
- if (emit(event) !== true) return;
334
- for (const item of group) entry.publishedUuids.add(item.uuid);
335
- for (const step of steps) entry.publishedStepIds.add(step.toolUseId);
336
- for (const record of records) entry.publishedModelIds.add(record.modelMessageId || 'noid:' + record.entryUuids[0]);
337
- chunkIndex += 1;
338
- }
339
- }
340
-
341
- function stepBelongsToGroup(step, uuids) {
342
- if (step.useEntryUuids.some(uuid => uuids.has(uuid))) return true;
343
- return step.resultEntryUuid !== null && uuids.has(step.resultEntryUuid);
344
- }
345
-
346
- function baseEvent(binding, scope, capture, normalized, snapshotComplete) {
347
- return {
348
- type: 'claude/native-task-transcript',
349
- source: 'sdk-public-read',
350
- scope: { storeScope: scope.storeScope, contextId: scope.contextId, parentSessionId: scope.parentSessionId },
351
- sdkSessionId: binding.sdkSessionId,
352
- taskId: binding.taskId,
353
- taskRef: binding.taskRef ? { ...binding.taskRef } : null,
354
- ownerRunId: binding.ownerRunId || null,
355
- capture,
356
- read: { status: 'ok', at: new Date().toISOString() },
357
- entries: [],
358
- toolSteps: [],
359
- usage: { records: [], summary: null },
360
- diagnostics: [],
361
- stats: snapshotComplete ? normalized.stats : null,
362
- chunk: { index: 0, count: 0, complete: false },
363
- };
364
- }
365
-
366
- return {
367
- // Real-event triggers. All of them resolve the target through the
368
- // registry's trusted bindings; an unknown or unbound identity schedules
369
- // nothing.
370
- notifyTrace(trace) {
371
- if (!trace || typeof trace !== 'object' || trace.type !== 'claude/native-trace') return;
372
- if (trace.source !== 'sdk-hook' || trace.hookEvent !== 'SubagentStop') return;
373
- const sdkSessionId = typeof trace.sdkSessionId === 'string' ? trace.sdkSessionId : null;
374
- const agentId = typeof trace.nativeAgentId === 'string' ? trace.nativeAgentId : null;
375
- if (sdkSessionId && agentId) schedule(sdkSessionId, agentId, 'subagent-stop');
376
- },
377
- notifySdkMessage(message) {
378
- // Only a real SDK task event (the stream that just moved a registry
379
- // task to a terminal status) is a trigger; prose and other system
380
- // traffic schedule nothing.
381
- if (!message || typeof message !== 'object' || message.type !== 'system') return;
382
- if (typeof message.subtype !== 'string' || !message.subtype.startsWith('task_')) return;
383
- const sdkSessionId = typeof message.session_id === 'string' && message.session_id !== '' ? message.session_id : null;
384
- if (!sdkSessionId) return;
385
- for (const binding of terminalBindings(sdkSessionId)) schedule(binding.sdkSessionId, binding.taskId, 'sdk-terminal');
386
- },
387
- sweep(capture) {
388
- for (const binding of terminalBindings(null)) schedule(binding.sdkSessionId, binding.taskId, capture || 'turn-end-sweep');
389
- },
390
- };
391
- }
392
-
393
- module.exports = { createNativeTaskTranscriptCollector };
1
+ 'use strict';
2
+
3
+ // Native task transcript collector - the read side of native task history.
4
+ // When a bound subagent reaches a real terminal signal (a SubagentStop hook
5
+ // trace, a terminal SDK task status, or the main reply ending while an agent
6
+ // task is already terminal), the collector performs ONE public SDK read
7
+ // (getSubagentMessages) for that agent, normalizes the snapshot through the
8
+ // redaction boundary (native-task-messages + the trace module's credential
9
+ // masking), and publishes it as claude/native-task-transcript facts through
10
+ // the SAME durable publish gate as every other native fact. It has no
11
+ // authority of its own: the agent identity comes exclusively from the
12
+ // registry's trusted bindings, the read uses only the installed SDK's public
13
+ // entry point with the query's real cwd and the process's inherited config
14
+ // root, and dedup keys/cursors are consumed only after the gate proves the
15
+ // fact durable. Every scheduled read freezes its notification-time Service
16
+ // owner and query context: a later query or a whole Service transfer can
17
+ // never redirect, republish or un-drop it. No polling, no private transcript
18
+ // parsing, no worker, no process.env mutation, no parent abort on read
19
+ // failure.
20
+
21
+ const { normalizeTaskMessages } = require('./native-task-messages');
22
+ const { createSecretRedactor } = require('./native-task-trace');
23
+ const { NATIVE_TASK_HISTORY_READER_LIMITS } = require('./native-task-history-reader');
24
+
25
+ const SDK_PAGE_LIMIT = 1000;
26
+ const MAX_READ_PAGES = 20;
27
+ const CHUNK_BYTES = 256 * 1024;
28
+ const CHUNK_ENVELOPE_HEADROOM = 2048;
29
+ const MAX_RETRIES = 2;
30
+ const RETRY_DELAYS_MS = [250, 1000];
31
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'stopped']);
32
+ // A built chunk must stay servable by the history reader: the reader refuses
33
+ // (HTTP 413 ROW_TOO_LARGE) any single row above its page byte cap, so a fact
34
+ // that big could never page back. The headroom covers the journal row
35
+ // envelope (sequence stamping and framing) wrapped around the event.
36
+ const READ_ROW_HEADROOM = 4096;
37
+ const MAX_SERVABLE_EVENT_BYTES = Math.max(0, NATIVE_TASK_HISTORY_READER_LIMITS.maxPageBytes - READ_ROW_HEADROOM);
38
+
39
+ // Fixed, content-free reason codes for the explicit read-state facts. The
40
+ // marker events carry no SDK error text and no paths.
41
+ const READ_REASONS = {
42
+ apiAbsent: 'NATIVE_TASK_TRANSCRIPT_API_ABSENT',
43
+ configRootMismatch: 'NATIVE_TASK_TRANSCRIPT_CONFIG_ROOT_MISMATCH',
44
+ cwdMismatch: 'NATIVE_TASK_TRANSCRIPT_CWD_MISMATCH',
45
+ readFailed: 'NATIVE_TASK_TRANSCRIPT_READ_FAILED',
46
+ redactFailed: 'NATIVE_TASK_TRANSCRIPT_REDACT_FAILED',
47
+ normalizeRefused: 'NATIVE_TASK_TRANSCRIPT_NORMALIZE_REFUSED',
48
+ entryOversize: 'NATIVE_TASK_TRANSCRIPT_ENTRY_OVERSIZE',
49
+ };
50
+
51
+ function byteLength(value) { return Buffer.byteLength(JSON.stringify(value)); }
52
+
53
+ // Precise Service ownership identity: the runtime epoch and the committer
54
+ // closure the read was initiated under. "Some binding currently exists" is
55
+ // not an owner - only the exact same epoch AND the exact same committer
56
+ // reference (or the very same identity object) counts. A null on either
57
+ // side never matches a live binding on the other.
58
+ function sameOwner(captured, current) {
59
+ if (captured === current) return true;
60
+ if (!captured || !current) return false;
61
+ return typeof captured.committer === 'function' && typeof current.committer === 'function'
62
+ && captured.epoch === current.epoch && captured.committer === current.committer;
63
+ }
64
+
65
+ function createNativeTaskTranscriptCollector({ isLive, getRuntime, isBound, emit, sdkImport, redactSource, queryContext, ownerIdentity } = {}) {
66
+ if (typeof isLive !== 'function' || typeof getRuntime !== 'function' || typeof isBound !== 'function'
67
+ || typeof emit !== 'function' || typeof sdkImport !== 'function' || typeof redactSource !== 'function'
68
+ || typeof queryContext !== 'function') {
69
+ throw new Error('createNativeTaskTranscriptCollector requires live/runtime/binding/emit/sdk/redaction/context accessors');
70
+ }
71
+ if (ownerIdentity !== undefined && typeof ownerIdentity !== 'function') {
72
+ throw new Error('createNativeTaskTranscriptCollector requires the owner identity accessor to be a function when provided');
73
+ }
74
+ // Per-task publish state. The dedup sets are consumed ONLY after the gate
75
+ // accepted a chunk: a refused fact never marks content as published.
76
+ const tasks = new Map();
77
+ function taskState(taskKey) {
78
+ let entry = tasks.get(taskKey);
79
+ if (!entry) {
80
+ entry = { publishedUuids: new Set(), publishedStepIds: new Set(), publishedModelIds: new Set(), markerReasons: new Set(), emptyPublished: false, inFlight: false, rerun: null, retries: 0 };
81
+ tasks.set(taskKey, entry);
82
+ }
83
+ return entry;
84
+ }
85
+
86
+ // Trusted lookup: only bindings the registry itself established.
87
+ function bindingFor(sdkSessionId, taskId) {
88
+ const runtime = getRuntime();
89
+ if (!runtime || typeof runtime.agentTranscriptBindings !== 'function') return null;
90
+ try {
91
+ const bindings = runtime.agentTranscriptBindings();
92
+ return bindings.find(binding => binding.sdkSessionId === sdkSessionId && binding.taskId === taskId) || null;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ function terminalBindings(sdkSessionId) {
98
+ const runtime = getRuntime();
99
+ if (!runtime || typeof runtime.agentTranscriptBindings !== 'function') return [];
100
+ try {
101
+ return runtime.agentTranscriptBindings().filter(binding => (!sdkSessionId || binding.sdkSessionId === sdkSessionId)
102
+ && TERMINAL_STATUSES.has(String(binding.status || '')));
103
+ } catch {
104
+ return [];
105
+ }
106
+ }
107
+
108
+ // Everything a scheduled read needs is frozen at NOTIFICATION time: the
109
+ // precise Service owner, the query context and the redaction source of the
110
+ // transport that produced the trigger. A later query (or a whole Service
111
+ // transfer) can never rewrite what an already-scheduled read uses; a
112
+ // collector that cannot even read one of these captures nothing instead of
113
+ // guessing.
114
+ function captureFrame(capture) {
115
+ const frame = { capture, owner: null, context: null, secrets: null, secretsFailed: false };
116
+ try { frame.owner = typeof ownerIdentity === 'function' ? ownerIdentity() : null; } catch { frame.owner = null; }
117
+ try { frame.context = queryContext(); } catch { frame.context = null; }
118
+ try { frame.secrets = redactSource(); } catch { frame.secrets = null; frame.secretsFailed = true; }
119
+ return frame;
120
+ }
121
+ function currentOwner() {
122
+ try { return typeof ownerIdentity === 'function' ? ownerIdentity() : null; } catch { return null; }
123
+ }
124
+ function ownerValid(frame) { return sameOwner(frame.owner, currentOwner()); }
125
+ // The redactor reads only the frame-frozen secrets; a source that failed
126
+ // at capture time keeps failing here so the normalizer fails closed.
127
+ function frameRedactor(frame) {
128
+ return createSecretRedactor(() => {
129
+ if (frame.secretsFailed) throw new Error('redaction source unavailable at capture');
130
+ return frame.secrets;
131
+ });
132
+ }
133
+
134
+ function schedule(sdkSessionId, taskId, capture) {
135
+ if (!isLive()) return;
136
+ const binding = bindingFor(sdkSessionId, taskId);
137
+ if (!binding) return;
138
+ const frame = captureFrame(capture);
139
+ const taskKey = JSON.stringify([sdkSessionId, taskId]);
140
+ const entry = taskState(taskKey);
141
+ if (entry.inFlight) { entry.rerun = frame; return; }
142
+ entry.inFlight = true;
143
+ void run(taskKey, binding, frame).catch(() => {
144
+ const current = tasks.get(taskKey);
145
+ if (current) { current.inFlight = false; current.rerun = null; }
146
+ });
147
+ }
148
+
149
+ async function run(taskKey, initialBinding, frame) {
150
+ const entry = tasks.get(taskKey);
151
+ try {
152
+ await attempt(taskKey, initialBinding, frame);
153
+ } finally {
154
+ const rerunFrame = entry.rerun;
155
+ entry.rerun = null;
156
+ if (rerunFrame && isLive()) {
157
+ // The queued rerun keeps the notification-time frame it was
158
+ // requested under: after an ownership transfer it is a stale request
159
+ // and stops before any read or publish, while a same-owner repeat
160
+ // re-reads normally.
161
+ const binding = bindingFor(initialBinding.sdkSessionId, initialBinding.taskId);
162
+ if (binding) {
163
+ void run(taskKey, binding, rerunFrame).catch(() => {
164
+ const current = tasks.get(taskKey);
165
+ if (current) { current.inFlight = false; current.rerun = null; }
166
+ });
167
+ return;
168
+ }
169
+ }
170
+ entry.inFlight = false;
171
+ }
172
+ }
173
+
174
+ // A full snapshot read: bounded explicit pages off the public API until a
175
+ // short page (complete) or the read budget (honestly incomplete). The
176
+ // query context is the frame-frozen one, never the current holder.
177
+ async function readSnapshot(binding, frame) {
178
+ const sdk = await sdkImport();
179
+ if (!sdk || typeof sdk.getSubagentMessages !== 'function') return { status: 'unavailable', reason: READ_REASONS.apiAbsent };
180
+ const context = frame.context;
181
+ if (!context || typeof context.cwd !== 'string' || context.cwd === '') return { status: 'unavailable', reason: READ_REASONS.cwdMismatch };
182
+ if ((process.env.CLAUDE_CONFIG_DIR || null) !== (context.configRoot || null)) {
183
+ return { status: 'unavailable', reason: READ_REASONS.configRootMismatch };
184
+ }
185
+ const live = bindingFor(binding.sdkSessionId, binding.taskId);
186
+ if (!live) return { status: 'stale' };
187
+ const messages = [];
188
+ let complete = false;
189
+ try {
190
+ for (let page = 0; page < MAX_READ_PAGES; page += 1) {
191
+ const slice = await sdk.getSubagentMessages(binding.sdkSessionId, binding.taskId, {
192
+ dir: context.cwd, limit: SDK_PAGE_LIMIT, offset: messages.length,
193
+ });
194
+ if (!Array.isArray(slice)) throw new Error('non-array page');
195
+ messages.push(...slice);
196
+ if (slice.length < SDK_PAGE_LIMIT) { complete = true; break; }
197
+ }
198
+ } catch {
199
+ return { status: 'retry' };
200
+ }
201
+ return { status: 'ok', messages, complete, cwd: context.cwd };
202
+ }
203
+
204
+ function markerEvent(binding, scope, capture, readStatus, reason) {
205
+ return {
206
+ type: 'claude/native-task-transcript',
207
+ source: 'sdk-public-read',
208
+ scope: { storeScope: scope.storeScope, contextId: scope.contextId, parentSessionId: scope.parentSessionId },
209
+ sdkSessionId: binding.sdkSessionId,
210
+ taskId: binding.taskId,
211
+ taskRef: binding.taskRef ? { ...binding.taskRef } : null,
212
+ ownerRunId: binding.ownerRunId || null,
213
+ capture,
214
+ read: { status: readStatus, reason, at: new Date().toISOString() },
215
+ entries: [],
216
+ toolSteps: [],
217
+ usage: { records: [], summary: null },
218
+ diagnostics: [],
219
+ chunk: { index: 0, count: 0, complete: false },
220
+ };
221
+ }
222
+
223
+ async function attempt(taskKey, binding, frame) {
224
+ const entry = tasks.get(taskKey);
225
+ // Every read, retry and rerun starts by re-proving the precise Service
226
+ // owner it was initiated under; a transferred or unbound owner stops
227
+ // here without publishing and without consuming anything.
228
+ if (!ownerValid(frame)) return;
229
+ let read;
230
+ try {
231
+ read = await readSnapshot(binding, frame);
232
+ } catch {
233
+ read = { status: 'retry' };
234
+ }
235
+ if (read.status === 'stale') return;
236
+ if (read.status === 'retry') {
237
+ if (entry.retries < MAX_RETRIES && isLive() && ownerValid(frame)) {
238
+ entry.retries += 1;
239
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAYS_MS[Math.min(entry.retries - 1, RETRY_DELAYS_MS.length - 1)]));
240
+ if (!isLive() || !ownerValid(frame)) { entry.retries = 0; return; }
241
+ return attempt(taskKey, binding, frame);
242
+ }
243
+ entry.retries = 0;
244
+ await publishMarker(taskKey, binding, frame, 'failed', READ_REASONS.readFailed);
245
+ return;
246
+ }
247
+ entry.retries = 0;
248
+ if (read.status === 'unavailable') {
249
+ await publishMarker(taskKey, binding, frame, 'unavailable', read.reason);
250
+ return;
251
+ }
252
+ let normalized;
253
+ try {
254
+ normalized = normalizeTaskMessages(read.messages, { redact: frameRedactor(frame) });
255
+ } catch (error) {
256
+ const reason = error && (error.code === 'NATIVE_TASK_MESSAGES_REDACT_REQUIRED' || error.code === 'NATIVE_TASK_MESSAGES_REDACT_FAILED')
257
+ ? READ_REASONS.redactFailed
258
+ : READ_REASONS.normalizeRefused;
259
+ await publishMarker(taskKey, binding, frame, 'failed', reason);
260
+ return;
261
+ }
262
+ await publishChunks(taskKey, binding, frame, normalized, read.complete);
263
+ }
264
+
265
+ async function publishMarker(taskKey, binding, frame, readStatus, reason) {
266
+ const entry = tasks.get(taskKey);
267
+ if (entry.markerReasons.has(reason)) return;
268
+ const runtime = getRuntime();
269
+ const scope = runtime && typeof runtime.scope === 'function' ? runtime.scope() : null;
270
+ if (!scope || !isLive() || !isBound() || !ownerValid(frame)) return;
271
+ const event = markerEvent(binding, scope, frame.capture, readStatus, reason);
272
+ if (emit(event) === true) entry.markerReasons.add(reason);
273
+ }
274
+
275
+ // Delta chunks: entries never published before, grouped under the byte
276
+ // budget, with the steps/usage records whose first contributing entry sits
277
+ // in the group. Cursor sets are consumed only after the gate accepted the
278
+ // chunk; a refusal stops the whole publish with nothing consumed.
279
+ async function publishChunks(taskKey, binding, frame, normalized, snapshotComplete) {
280
+ const entry = tasks.get(taskKey);
281
+ const runtime = getRuntime();
282
+ const scope = runtime && typeof runtime.scope === 'function' ? runtime.scope() : null;
283
+ if (!scope || !isLive() || !isBound() || !ownerValid(frame)) return;
284
+ const deltaEntries = normalized.entries.filter(item => !entry.publishedUuids.has(item.uuid));
285
+ if (deltaEntries.length === 0) {
286
+ // An empty first read is an honest fact: the SDK published no messages
287
+ // for this agent yet the binding is real and terminal. Published once
288
+ // and only before any content ever landed; later still-empty reads add
289
+ // nothing.
290
+ if (entry.publishedUuids.size > 0 || entry.emptyPublished) return;
291
+ const event = baseEvent(binding, scope, frame.capture, normalized, snapshotComplete);
292
+ event.chunk = { index: 0, count: 1, complete: snapshotComplete };
293
+ if (emit(event) === true) entry.emptyPublished = true;
294
+ return;
295
+ }
296
+ const groups = [];
297
+ let current = [];
298
+ let currentBytes = CHUNK_ENVELOPE_HEADROOM;
299
+ for (const item of deltaEntries) {
300
+ const size = byteLength(item);
301
+ if (current.length > 0 && currentBytes + size > CHUNK_BYTES) {
302
+ groups.push(current);
303
+ current = [];
304
+ currentBytes = CHUNK_ENVELOPE_HEADROOM;
305
+ }
306
+ current.push(item);
307
+ currentBytes += size;
308
+ }
309
+ if (current.length > 0) groups.push(current);
310
+ const groupUuids = groups.map(group => new Set(group.map(item => item.uuid)));
311
+ let chunkIndex = 0;
312
+ for (const group of groups) {
313
+ if (!isLive() || !isBound() || !ownerValid(frame)) return;
314
+ const uuids = groupUuids[chunkIndex];
315
+ const steps = normalized.toolSteps.filter(step => !entry.publishedStepIds.has(step.toolUseId)
316
+ && stepBelongsToGroup(step, uuids));
317
+ const records = normalized.usageRecords.filter(record => !entry.publishedModelIds.has(record.modelMessageId || 'noid:' + record.entryUuids[0])
318
+ && record.entryUuids.some(uuid => uuids.has(uuid)));
319
+ const event = baseEvent(binding, scope, frame.capture, normalized, snapshotComplete);
320
+ event.entries = group;
321
+ event.toolSteps = steps;
322
+ event.usage = { records, summary: chunkIndex === groups.length - 1 && snapshotComplete ? normalized.usageSummary : null };
323
+ event.diagnostics = chunkIndex === groups.length - 1 ? normalized.diagnostics : [];
324
+ event.chunk = { index: chunkIndex, count: groups.length, complete: chunkIndex === groups.length - 1 && snapshotComplete };
325
+ // A chunk the history reader could never serve - its page byte cap
326
+ // refuses a row this large, so the fact could never page back - must
327
+ // not land as a fake success: one explicit unavailable marker is the
328
+ // honest outcome, no cursor is consumed and publishing stops.
329
+ if (byteLength(event) > MAX_SERVABLE_EVENT_BYTES) {
330
+ await publishMarker(taskKey, binding, frame, 'unavailable', READ_REASONS.entryOversize);
331
+ return;
332
+ }
333
+ if (emit(event) !== true) return;
334
+ for (const item of group) entry.publishedUuids.add(item.uuid);
335
+ for (const step of steps) entry.publishedStepIds.add(step.toolUseId);
336
+ for (const record of records) entry.publishedModelIds.add(record.modelMessageId || 'noid:' + record.entryUuids[0]);
337
+ chunkIndex += 1;
338
+ }
339
+ }
340
+
341
+ function stepBelongsToGroup(step, uuids) {
342
+ if (step.useEntryUuids.some(uuid => uuids.has(uuid))) return true;
343
+ return step.resultEntryUuid !== null && uuids.has(step.resultEntryUuid);
344
+ }
345
+
346
+ function baseEvent(binding, scope, capture, normalized, snapshotComplete) {
347
+ return {
348
+ type: 'claude/native-task-transcript',
349
+ source: 'sdk-public-read',
350
+ scope: { storeScope: scope.storeScope, contextId: scope.contextId, parentSessionId: scope.parentSessionId },
351
+ sdkSessionId: binding.sdkSessionId,
352
+ taskId: binding.taskId,
353
+ taskRef: binding.taskRef ? { ...binding.taskRef } : null,
354
+ ownerRunId: binding.ownerRunId || null,
355
+ capture,
356
+ read: { status: 'ok', at: new Date().toISOString() },
357
+ entries: [],
358
+ toolSteps: [],
359
+ usage: { records: [], summary: null },
360
+ diagnostics: [],
361
+ stats: snapshotComplete ? normalized.stats : null,
362
+ chunk: { index: 0, count: 0, complete: false },
363
+ };
364
+ }
365
+
366
+ return {
367
+ // Real-event triggers. All of them resolve the target through the
368
+ // registry's trusted bindings; an unknown or unbound identity schedules
369
+ // nothing.
370
+ notifyTrace(trace) {
371
+ if (!trace || typeof trace !== 'object' || trace.type !== 'claude/native-trace') return;
372
+ if (trace.source !== 'sdk-hook' || trace.hookEvent !== 'SubagentStop') return;
373
+ const sdkSessionId = typeof trace.sdkSessionId === 'string' ? trace.sdkSessionId : null;
374
+ const agentId = typeof trace.nativeAgentId === 'string' ? trace.nativeAgentId : null;
375
+ if (sdkSessionId && agentId) schedule(sdkSessionId, agentId, 'subagent-stop');
376
+ },
377
+ notifySdkMessage(message) {
378
+ // Only a real SDK task event (the stream that just moved a registry
379
+ // task to a terminal status) is a trigger; prose and other system
380
+ // traffic schedule nothing.
381
+ if (!message || typeof message !== 'object' || message.type !== 'system') return;
382
+ if (typeof message.subtype !== 'string' || !message.subtype.startsWith('task_')) return;
383
+ const sdkSessionId = typeof message.session_id === 'string' && message.session_id !== '' ? message.session_id : null;
384
+ if (!sdkSessionId) return;
385
+ for (const binding of terminalBindings(sdkSessionId)) schedule(binding.sdkSessionId, binding.taskId, 'sdk-terminal');
386
+ },
387
+ sweep(capture) {
388
+ for (const binding of terminalBindings(null)) schedule(binding.sdkSessionId, binding.taskId, capture || 'turn-end-sweep');
389
+ },
390
+ };
391
+ }
392
+
393
+ module.exports = { createNativeTaskTranscriptCollector };