@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3

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