@sanqianx/claude-ai-workbench 0.13.2 → 0.13.4

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 (122) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +652 -613
  4. package/apps/agent-terminal/app.js +143 -67
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +172 -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 +189 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +652 -521
  11. package/apps/agent-terminal/user-question-card.js +68 -0
  12. package/apps/ai-profile-settings-host/index.html +25 -25
  13. package/apps/ai-profile-settings-host/page.css +1 -1
  14. package/apps/ai-profile-settings-host/page.js +113 -113
  15. package/apps/minimal-test-host/README.md +15 -15
  16. package/apps/minimal-test-host/host.css +2 -2
  17. package/apps/minimal-test-host/host.js +35 -35
  18. package/apps/minimal-test-host/index.html +58 -58
  19. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  20. package/docs/sdk/FILE_LAYOUT.md +31 -31
  21. package/docs/sdk/INTEGRATION.md +126 -126
  22. package/docs/sdk/README.md +21 -21
  23. package/docs/sdk/SESSION_EVENTS.md +15 -13
  24. package/docs/sdk/openapi.yaml +16 -1
  25. package/package.json +1 -1
  26. package/packages/client/README.md +29 -29
  27. package/packages/client/index.d.ts +122 -122
  28. package/packages/client/index.js +260 -260
  29. package/packages/client/package.json +2 -2
  30. package/packages/contracts/README.md +9 -9
  31. package/packages/contracts/index.d.ts +519 -517
  32. package/packages/contracts/index.js +312 -312
  33. package/packages/contracts/package.json +1 -1
  34. package/packages/core/README.md +8 -8
  35. package/packages/core/index.d.ts +76 -76
  36. package/packages/core/index.js +222 -222
  37. package/packages/core/package.json +2 -2
  38. package/packages/server/README.md +17 -17
  39. package/packages/server/bin/agent-terminal-server.js +42 -42
  40. package/packages/server/bin/claude-workbench.js +225 -225
  41. package/packages/server/index.d.ts +171 -171
  42. package/packages/server/index.js +230 -230
  43. package/packages/server/lib/agent-discovery.js +133 -133
  44. package/packages/server/lib/agent-registry.js +143 -143
  45. package/packages/server/lib/agent-spawn-env.js +17 -17
  46. package/packages/server/lib/attachment-store.js +78 -78
  47. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  48. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  49. package/packages/server/lib/capture-settings-store.js +35 -35
  50. package/packages/server/lib/claude-history-recovery.js +87 -87
  51. package/packages/server/lib/cli-session-recovery.js +28 -28
  52. package/packages/server/lib/context-resolver.js +55 -55
  53. package/packages/server/lib/conversation-capture.js +51 -51
  54. package/packages/server/lib/delegation-phase-store.js +357 -357
  55. package/packages/server/lib/delegation-store.js +474 -468
  56. package/packages/server/lib/diagnostics.js +32 -32
  57. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  58. package/packages/server/lib/drivers/codex-driver.js +1461 -1442
  59. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  60. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  61. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  62. package/packages/server/lib/event-stream.js +138 -138
  63. package/packages/server/lib/folder-picker.js +623 -623
  64. package/packages/server/lib/http-handler.js +339 -339
  65. package/packages/server/lib/json-file.js +26 -26
  66. package/packages/server/lib/knowledge-context.js +33 -33
  67. package/packages/server/lib/model-detection.js +295 -295
  68. package/packages/server/lib/parent-context.js +44 -0
  69. package/packages/server/lib/profile-store.js +283 -283
  70. package/packages/server/lib/project-store.js +102 -102
  71. package/packages/server/lib/runtime-loader.js +120 -120
  72. package/packages/server/lib/session-archive-store.js +53 -53
  73. package/packages/server/lib/session-history.js +612 -612
  74. package/packages/server/lib/system-doctor.js +125 -125
  75. package/packages/server/lib/ui-preferences-store.js +48 -48
  76. package/packages/server/lib/user-questions.js +67 -0
  77. package/packages/server/lib/workbench-service.js +1696 -1675
  78. package/packages/server/package.json +2 -2
  79. package/packages/ui/README.md +30 -30
  80. package/packages/ui/ai-profile-settings.d.ts +45 -45
  81. package/packages/ui/ai-profile-settings.js +283 -283
  82. package/packages/ui/claude-terminal.d.ts +31 -31
  83. package/packages/ui/claude-terminal.js +343 -343
  84. package/packages/ui/index.d.ts +18 -18
  85. package/packages/ui/index.js +51 -51
  86. package/packages/ui/package.json +2 -2
  87. package/packages/ui/theme.css +13 -13
  88. package/src/assets/claude-prompts.json +21 -21
  89. package/src/backend/http/ai-profile-routes.js +61 -61
  90. package/src/backend/http/claude-routes.js +170 -170
  91. package/src/backend/http/node-helpers.js +45 -45
  92. package/src/backend/index.js +21 -21
  93. package/src/backend/lib/ai-adapter.js +144 -144
  94. package/src/backend/lib/ai-profile-store.js +259 -259
  95. package/src/backend/lib/ai-vendor-presets.js +83 -83
  96. package/src/backend/lib/ai-workspace.js +140 -140
  97. package/src/backend/lib/automation-config.js +253 -253
  98. package/src/backend/lib/claude-cli-runner.js +2408 -2392
  99. package/src/backend/lib/data-dir.js +169 -169
  100. package/src/backend/lib/llm-client.js +176 -176
  101. package/src/backend/lib/model-context-windows.js +82 -82
  102. package/src/backend/lib/native-task-control.js +217 -217
  103. package/src/backend/lib/native-task-history-index.js +475 -475
  104. package/src/backend/lib/native-task-history-reader.js +526 -526
  105. package/src/backend/lib/native-task-messages.js +457 -457
  106. package/src/backend/lib/native-task-registry.js +325 -325
  107. package/src/backend/lib/native-task-runtime.js +242 -242
  108. package/src/backend/lib/native-task-trace.js +440 -440
  109. package/src/backend/lib/native-task-transcript.js +393 -393
  110. package/src/backend/lib/prompt-registry.js +166 -166
  111. package/src/backend/lib/runtime-config.js +121 -121
  112. package/src/backend/lib/sdk-session-stream.js +143 -143
  113. package/src/backend/lib/sdk-turn-stream.js +16 -16
  114. package/src/backend/lib/store-scope.js +131 -131
  115. package/src/backend/lib/task-identity.js +104 -104
  116. package/src/backend/lib/task-state-projection.js +58 -58
  117. package/src/index.js +7 -7
  118. package/src/sdk-wave-a.ts +1 -1
  119. package/src/sdk-wave-b.ts +1 -1
  120. package/src/sdk-wave-c.ts +1 -1
  121. package/src/sdk-wave-d.ts +1 -1
  122. 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 };