@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,475 +1,475 @@
1
- 'use strict';
2
-
3
- // Native task history index - a rebuildable, scope-fixed index over one
4
- // session's durable history rows. Inputs are { offset, endOffset, event }
5
- // records whose byte boundaries a later file reader measured on the real
6
- // line; every accepted row is copied into a small immutable snapshot
7
- // (offsets, kind, roles, traceId, sequence, hash) at observe time - tool
8
- // bodies, results and source text are never stored, and later mutation of
9
- // the caller's record cannot rewrite an accepted fact. Ownership comes
10
- // solely from real claude/native-task-state rows (complete fixed scope,
11
- // sdkSessionId/taskId, canonical NativeTaskRef and a fully consistent
12
- // owner: sdk-launch tuple, ownerBasis, launcher kind, receipt and task
13
- // type); a row that fails any check is rejected whole - it applies no
14
- // status and consumes no row position. Child claude/native-trace rows
15
- // attach by actual (sdkSessionId, nativeAgentId); lifecycle rows such as
16
- // SubagentStop legitimately carry no tool id. Public-read
17
- // claude/native-task-transcript rows attach by the same trusted
18
- // (sdkSessionId, agent id) tuple - for a bound agent the taskId IS the
19
- // receipt agent id - and never create a task or claim ownership. Bash facts
20
- // route by (sdkSessionId, parent nativeAgentId, toolUseId) so identical
21
- // tool ids under different parents never cross. Pending rows hold metadata
22
- // only and are never artificially capped - a complete history rebuilds
23
- // completely. Pure index: no file I/O, no HTTP, no stop, no public cursor.
24
-
25
- const crypto = require('node:crypto');
26
- const { createTaskScope, createNativeTaskRef, nativeTaskKey, sameTaskScope } = require('./task-identity');
27
-
28
- const TASK_EVENT_TYPES = new Set(['claude/task-started', 'claude/task-progress', 'claude/task-updated', 'claude/task-notification']);
29
- const AGENT_TOOL_NAMES = new Set(['Agent', 'Task']);
30
- const TASK_STATUSES = new Set(['pending', 'running', 'completed', 'failed', 'stopped']);
31
- const TERMINAL_STATUSES = new Set(['completed', 'failed', 'stopped']);
32
- const TASK_KIND_BY_TYPE = new Map([['local_agent', 'agent'], ['local_bash', 'local_bash']]);
33
- const STATE_ASSOCIATIONS = new Set(['bound', 'main-command', 'unassigned']);
34
- const MAX_DIAGNOSTICS = 500;
35
- const MAX_SEEN_SIGNATURES = 2000;
36
- const DEFAULT_LIMIT = 100;
37
- const MAX_LIMIT = 1000;
38
-
39
- function id(value) { return typeof value === 'string' && value.trim() !== '' ? value : null; }
40
- function tupleKey() { return JSON.stringify([...arguments]); }
41
- function rowHash(event) { return crypto.createHash('sha256').update(JSON.stringify(event)).digest('hex'); }
42
-
43
- // Full consistency validation of a bound claude/native-task-state claim,
44
- // checked BEFORE anything is applied: canonical ref matching the row and
45
- // constructor scope, registry owner basis, launcher/receipt rules, a known
46
- // task type consistent with the established one, and the canonical
47
- // sdk-launch ownerRunId tuple. Returns a diagnostic code, or null when the
48
- // ownership claim is fully consistent.
49
- function validateBoundState(event, scope, sdkSessionId, taskId, establishedTaskType) {
50
- const ref = createNativeTaskRef(event.taskRef);
51
- if (!ref) return 'state-ref-invalid';
52
- if (!sameTaskScope(ref, scope)) return 'state-ref-scope-mismatch';
53
- if (ref.taskId !== taskId || ref.ownerRunId !== id(event.ownerRunId)) return 'state-ref-inconsistent';
54
- if (event.typeConflict === true) return 'conflicted-type-not-bindable';
55
- const taskType = id(event.taskType);
56
- const taskKind = TASK_KIND_BY_TYPE.get(taskType) || null;
57
- if (!taskKind) return 'unknown-task-type';
58
- if (id(event.taskKind) !== taskKind) return 'state-kind-inconsistent';
59
- if (establishedTaskType !== null && establishedTaskType !== taskType) return 'task-type-conflict-bind';
60
- const owner = event.owner && typeof event.owner === 'object' ? event.owner : null;
61
- if (!owner || id(owner.ownerBasis) !== 'sdk-tool-launch') return 'owner-basis-invalid';
62
- const launcherKind = id(owner.launcherKind);
63
- if (launcherKind !== 'root' && launcherKind !== 'nested') return 'owner-basis-invalid';
64
- const parentNativeAgentId = id(owner.parentNativeAgentId);
65
- const launcherToolUseId = id(owner.launcherToolUseId);
66
- if (!launcherToolUseId) return 'owner-basis-invalid';
67
- if ((launcherKind === 'root') !== (parentNativeAgentId === null)) return 'launcher-kind-inconsistent';
68
- if (taskKind === 'local_bash' && launcherKind !== 'nested') return 'launcher-kind-inconsistent';
69
- const receiptAgentId = id(owner.receiptAgentId);
70
- if (taskKind === 'agent' && receiptAgentId !== taskId) return 'receipt-task-mismatch';
71
- if (taskKind === 'local_bash' && receiptAgentId !== null && receiptAgentId !== taskId) return 'receipt-task-mismatch';
72
- let tuple = null;
73
- try { tuple = JSON.parse(event.ownerRunId); } catch (error) { tuple = null; }
74
- const tupleOk = Array.isArray(tuple) && tuple.length === 5 && tuple[0] === 'sdk-launch' && tuple[1] === 1
75
- && tuple[2] === sdkSessionId && tuple[3] === parentNativeAgentId && tuple[4] === launcherToolUseId;
76
- if (!tupleOk) return 'owner-run-id-invalid';
77
- return null;
78
- }
79
-
80
- class NativeTaskHistoryIndex {
81
- constructor(scopeInput) {
82
- const scope = createTaskScope(scopeInput);
83
- if (!scope) throw new Error('NativeTaskHistoryIndex requires a valid fixed TaskScope');
84
- this._scope = Object.freeze(scope);
85
- this._tasks = new Map();
86
- this._byRefKey = new Map();
87
- this._agents = new Map();
88
- this._bashByTool = new Map();
89
- this._seenRows = new Map();
90
- this._pendingTraces = new Map();
91
- this._pendingDispatch = new Map();
92
- this._pendingBash = new Map();
93
- this._pendingRefs = 0;
94
- this._diagnostics = [];
95
- this._diagSeen = new Set();
96
- this._stats = { rowsObserved: 0, rowsReplayed: 0, rowsIndexed: 0, rowsUnattributed: 0, diagnostics: 0 };
97
- }
98
-
99
- get scope() { return { storeScope: this._scope.storeScope, contextId: this._scope.contextId, parentSessionId: this._scope.parentSessionId }; }
100
- diagnostics() { return this._diagnostics.map(d => ({ ...d })); }
101
-
102
- // One durable row. Returns the number of NEW per-task view positions the
103
- // row produced; rows held pending until their real binding state arrives
104
- // count zero here. Everything the index keeps about the row is copied
105
- // into one immutable snap before routing, so a later mutation of the
106
- // caller's record cannot rewrite an accepted fact. Replaying a
107
- // byte-identical row is a no-op; the same offset with a different range
108
- // or content is an explicit conflict.
109
- observe(record) {
110
- this._stats.rowsObserved++;
111
- if (!record || typeof record !== 'object' || !Number.isSafeInteger(record.offset) || record.offset <= 0
112
- || !Number.isSafeInteger(record.endOffset) || record.endOffset <= record.offset
113
- || !record.event || typeof record.event !== 'object') {
114
- this._diag('invalid-record', {}); return 0;
115
- }
116
- const event = record.event;
117
- if (event.type !== 'claude/native-task-state' && event.type !== 'claude/native-trace'
118
- && event.type !== 'claude/native-task-transcript' && !TASK_EVENT_TYPES.has(event.type)) return 0;
119
- const hash = rowHash(event);
120
- const seen = this._seenRows.get(record.offset);
121
- if (seen) {
122
- this._stats.rowsReplayed++;
123
- if (seen.endOffset !== record.endOffset) this._diag('row-range-conflict', { offset: record.offset, existingEnd: seen.endOffset, incomingEnd: record.endOffset });
124
- else if (seen.hash !== hash) this._diag('row-content-conflict', { offset: record.offset });
125
- return 0;
126
- }
127
- this._seenRows.set(record.offset, { endOffset: record.endOffset, hash });
128
- const snap = Object.freeze({ offset: record.offset, endOffset: record.endOffset, kind: event.type, traceId: id(event.traceId), sequence: Number(event.sequence) || null, hash });
129
- let added = 0;
130
- if (event.type === 'claude/native-task-state') added = this._observeState(event, snap);
131
- else if (event.type === 'claude/native-trace') added = this._observeTrace(event, snap);
132
- else if (event.type === 'claude/native-task-transcript') added = this._observeTranscript(event, snap);
133
- else added = this._observeTaskEvent(event, snap);
134
- if (added === 0) this._stats.rowsUnattributed++;
135
- return added;
136
- }
137
-
138
- // Only a real claude/native-task-state row with the complete fixed scope,
139
- // a canonical NativeTaskRef and a fully consistent owner establishes
140
- // ownership; anything insufficient stays explicitly unassigned. Every
141
- // check runs to completion BEFORE anything is applied, so a rejected row
142
- // applies no status, attaches no position, claims no agent and creates
143
- // no task.
144
- _observeState(event, snap) {
145
- const sdkSessionId = id(event.sdkSessionId);
146
- const taskId = id(event.taskId);
147
- if (this._explicitScopeConflict(event) || !sameTaskScope(event.scope, this._scope)) {
148
- this._diag('state-scope-mismatch', { sdkSessionId, taskId });
149
- return 0;
150
- }
151
- if (!sdkSessionId || !taskId) { this._diag('state-missing-identity', { hasSession: sdkSessionId !== null, hasTask: taskId !== null }); return 0; }
152
- const association = id(event.association);
153
- if (!STATE_ASSOCIATIONS.has(association)) {
154
- this._diag('unknown-association', { sdkSessionId, taskId, association: association === null ? null : association.slice(0, 40) });
155
- return 0;
156
- }
157
- const taskKey = JSON.stringify([sdkSessionId, taskId]);
158
- const existing = this._tasks.get(taskKey) || null;
159
- if (association !== 'bound') {
160
- if (existing && existing.association === 'bound') {
161
- this._diag('association-conflict', { sdkSessionId, taskId, prior: 'bound', incoming: association });
162
- return 0;
163
- }
164
- const task = this._ensureTask(sdkSessionId, taskId);
165
- const added = this._attach(task, snap, 'state');
166
- this._applyStatus(task, event);
167
- this._applyType(task, event);
168
- task.association = association;
169
- if (association === 'main-command') {
170
- task.mainToolUseId = id(event.mainToolUseId) || id(event.toolUseId) || task.mainToolUseId;
171
- this._registerBash(sdkSessionId, null, task.mainToolUseId, task);
172
- }
173
- return added;
174
- }
175
- const err = validateBoundState(event, this._scope, sdkSessionId, taskId, existing ? existing.taskType : null);
176
- if (err) { this._diag(err, { sdkSessionId, taskId }); return 0; }
177
- const ref = createNativeTaskRef(event.taskRef);
178
- const refKey = nativeTaskKey(ref);
179
- if (existing && existing.association === 'bound') {
180
- // An owner, once bound, is immutable: a later different owner is a
181
- // diagnosed conflict that changes nothing, never a rewrite.
182
- if (existing.ownerRunId !== ref.ownerRunId) {
183
- this._diag('owner-conflict', { sdkSessionId, taskId, existing: existing.ownerRunId, incoming: ref.ownerRunId });
184
- return 0;
185
- }
186
- } else {
187
- const claimed = this._byRefKey.get(refKey);
188
- if (claimed && claimed !== taskKey) { this._diag('owner-ref-collision', { sdkSessionId, taskId }); return 0; }
189
- }
190
- const task = this._ensureTask(sdkSessionId, taskId);
191
- if (task.association !== 'bound') {
192
- task.association = 'bound';
193
- task.taskRef = ref;
194
- task.ownerRunId = ref.ownerRunId;
195
- const source = event.owner && typeof event.owner === 'object' ? event.owner : {};
196
- task.owner = { ownerBasis: id(source.ownerBasis), launcherKind: id(source.launcherKind), parentNativeAgentId: id(source.parentNativeAgentId), launcherToolUseId: id(source.launcherToolUseId), receiptAgentId: id(source.receiptAgentId) };
197
- this._byRefKey.set(refKey, task.taskKey);
198
- }
199
- const added = this._attach(task, snap, 'state');
200
- this._applyStatus(task, event);
201
- this._applyType(task, event);
202
- this._flushPendingFor(task);
203
- return added;
204
- }
205
-
206
- // Any explicitly present session/scope field that contradicts the fixed
207
- // scope rejects the whole row; absent fields stay absent - the index
208
- // never guesses a scope from silence. Returns the conflicting field
209
- // name, or null when every explicit field agrees.
210
- _explicitScopeConflict(event) {
211
- if (event.parentSessionId !== undefined && id(event.parentSessionId) !== this._scope.parentSessionId) return 'parentSessionId';
212
- if (event.contextId !== undefined && id(event.contextId) !== this._scope.contextId) return 'contextId';
213
- if (event.storeScope !== undefined && id(event.storeScope) !== this._scope.storeScope) return 'storeScope';
214
- if (event.scope !== undefined) {
215
- if (!event.scope || typeof event.scope !== 'object') return 'scope';
216
- if (id(event.scope.storeScope) !== this._scope.storeScope) return 'storeScope';
217
- if (id(event.scope.contextId) !== this._scope.contextId) return 'contextId';
218
- if (id(event.scope.parentSessionId) !== this._scope.parentSessionId) return 'parentSessionId';
219
- }
220
- return null;
221
- }
222
-
223
- // Child claude/native-trace rows: the actor (nativeAgentId) is the agent
224
- // in whose context the event happened - its own tools, its lifecycle
225
- // rows and the nested dispatch tools it launched all belong to that
226
- // dispatcher. Lifecycle rows (SubagentStart/SubagentStop) legitimately
227
- // carry no tool id; tool rows (PreToolUse/PostToolUse) must carry one.
228
- // A root Agent/Task PostToolUse receipt only identifies the spawned
229
- // agent: it is dispatch metadata for that agent, never the agent's own
230
- // executed tool. A Bash fact feeds both the parent agent view and the
231
- // command view, keyed by the REAL parent (root explicitly null) so
232
- // identical tool ids under different parents never cross; each view
233
- // holds the offset exactly once.
234
- _observeTrace(event, snap) {
235
- if (event.source !== 'sdk-hook') return 0;
236
- const sdkSessionId = id(event.sdkSessionId);
237
- if (!sdkSessionId) { this._diag('trace-missing-session', {}); return 0; }
238
- const conflict = this._explicitScopeConflict(event);
239
- if (conflict) {
240
- this._diag(conflict === 'parentSessionId' ? 'trace-parent-session-mismatch' : 'trace-scope-mismatch', { sdkSessionId, field: conflict });
241
- return 0;
242
- }
243
- const rawId = id(event.rawToolUseId);
244
- const callbackId = id(event.callbackToolUseId);
245
- if (rawId && callbackId && rawId !== callbackId) { this._diag('trace-id-mismatch', { sdkSessionId }); return 0; }
246
- const toolUseId = rawId || callbackId;
247
- const hookEvent = id(event.hookEvent);
248
- if ((hookEvent === 'PreToolUse' || hookEvent === 'PostToolUse') && !toolUseId) {
249
- this._diag('trace-missing-tool-use-id', { sdkSessionId, hookEvent });
250
- return 0;
251
- }
252
- const actor = id(event.nativeAgentId);
253
- const toolName = id(event.toolName);
254
- const response = event.toolResponse && typeof event.toolResponse === 'object' ? event.toolResponse : null;
255
- const receiptAgentId = response ? id(response.agentId) : null;
256
- let added = 0;
257
- if (actor) {
258
- const taskKey = this._agents.get(tupleKey(sdkSessionId, actor));
259
- if (taskKey) added += this._attach(this._tasks.get(taskKey), snap, 'trace');
260
- else this._pend(this._pendingTraces, tupleKey(sdkSessionId, actor), snap);
261
- } else if (receiptAgentId && AGENT_TOOL_NAMES.has(toolName) && hookEvent === 'PostToolUse') {
262
- const taskKey = this._agents.get(tupleKey(sdkSessionId, receiptAgentId));
263
- if (taskKey) added += this._attach(this._tasks.get(taskKey), snap, 'dispatch');
264
- else this._pend(this._pendingDispatch, tupleKey(sdkSessionId, receiptAgentId), snap);
265
- }
266
- if (toolName === 'Bash') {
267
- if (toolUseId) added += this._attachBash(sdkSessionId, actor, toolUseId, snap);
268
- else this._diag('bash-trace-missing-tool-use-id', { sdkSessionId, hookEvent });
269
- }
270
- return added;
271
- }
272
-
273
- // Public-read transcript snapshots (claude/native-task-transcript). The
274
- // row attaches to the bound agent task it was read for, routed by the
275
- // same trusted (sdkSessionId, agent id) tuple the traces use - for a
276
- // bound agent the row's taskId IS the receipt agent id. A transcript row
277
- // never creates a task, never claims ownership, never applies a status:
278
- // it is content evidence under an ownership someone else already proved.
279
- // A row with no binding yet waits pending exactly like a trace row.
280
- _observeTranscript(event, snap) {
281
- if (event.source !== 'sdk-public-read') return 0;
282
- const sdkSessionId = id(event.sdkSessionId);
283
- const taskId = id(event.taskId);
284
- if (!sdkSessionId || !taskId) { this._diag('transcript-missing-identity', { hasSession: sdkSessionId !== null, hasTask: taskId !== null }); return 0; }
285
- const conflict = this._explicitScopeConflict(event);
286
- if (conflict) { this._diag('transcript-scope-mismatch', { sdkSessionId, taskId, field: conflict }); return 0; }
287
- const taskKey = this._agents.get(tupleKey(sdkSessionId, taskId));
288
- if (taskKey) return this._attach(this._tasks.get(taskKey), snap, 'transcript');
289
- this._pend(this._pendingTraces, tupleKey(sdkSessionId, taskId), snap);
290
- return 0;
291
- }
292
-
293
- _attachBash(sdkSessionId, parentNativeAgentId, toolUseId, snap) {
294
- const key = tupleKey(sdkSessionId, parentNativeAgentId, toolUseId);
295
- const taskKey = this._bashByTool.get(key);
296
- if (taskKey) return this._attach(this._tasks.get(taskKey), snap, 'trace');
297
- this._pend(this._pendingBash, key, snap);
298
- return 0;
299
- }
300
-
301
- // Pending rows keep only their immutable snap - offsets, hash and the
302
- // routing decision made at observe time. There is deliberately no cap:
303
- // an artificially truncated pending set would silently turn a complete
304
- // history into an incomplete one.
305
- _pend(map, key, snap) {
306
- const list = map.get(key) || [];
307
- list.push(snap);
308
- map.set(key, list);
309
- this._pendingRefs++;
310
- }
311
-
312
- // Canonical SDK task events (claude/task-*). Their sdkSessionId/taskId
313
- // are real facts once present; rows without them are insufficient old
314
- // data and never guess an identity. status may be an explicit null from
315
- // the reviewed no-fake-success semantics.
316
- _observeTaskEvent(event, snap) {
317
- const sdkSessionId = id(event.sdkSessionId);
318
- const taskId = id(event.taskId);
319
- if (!sdkSessionId || !taskId) { this._diag('task-event-missing-identity', { type: event.type }); return 0; }
320
- if (id(event.parentSessionId) !== null && event.parentSessionId !== this._scope.parentSessionId) {
321
- this._diag('task-event-scope-mismatch', { type: event.type, sdkSessionId, taskId });
322
- return 0;
323
- }
324
- const task = this._ensureTask(sdkSessionId, taskId);
325
- this._applyStatus(task, event);
326
- if (id(event.taskType) !== null) this._applyType(task, event);
327
- return this._attach(task, snap, 'task-event');
328
- }
329
-
330
- _applyStatus(task, event) {
331
- const raw = id(event.status);
332
- const source = id(event.statusSource) || id(event.type);
333
- if (raw === null) return;
334
- if (!TASK_STATUSES.has(raw)) { this._diag('unknown-task-status', { sdkSessionId: task.sdkSessionId, taskId: task.taskId, raw: raw.slice(0, 40) }); return; }
335
- const prior = task.status;
336
- if (prior === null) { task.status = raw; task.statusSource = source; return; }
337
- if (TERMINAL_STATUSES.has(prior)) {
338
- if (!TERMINAL_STATUSES.has(raw)) { this._diag('terminal-no-revival', { sdkSessionId: task.sdkSessionId, taskId: task.taskId, incoming: raw }); return; }
339
- if (raw !== prior) { this._diag('terminal-conflict', { sdkSessionId: task.sdkSessionId, taskId: task.taskId, prior, incoming: raw }); return; }
340
- return;
341
- }
342
- task.status = raw; task.statusSource = source;
343
- }
344
-
345
- _applyType(task, event) {
346
- const taskType = id(event.taskType);
347
- if (taskType === null) return;
348
- if (task.taskType === null) {
349
- task.taskType = taskType;
350
- task.taskKind = TASK_KIND_BY_TYPE.get(taskType) || null;
351
- } else if (taskType !== task.taskType) {
352
- task.typeConflict = true;
353
- this._diag('task-type-conflict', { sdkSessionId: task.sdkSessionId, taskId: task.taskId, existing: task.taskType, incoming: taskType });
354
- }
355
- }
356
-
357
- _ensureTask(sdkSessionId, taskId) {
358
- const taskKey = JSON.stringify([sdkSessionId, taskId]);
359
- let task = this._tasks.get(taskKey);
360
- if (!task) {
361
- task = { taskKey, sdkSessionId, taskId, taskKind: null, taskType: null, typeConflict: false, status: null, statusSource: null, association: 'unassigned', taskRef: null, ownerRunId: null, owner: null, mainToolUseId: null, view: new Map() };
362
- this._tasks.set(taskKey, task);
363
- }
364
- return task;
365
- }
366
-
367
- _attach(task, snap, role) {
368
- if (!task) return 0;
369
- let entry = task.view.get(snap.offset);
370
- if (!entry) {
371
- entry = { offset: snap.offset, endOffset: snap.endOffset, kind: snap.kind, roles: new Set([role]), traceId: snap.traceId, sequence: snap.sequence, hash: snap.hash };
372
- task.view.set(snap.offset, entry);
373
- this._stats.rowsIndexed++;
374
- return 1;
375
- }
376
- entry.roles.add(role);
377
- return 0;
378
- }
379
-
380
- _flushPendingFor(task) {
381
- if (task.association !== 'bound') return;
382
- if (task.taskKind === 'agent' && task.owner && task.owner.receiptAgentId) {
383
- const key = tupleKey(task.sdkSessionId, task.owner.receiptAgentId);
384
- const claimant = this._agents.get(key);
385
- if (claimant && claimant !== task.taskKey) { this._diag('agent-claim-conflict', { sdkSessionId: task.sdkSessionId, agentId: task.owner.receiptAgentId }); return; }
386
- this._agents.set(key, task.taskKey);
387
- this._flushList(this._pendingTraces, key, task, 'trace');
388
- this._flushList(this._pendingDispatch, key, task, 'dispatch');
389
- }
390
- if (task.taskKind === 'local_bash' && task.owner) {
391
- this._registerBash(task.sdkSessionId, task.owner.parentNativeAgentId, task.owner.launcherToolUseId, task);
392
- }
393
- }
394
-
395
- _flushList(map, key, task, role) {
396
- const list = map.get(key);
397
- if (!list) return;
398
- map.delete(key);
399
- this._pendingRefs -= list.length;
400
- for (const snap of list) this._attach(task, snap, role);
401
- }
402
-
403
- // First real claim on (sdkSessionId, parent nativeAgentId, toolUseId)
404
- // wins - a root command claims with an explicit null parent. A different
405
- // later claimant is a diagnosed conflict, never a silent re-route.
406
- _registerBash(sdkSessionId, parentNativeAgentId, toolUseId, task) {
407
- if (!toolUseId) return;
408
- const key = tupleKey(sdkSessionId, parentNativeAgentId, toolUseId);
409
- const existing = this._bashByTool.get(key);
410
- if (existing && existing !== task.taskKey) { this._diag('bash-claim-conflict', { sdkSessionId, parentNativeAgentId, toolUseId }); return; }
411
- this._bashByTool.set(key, task.taskKey);
412
- this._flushList(this._pendingBash, key, task, 'trace');
413
- }
414
-
415
- // Metadata-only task snapshots; historical 'running' is a past
416
- // observation and never a claim that a current process is still alive.
417
- listTasks() {
418
- const out = [];
419
- for (const task of this._tasks.values()) {
420
- let first = null, last = null;
421
- for (const offset of task.view.keys()) {
422
- if (first === null || offset < first) first = offset;
423
- if (last === null || offset > last) last = offset;
424
- }
425
- out.push({ sdkSessionId: task.sdkSessionId, taskId: task.taskId, taskKind: task.taskKind, taskType: task.taskType, typeConflict: task.typeConflict === true, status: task.status, statusSource: task.statusSource, association: task.association, taskRef: task.taskRef, ownerRunId: task.ownerRunId, owner: task.owner, mainToolUseId: task.mainToolUseId, rowCount: task.view.size, firstOffset: first, lastOffset: last });
426
- }
427
- return JSON.parse(JSON.stringify(out));
428
- }
429
-
430
- // Internal byte-position pagination over one task's original rows, newest
431
- // window first and ascending within a page. `before` is an exclusive raw
432
- // offset boundary inside the same file era - NOT a public opaque cursor;
433
- // stream sequences are never treated as positions.
434
- listOffsets(taskRef, { before, limit } = {}) {
435
- const ref = createNativeTaskRef(taskRef);
436
- if (!ref) throw Object.assign(new Error('invalid native task reference'), { code: 'INVALID_TASK_REF' });
437
- if (!sameTaskScope(ref, this._scope)) throw Object.assign(new Error('task reference scope does not match this index'), { code: 'SCOPE_MISMATCH' });
438
- const size = limit === undefined ? DEFAULT_LIMIT : limit;
439
- if (!Number.isSafeInteger(size) || size < 1 || size > MAX_LIMIT) throw Object.assign(new Error('limit must be between 1 and ' + MAX_LIMIT), { code: 'INVALID_LIMIT' });
440
- let bound = null;
441
- if (before !== undefined && before !== null) {
442
- if (!Number.isSafeInteger(before) || before <= 0) throw Object.assign(new Error('invalid before offset'), { code: 'INVALID_BEFORE' });
443
- bound = before;
444
- }
445
- const taskKey = this._byRefKey.get(nativeTaskKey(ref));
446
- if (!taskKey) return { entries: [], nextBefore: null };
447
- const task = this._tasks.get(taskKey);
448
- const offsets = [...task.view.keys()].sort((a, b) => a - b).filter(offset => bound === null || offset < bound);
449
- const start = Math.max(0, offsets.length - size);
450
- const page = offsets.slice(start);
451
- return { entries: page.map(offset => this._entrySnapshot(task.view.get(offset))), nextBefore: start > 0 && page.length > 0 ? page[0] : null };
452
- }
453
-
454
- _entrySnapshot(entry) {
455
- return { offset: entry.offset, endOffset: entry.endOffset, kind: entry.kind, roles: [...entry.roles].sort(), traceId: entry.traceId, sequence: entry.sequence, hash: entry.hash };
456
- }
457
-
458
- stats() {
459
- let bound = 0;
460
- for (const task of this._tasks.values()) if (task.association === 'bound') bound++;
461
- return { ...this._stats, tasksTotal: this._tasks.size, tasksBound: bound, tasksUnassigned: this._tasks.size - bound, pendingRefs: this._pendingRefs };
462
- }
463
-
464
- _diag(code, ids) {
465
- this._stats.diagnostics++;
466
- const signature = code + '|' + JSON.stringify(ids || {});
467
- if (this._diagSeen.has(signature)) return;
468
- if (this._diagSeen.size >= MAX_SEEN_SIGNATURES) return;
469
- this._diagSeen.add(signature);
470
- if (this._diagnostics.length >= MAX_DIAGNOSTICS) return;
471
- this._diagnostics.push(Object.assign({ code }, ids || {}));
472
- }
473
- }
474
-
475
- module.exports = { NativeTaskHistoryIndex };
1
+ 'use strict';
2
+
3
+ // Native task history index - a rebuildable, scope-fixed index over one
4
+ // session's durable history rows. Inputs are { offset, endOffset, event }
5
+ // records whose byte boundaries a later file reader measured on the real
6
+ // line; every accepted row is copied into a small immutable snapshot
7
+ // (offsets, kind, roles, traceId, sequence, hash) at observe time - tool
8
+ // bodies, results and source text are never stored, and later mutation of
9
+ // the caller's record cannot rewrite an accepted fact. Ownership comes
10
+ // solely from real claude/native-task-state rows (complete fixed scope,
11
+ // sdkSessionId/taskId, canonical NativeTaskRef and a fully consistent
12
+ // owner: sdk-launch tuple, ownerBasis, launcher kind, receipt and task
13
+ // type); a row that fails any check is rejected whole - it applies no
14
+ // status and consumes no row position. Child claude/native-trace rows
15
+ // attach by actual (sdkSessionId, nativeAgentId); lifecycle rows such as
16
+ // SubagentStop legitimately carry no tool id. Public-read
17
+ // claude/native-task-transcript rows attach by the same trusted
18
+ // (sdkSessionId, agent id) tuple - for a bound agent the taskId IS the
19
+ // receipt agent id - and never create a task or claim ownership. Bash facts
20
+ // route by (sdkSessionId, parent nativeAgentId, toolUseId) so identical
21
+ // tool ids under different parents never cross. Pending rows hold metadata
22
+ // only and are never artificially capped - a complete history rebuilds
23
+ // completely. Pure index: no file I/O, no HTTP, no stop, no public cursor.
24
+
25
+ const crypto = require('node:crypto');
26
+ const { createTaskScope, createNativeTaskRef, nativeTaskKey, sameTaskScope } = require('./task-identity');
27
+
28
+ const TASK_EVENT_TYPES = new Set(['claude/task-started', 'claude/task-progress', 'claude/task-updated', 'claude/task-notification']);
29
+ const AGENT_TOOL_NAMES = new Set(['Agent', 'Task']);
30
+ const TASK_STATUSES = new Set(['pending', 'running', 'completed', 'failed', 'stopped']);
31
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'stopped']);
32
+ const TASK_KIND_BY_TYPE = new Map([['local_agent', 'agent'], ['local_bash', 'local_bash']]);
33
+ const STATE_ASSOCIATIONS = new Set(['bound', 'main-command', 'unassigned']);
34
+ const MAX_DIAGNOSTICS = 500;
35
+ const MAX_SEEN_SIGNATURES = 2000;
36
+ const DEFAULT_LIMIT = 100;
37
+ const MAX_LIMIT = 1000;
38
+
39
+ function id(value) { return typeof value === 'string' && value.trim() !== '' ? value : null; }
40
+ function tupleKey() { return JSON.stringify([...arguments]); }
41
+ function rowHash(event) { return crypto.createHash('sha256').update(JSON.stringify(event)).digest('hex'); }
42
+
43
+ // Full consistency validation of a bound claude/native-task-state claim,
44
+ // checked BEFORE anything is applied: canonical ref matching the row and
45
+ // constructor scope, registry owner basis, launcher/receipt rules, a known
46
+ // task type consistent with the established one, and the canonical
47
+ // sdk-launch ownerRunId tuple. Returns a diagnostic code, or null when the
48
+ // ownership claim is fully consistent.
49
+ function validateBoundState(event, scope, sdkSessionId, taskId, establishedTaskType) {
50
+ const ref = createNativeTaskRef(event.taskRef);
51
+ if (!ref) return 'state-ref-invalid';
52
+ if (!sameTaskScope(ref, scope)) return 'state-ref-scope-mismatch';
53
+ if (ref.taskId !== taskId || ref.ownerRunId !== id(event.ownerRunId)) return 'state-ref-inconsistent';
54
+ if (event.typeConflict === true) return 'conflicted-type-not-bindable';
55
+ const taskType = id(event.taskType);
56
+ const taskKind = TASK_KIND_BY_TYPE.get(taskType) || null;
57
+ if (!taskKind) return 'unknown-task-type';
58
+ if (id(event.taskKind) !== taskKind) return 'state-kind-inconsistent';
59
+ if (establishedTaskType !== null && establishedTaskType !== taskType) return 'task-type-conflict-bind';
60
+ const owner = event.owner && typeof event.owner === 'object' ? event.owner : null;
61
+ if (!owner || id(owner.ownerBasis) !== 'sdk-tool-launch') return 'owner-basis-invalid';
62
+ const launcherKind = id(owner.launcherKind);
63
+ if (launcherKind !== 'root' && launcherKind !== 'nested') return 'owner-basis-invalid';
64
+ const parentNativeAgentId = id(owner.parentNativeAgentId);
65
+ const launcherToolUseId = id(owner.launcherToolUseId);
66
+ if (!launcherToolUseId) return 'owner-basis-invalid';
67
+ if ((launcherKind === 'root') !== (parentNativeAgentId === null)) return 'launcher-kind-inconsistent';
68
+ if (taskKind === 'local_bash' && launcherKind !== 'nested') return 'launcher-kind-inconsistent';
69
+ const receiptAgentId = id(owner.receiptAgentId);
70
+ if (taskKind === 'agent' && receiptAgentId !== taskId) return 'receipt-task-mismatch';
71
+ if (taskKind === 'local_bash' && receiptAgentId !== null && receiptAgentId !== taskId) return 'receipt-task-mismatch';
72
+ let tuple = null;
73
+ try { tuple = JSON.parse(event.ownerRunId); } catch (error) { tuple = null; }
74
+ const tupleOk = Array.isArray(tuple) && tuple.length === 5 && tuple[0] === 'sdk-launch' && tuple[1] === 1
75
+ && tuple[2] === sdkSessionId && tuple[3] === parentNativeAgentId && tuple[4] === launcherToolUseId;
76
+ if (!tupleOk) return 'owner-run-id-invalid';
77
+ return null;
78
+ }
79
+
80
+ class NativeTaskHistoryIndex {
81
+ constructor(scopeInput) {
82
+ const scope = createTaskScope(scopeInput);
83
+ if (!scope) throw new Error('NativeTaskHistoryIndex requires a valid fixed TaskScope');
84
+ this._scope = Object.freeze(scope);
85
+ this._tasks = new Map();
86
+ this._byRefKey = new Map();
87
+ this._agents = new Map();
88
+ this._bashByTool = new Map();
89
+ this._seenRows = new Map();
90
+ this._pendingTraces = new Map();
91
+ this._pendingDispatch = new Map();
92
+ this._pendingBash = new Map();
93
+ this._pendingRefs = 0;
94
+ this._diagnostics = [];
95
+ this._diagSeen = new Set();
96
+ this._stats = { rowsObserved: 0, rowsReplayed: 0, rowsIndexed: 0, rowsUnattributed: 0, diagnostics: 0 };
97
+ }
98
+
99
+ get scope() { return { storeScope: this._scope.storeScope, contextId: this._scope.contextId, parentSessionId: this._scope.parentSessionId }; }
100
+ diagnostics() { return this._diagnostics.map(d => ({ ...d })); }
101
+
102
+ // One durable row. Returns the number of NEW per-task view positions the
103
+ // row produced; rows held pending until their real binding state arrives
104
+ // count zero here. Everything the index keeps about the row is copied
105
+ // into one immutable snap before routing, so a later mutation of the
106
+ // caller's record cannot rewrite an accepted fact. Replaying a
107
+ // byte-identical row is a no-op; the same offset with a different range
108
+ // or content is an explicit conflict.
109
+ observe(record) {
110
+ this._stats.rowsObserved++;
111
+ if (!record || typeof record !== 'object' || !Number.isSafeInteger(record.offset) || record.offset <= 0
112
+ || !Number.isSafeInteger(record.endOffset) || record.endOffset <= record.offset
113
+ || !record.event || typeof record.event !== 'object') {
114
+ this._diag('invalid-record', {}); return 0;
115
+ }
116
+ const event = record.event;
117
+ if (event.type !== 'claude/native-task-state' && event.type !== 'claude/native-trace'
118
+ && event.type !== 'claude/native-task-transcript' && !TASK_EVENT_TYPES.has(event.type)) return 0;
119
+ const hash = rowHash(event);
120
+ const seen = this._seenRows.get(record.offset);
121
+ if (seen) {
122
+ this._stats.rowsReplayed++;
123
+ if (seen.endOffset !== record.endOffset) this._diag('row-range-conflict', { offset: record.offset, existingEnd: seen.endOffset, incomingEnd: record.endOffset });
124
+ else if (seen.hash !== hash) this._diag('row-content-conflict', { offset: record.offset });
125
+ return 0;
126
+ }
127
+ this._seenRows.set(record.offset, { endOffset: record.endOffset, hash });
128
+ const snap = Object.freeze({ offset: record.offset, endOffset: record.endOffset, kind: event.type, traceId: id(event.traceId), sequence: Number(event.sequence) || null, hash });
129
+ let added = 0;
130
+ if (event.type === 'claude/native-task-state') added = this._observeState(event, snap);
131
+ else if (event.type === 'claude/native-trace') added = this._observeTrace(event, snap);
132
+ else if (event.type === 'claude/native-task-transcript') added = this._observeTranscript(event, snap);
133
+ else added = this._observeTaskEvent(event, snap);
134
+ if (added === 0) this._stats.rowsUnattributed++;
135
+ return added;
136
+ }
137
+
138
+ // Only a real claude/native-task-state row with the complete fixed scope,
139
+ // a canonical NativeTaskRef and a fully consistent owner establishes
140
+ // ownership; anything insufficient stays explicitly unassigned. Every
141
+ // check runs to completion BEFORE anything is applied, so a rejected row
142
+ // applies no status, attaches no position, claims no agent and creates
143
+ // no task.
144
+ _observeState(event, snap) {
145
+ const sdkSessionId = id(event.sdkSessionId);
146
+ const taskId = id(event.taskId);
147
+ if (this._explicitScopeConflict(event) || !sameTaskScope(event.scope, this._scope)) {
148
+ this._diag('state-scope-mismatch', { sdkSessionId, taskId });
149
+ return 0;
150
+ }
151
+ if (!sdkSessionId || !taskId) { this._diag('state-missing-identity', { hasSession: sdkSessionId !== null, hasTask: taskId !== null }); return 0; }
152
+ const association = id(event.association);
153
+ if (!STATE_ASSOCIATIONS.has(association)) {
154
+ this._diag('unknown-association', { sdkSessionId, taskId, association: association === null ? null : association.slice(0, 40) });
155
+ return 0;
156
+ }
157
+ const taskKey = JSON.stringify([sdkSessionId, taskId]);
158
+ const existing = this._tasks.get(taskKey) || null;
159
+ if (association !== 'bound') {
160
+ if (existing && existing.association === 'bound') {
161
+ this._diag('association-conflict', { sdkSessionId, taskId, prior: 'bound', incoming: association });
162
+ return 0;
163
+ }
164
+ const task = this._ensureTask(sdkSessionId, taskId);
165
+ const added = this._attach(task, snap, 'state');
166
+ this._applyStatus(task, event);
167
+ this._applyType(task, event);
168
+ task.association = association;
169
+ if (association === 'main-command') {
170
+ task.mainToolUseId = id(event.mainToolUseId) || id(event.toolUseId) || task.mainToolUseId;
171
+ this._registerBash(sdkSessionId, null, task.mainToolUseId, task);
172
+ }
173
+ return added;
174
+ }
175
+ const err = validateBoundState(event, this._scope, sdkSessionId, taskId, existing ? existing.taskType : null);
176
+ if (err) { this._diag(err, { sdkSessionId, taskId }); return 0; }
177
+ const ref = createNativeTaskRef(event.taskRef);
178
+ const refKey = nativeTaskKey(ref);
179
+ if (existing && existing.association === 'bound') {
180
+ // An owner, once bound, is immutable: a later different owner is a
181
+ // diagnosed conflict that changes nothing, never a rewrite.
182
+ if (existing.ownerRunId !== ref.ownerRunId) {
183
+ this._diag('owner-conflict', { sdkSessionId, taskId, existing: existing.ownerRunId, incoming: ref.ownerRunId });
184
+ return 0;
185
+ }
186
+ } else {
187
+ const claimed = this._byRefKey.get(refKey);
188
+ if (claimed && claimed !== taskKey) { this._diag('owner-ref-collision', { sdkSessionId, taskId }); return 0; }
189
+ }
190
+ const task = this._ensureTask(sdkSessionId, taskId);
191
+ if (task.association !== 'bound') {
192
+ task.association = 'bound';
193
+ task.taskRef = ref;
194
+ task.ownerRunId = ref.ownerRunId;
195
+ const source = event.owner && typeof event.owner === 'object' ? event.owner : {};
196
+ task.owner = { ownerBasis: id(source.ownerBasis), launcherKind: id(source.launcherKind), parentNativeAgentId: id(source.parentNativeAgentId), launcherToolUseId: id(source.launcherToolUseId), receiptAgentId: id(source.receiptAgentId) };
197
+ this._byRefKey.set(refKey, task.taskKey);
198
+ }
199
+ const added = this._attach(task, snap, 'state');
200
+ this._applyStatus(task, event);
201
+ this._applyType(task, event);
202
+ this._flushPendingFor(task);
203
+ return added;
204
+ }
205
+
206
+ // Any explicitly present session/scope field that contradicts the fixed
207
+ // scope rejects the whole row; absent fields stay absent - the index
208
+ // never guesses a scope from silence. Returns the conflicting field
209
+ // name, or null when every explicit field agrees.
210
+ _explicitScopeConflict(event) {
211
+ if (event.parentSessionId !== undefined && id(event.parentSessionId) !== this._scope.parentSessionId) return 'parentSessionId';
212
+ if (event.contextId !== undefined && id(event.contextId) !== this._scope.contextId) return 'contextId';
213
+ if (event.storeScope !== undefined && id(event.storeScope) !== this._scope.storeScope) return 'storeScope';
214
+ if (event.scope !== undefined) {
215
+ if (!event.scope || typeof event.scope !== 'object') return 'scope';
216
+ if (id(event.scope.storeScope) !== this._scope.storeScope) return 'storeScope';
217
+ if (id(event.scope.contextId) !== this._scope.contextId) return 'contextId';
218
+ if (id(event.scope.parentSessionId) !== this._scope.parentSessionId) return 'parentSessionId';
219
+ }
220
+ return null;
221
+ }
222
+
223
+ // Child claude/native-trace rows: the actor (nativeAgentId) is the agent
224
+ // in whose context the event happened - its own tools, its lifecycle
225
+ // rows and the nested dispatch tools it launched all belong to that
226
+ // dispatcher. Lifecycle rows (SubagentStart/SubagentStop) legitimately
227
+ // carry no tool id; tool rows (PreToolUse/PostToolUse) must carry one.
228
+ // A root Agent/Task PostToolUse receipt only identifies the spawned
229
+ // agent: it is dispatch metadata for that agent, never the agent's own
230
+ // executed tool. A Bash fact feeds both the parent agent view and the
231
+ // command view, keyed by the REAL parent (root explicitly null) so
232
+ // identical tool ids under different parents never cross; each view
233
+ // holds the offset exactly once.
234
+ _observeTrace(event, snap) {
235
+ if (event.source !== 'sdk-hook') return 0;
236
+ const sdkSessionId = id(event.sdkSessionId);
237
+ if (!sdkSessionId) { this._diag('trace-missing-session', {}); return 0; }
238
+ const conflict = this._explicitScopeConflict(event);
239
+ if (conflict) {
240
+ this._diag(conflict === 'parentSessionId' ? 'trace-parent-session-mismatch' : 'trace-scope-mismatch', { sdkSessionId, field: conflict });
241
+ return 0;
242
+ }
243
+ const rawId = id(event.rawToolUseId);
244
+ const callbackId = id(event.callbackToolUseId);
245
+ if (rawId && callbackId && rawId !== callbackId) { this._diag('trace-id-mismatch', { sdkSessionId }); return 0; }
246
+ const toolUseId = rawId || callbackId;
247
+ const hookEvent = id(event.hookEvent);
248
+ if ((hookEvent === 'PreToolUse' || hookEvent === 'PostToolUse') && !toolUseId) {
249
+ this._diag('trace-missing-tool-use-id', { sdkSessionId, hookEvent });
250
+ return 0;
251
+ }
252
+ const actor = id(event.nativeAgentId);
253
+ const toolName = id(event.toolName);
254
+ const response = event.toolResponse && typeof event.toolResponse === 'object' ? event.toolResponse : null;
255
+ const receiptAgentId = response ? id(response.agentId) : null;
256
+ let added = 0;
257
+ if (actor) {
258
+ const taskKey = this._agents.get(tupleKey(sdkSessionId, actor));
259
+ if (taskKey) added += this._attach(this._tasks.get(taskKey), snap, 'trace');
260
+ else this._pend(this._pendingTraces, tupleKey(sdkSessionId, actor), snap);
261
+ } else if (receiptAgentId && AGENT_TOOL_NAMES.has(toolName) && hookEvent === 'PostToolUse') {
262
+ const taskKey = this._agents.get(tupleKey(sdkSessionId, receiptAgentId));
263
+ if (taskKey) added += this._attach(this._tasks.get(taskKey), snap, 'dispatch');
264
+ else this._pend(this._pendingDispatch, tupleKey(sdkSessionId, receiptAgentId), snap);
265
+ }
266
+ if (toolName === 'Bash') {
267
+ if (toolUseId) added += this._attachBash(sdkSessionId, actor, toolUseId, snap);
268
+ else this._diag('bash-trace-missing-tool-use-id', { sdkSessionId, hookEvent });
269
+ }
270
+ return added;
271
+ }
272
+
273
+ // Public-read transcript snapshots (claude/native-task-transcript). The
274
+ // row attaches to the bound agent task it was read for, routed by the
275
+ // same trusted (sdkSessionId, agent id) tuple the traces use - for a
276
+ // bound agent the row's taskId IS the receipt agent id. A transcript row
277
+ // never creates a task, never claims ownership, never applies a status:
278
+ // it is content evidence under an ownership someone else already proved.
279
+ // A row with no binding yet waits pending exactly like a trace row.
280
+ _observeTranscript(event, snap) {
281
+ if (event.source !== 'sdk-public-read') return 0;
282
+ const sdkSessionId = id(event.sdkSessionId);
283
+ const taskId = id(event.taskId);
284
+ if (!sdkSessionId || !taskId) { this._diag('transcript-missing-identity', { hasSession: sdkSessionId !== null, hasTask: taskId !== null }); return 0; }
285
+ const conflict = this._explicitScopeConflict(event);
286
+ if (conflict) { this._diag('transcript-scope-mismatch', { sdkSessionId, taskId, field: conflict }); return 0; }
287
+ const taskKey = this._agents.get(tupleKey(sdkSessionId, taskId));
288
+ if (taskKey) return this._attach(this._tasks.get(taskKey), snap, 'transcript');
289
+ this._pend(this._pendingTraces, tupleKey(sdkSessionId, taskId), snap);
290
+ return 0;
291
+ }
292
+
293
+ _attachBash(sdkSessionId, parentNativeAgentId, toolUseId, snap) {
294
+ const key = tupleKey(sdkSessionId, parentNativeAgentId, toolUseId);
295
+ const taskKey = this._bashByTool.get(key);
296
+ if (taskKey) return this._attach(this._tasks.get(taskKey), snap, 'trace');
297
+ this._pend(this._pendingBash, key, snap);
298
+ return 0;
299
+ }
300
+
301
+ // Pending rows keep only their immutable snap - offsets, hash and the
302
+ // routing decision made at observe time. There is deliberately no cap:
303
+ // an artificially truncated pending set would silently turn a complete
304
+ // history into an incomplete one.
305
+ _pend(map, key, snap) {
306
+ const list = map.get(key) || [];
307
+ list.push(snap);
308
+ map.set(key, list);
309
+ this._pendingRefs++;
310
+ }
311
+
312
+ // Canonical SDK task events (claude/task-*). Their sdkSessionId/taskId
313
+ // are real facts once present; rows without them are insufficient old
314
+ // data and never guess an identity. status may be an explicit null from
315
+ // the reviewed no-fake-success semantics.
316
+ _observeTaskEvent(event, snap) {
317
+ const sdkSessionId = id(event.sdkSessionId);
318
+ const taskId = id(event.taskId);
319
+ if (!sdkSessionId || !taskId) { this._diag('task-event-missing-identity', { type: event.type }); return 0; }
320
+ if (id(event.parentSessionId) !== null && event.parentSessionId !== this._scope.parentSessionId) {
321
+ this._diag('task-event-scope-mismatch', { type: event.type, sdkSessionId, taskId });
322
+ return 0;
323
+ }
324
+ const task = this._ensureTask(sdkSessionId, taskId);
325
+ this._applyStatus(task, event);
326
+ if (id(event.taskType) !== null) this._applyType(task, event);
327
+ return this._attach(task, snap, 'task-event');
328
+ }
329
+
330
+ _applyStatus(task, event) {
331
+ const raw = id(event.status);
332
+ const source = id(event.statusSource) || id(event.type);
333
+ if (raw === null) return;
334
+ if (!TASK_STATUSES.has(raw)) { this._diag('unknown-task-status', { sdkSessionId: task.sdkSessionId, taskId: task.taskId, raw: raw.slice(0, 40) }); return; }
335
+ const prior = task.status;
336
+ if (prior === null) { task.status = raw; task.statusSource = source; return; }
337
+ if (TERMINAL_STATUSES.has(prior)) {
338
+ if (!TERMINAL_STATUSES.has(raw)) { this._diag('terminal-no-revival', { sdkSessionId: task.sdkSessionId, taskId: task.taskId, incoming: raw }); return; }
339
+ if (raw !== prior) { this._diag('terminal-conflict', { sdkSessionId: task.sdkSessionId, taskId: task.taskId, prior, incoming: raw }); return; }
340
+ return;
341
+ }
342
+ task.status = raw; task.statusSource = source;
343
+ }
344
+
345
+ _applyType(task, event) {
346
+ const taskType = id(event.taskType);
347
+ if (taskType === null) return;
348
+ if (task.taskType === null) {
349
+ task.taskType = taskType;
350
+ task.taskKind = TASK_KIND_BY_TYPE.get(taskType) || null;
351
+ } else if (taskType !== task.taskType) {
352
+ task.typeConflict = true;
353
+ this._diag('task-type-conflict', { sdkSessionId: task.sdkSessionId, taskId: task.taskId, existing: task.taskType, incoming: taskType });
354
+ }
355
+ }
356
+
357
+ _ensureTask(sdkSessionId, taskId) {
358
+ const taskKey = JSON.stringify([sdkSessionId, taskId]);
359
+ let task = this._tasks.get(taskKey);
360
+ if (!task) {
361
+ task = { taskKey, sdkSessionId, taskId, taskKind: null, taskType: null, typeConflict: false, status: null, statusSource: null, association: 'unassigned', taskRef: null, ownerRunId: null, owner: null, mainToolUseId: null, view: new Map() };
362
+ this._tasks.set(taskKey, task);
363
+ }
364
+ return task;
365
+ }
366
+
367
+ _attach(task, snap, role) {
368
+ if (!task) return 0;
369
+ let entry = task.view.get(snap.offset);
370
+ if (!entry) {
371
+ entry = { offset: snap.offset, endOffset: snap.endOffset, kind: snap.kind, roles: new Set([role]), traceId: snap.traceId, sequence: snap.sequence, hash: snap.hash };
372
+ task.view.set(snap.offset, entry);
373
+ this._stats.rowsIndexed++;
374
+ return 1;
375
+ }
376
+ entry.roles.add(role);
377
+ return 0;
378
+ }
379
+
380
+ _flushPendingFor(task) {
381
+ if (task.association !== 'bound') return;
382
+ if (task.taskKind === 'agent' && task.owner && task.owner.receiptAgentId) {
383
+ const key = tupleKey(task.sdkSessionId, task.owner.receiptAgentId);
384
+ const claimant = this._agents.get(key);
385
+ if (claimant && claimant !== task.taskKey) { this._diag('agent-claim-conflict', { sdkSessionId: task.sdkSessionId, agentId: task.owner.receiptAgentId }); return; }
386
+ this._agents.set(key, task.taskKey);
387
+ this._flushList(this._pendingTraces, key, task, 'trace');
388
+ this._flushList(this._pendingDispatch, key, task, 'dispatch');
389
+ }
390
+ if (task.taskKind === 'local_bash' && task.owner) {
391
+ this._registerBash(task.sdkSessionId, task.owner.parentNativeAgentId, task.owner.launcherToolUseId, task);
392
+ }
393
+ }
394
+
395
+ _flushList(map, key, task, role) {
396
+ const list = map.get(key);
397
+ if (!list) return;
398
+ map.delete(key);
399
+ this._pendingRefs -= list.length;
400
+ for (const snap of list) this._attach(task, snap, role);
401
+ }
402
+
403
+ // First real claim on (sdkSessionId, parent nativeAgentId, toolUseId)
404
+ // wins - a root command claims with an explicit null parent. A different
405
+ // later claimant is a diagnosed conflict, never a silent re-route.
406
+ _registerBash(sdkSessionId, parentNativeAgentId, toolUseId, task) {
407
+ if (!toolUseId) return;
408
+ const key = tupleKey(sdkSessionId, parentNativeAgentId, toolUseId);
409
+ const existing = this._bashByTool.get(key);
410
+ if (existing && existing !== task.taskKey) { this._diag('bash-claim-conflict', { sdkSessionId, parentNativeAgentId, toolUseId }); return; }
411
+ this._bashByTool.set(key, task.taskKey);
412
+ this._flushList(this._pendingBash, key, task, 'trace');
413
+ }
414
+
415
+ // Metadata-only task snapshots; historical 'running' is a past
416
+ // observation and never a claim that a current process is still alive.
417
+ listTasks() {
418
+ const out = [];
419
+ for (const task of this._tasks.values()) {
420
+ let first = null, last = null;
421
+ for (const offset of task.view.keys()) {
422
+ if (first === null || offset < first) first = offset;
423
+ if (last === null || offset > last) last = offset;
424
+ }
425
+ out.push({ sdkSessionId: task.sdkSessionId, taskId: task.taskId, taskKind: task.taskKind, taskType: task.taskType, typeConflict: task.typeConflict === true, status: task.status, statusSource: task.statusSource, association: task.association, taskRef: task.taskRef, ownerRunId: task.ownerRunId, owner: task.owner, mainToolUseId: task.mainToolUseId, rowCount: task.view.size, firstOffset: first, lastOffset: last });
426
+ }
427
+ return JSON.parse(JSON.stringify(out));
428
+ }
429
+
430
+ // Internal byte-position pagination over one task's original rows, newest
431
+ // window first and ascending within a page. `before` is an exclusive raw
432
+ // offset boundary inside the same file era - NOT a public opaque cursor;
433
+ // stream sequences are never treated as positions.
434
+ listOffsets(taskRef, { before, limit } = {}) {
435
+ const ref = createNativeTaskRef(taskRef);
436
+ if (!ref) throw Object.assign(new Error('invalid native task reference'), { code: 'INVALID_TASK_REF' });
437
+ if (!sameTaskScope(ref, this._scope)) throw Object.assign(new Error('task reference scope does not match this index'), { code: 'SCOPE_MISMATCH' });
438
+ const size = limit === undefined ? DEFAULT_LIMIT : limit;
439
+ if (!Number.isSafeInteger(size) || size < 1 || size > MAX_LIMIT) throw Object.assign(new Error('limit must be between 1 and ' + MAX_LIMIT), { code: 'INVALID_LIMIT' });
440
+ let bound = null;
441
+ if (before !== undefined && before !== null) {
442
+ if (!Number.isSafeInteger(before) || before <= 0) throw Object.assign(new Error('invalid before offset'), { code: 'INVALID_BEFORE' });
443
+ bound = before;
444
+ }
445
+ const taskKey = this._byRefKey.get(nativeTaskKey(ref));
446
+ if (!taskKey) return { entries: [], nextBefore: null };
447
+ const task = this._tasks.get(taskKey);
448
+ const offsets = [...task.view.keys()].sort((a, b) => a - b).filter(offset => bound === null || offset < bound);
449
+ const start = Math.max(0, offsets.length - size);
450
+ const page = offsets.slice(start);
451
+ return { entries: page.map(offset => this._entrySnapshot(task.view.get(offset))), nextBefore: start > 0 && page.length > 0 ? page[0] : null };
452
+ }
453
+
454
+ _entrySnapshot(entry) {
455
+ return { offset: entry.offset, endOffset: entry.endOffset, kind: entry.kind, roles: [...entry.roles].sort(), traceId: entry.traceId, sequence: entry.sequence, hash: entry.hash };
456
+ }
457
+
458
+ stats() {
459
+ let bound = 0;
460
+ for (const task of this._tasks.values()) if (task.association === 'bound') bound++;
461
+ return { ...this._stats, tasksTotal: this._tasks.size, tasksBound: bound, tasksUnassigned: this._tasks.size - bound, pendingRefs: this._pendingRefs };
462
+ }
463
+
464
+ _diag(code, ids) {
465
+ this._stats.diagnostics++;
466
+ const signature = code + '|' + JSON.stringify(ids || {});
467
+ if (this._diagSeen.has(signature)) return;
468
+ if (this._diagSeen.size >= MAX_SEEN_SIGNATURES) return;
469
+ this._diagSeen.add(signature);
470
+ if (this._diagnostics.length >= MAX_DIAGNOSTICS) return;
471
+ this._diagnostics.push(Object.assign({ code }, ids || {}));
472
+ }
473
+ }
474
+
475
+ module.exports = { NativeTaskHistoryIndex };