@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.
- package/LICENSE +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +636 -613
- package/apps/agent-terminal/app.js +125 -65
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +171 -170
- package/apps/agent-terminal/panel-host-proof.html +152 -152
- package/apps/agent-terminal/panel-proof-content.html +41 -41
- package/apps/agent-terminal/right-workspace-dock.css +187 -159
- package/apps/agent-terminal/right-workspace-dock.js +623 -519
- package/apps/ai-profile-settings-host/index.html +25 -25
- package/apps/ai-profile-settings-host/page.css +1 -1
- package/apps/ai-profile-settings-host/page.js +113 -113
- package/apps/minimal-test-host/README.md +15 -15
- package/apps/minimal-test-host/host.css +2 -2
- package/apps/minimal-test-host/host.js +35 -35
- package/apps/minimal-test-host/index.html +58 -58
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
- package/docs/sdk/FILE_LAYOUT.md +31 -31
- package/docs/sdk/INTEGRATION.md +126 -126
- package/docs/sdk/README.md +21 -21
- package/docs/sdk/SESSION_EVENTS.md +13 -13
- package/docs/sdk/openapi.yaml +8 -1
- package/package.json +1 -1
- package/packages/client/README.md +29 -29
- package/packages/client/index.d.ts +122 -122
- package/packages/client/index.js +260 -260
- package/packages/client/package.json +2 -2
- package/packages/contracts/README.md +9 -9
- package/packages/contracts/index.d.ts +519 -517
- package/packages/contracts/index.js +312 -312
- package/packages/contracts/package.json +1 -1
- package/packages/core/README.md +8 -8
- package/packages/core/index.d.ts +76 -76
- package/packages/core/index.js +222 -222
- package/packages/core/package.json +2 -2
- package/packages/server/README.md +17 -17
- package/packages/server/bin/agent-terminal-server.js +42 -42
- package/packages/server/bin/claude-workbench.js +225 -225
- package/packages/server/index.d.ts +171 -171
- package/packages/server/index.js +230 -230
- package/packages/server/lib/agent-discovery.js +133 -133
- package/packages/server/lib/agent-registry.js +143 -143
- package/packages/server/lib/agent-spawn-env.js +17 -17
- package/packages/server/lib/attachment-store.js +78 -78
- package/packages/server/lib/bridges/app-server-bridge.js +373 -373
- package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
- package/packages/server/lib/capture-settings-store.js +35 -35
- package/packages/server/lib/claude-history-recovery.js +87 -87
- package/packages/server/lib/cli-session-recovery.js +28 -28
- package/packages/server/lib/context-resolver.js +55 -55
- package/packages/server/lib/conversation-capture.js +51 -51
- package/packages/server/lib/delegation-phase-store.js +357 -357
- package/packages/server/lib/delegation-store.js +474 -468
- package/packages/server/lib/diagnostics.js +32 -32
- package/packages/server/lib/drivers/cli-json-runner.js +974 -974
- package/packages/server/lib/drivers/codex-driver.js +1442 -1442
- package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
- package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
- package/packages/server/lib/event-stream.js +138 -138
- package/packages/server/lib/folder-picker.js +623 -623
- package/packages/server/lib/http-handler.js +339 -339
- package/packages/server/lib/json-file.js +26 -26
- package/packages/server/lib/knowledge-context.js +33 -33
- package/packages/server/lib/model-detection.js +295 -295
- package/packages/server/lib/parent-context.js +44 -0
- package/packages/server/lib/profile-store.js +283 -283
- package/packages/server/lib/project-store.js +102 -102
- package/packages/server/lib/runtime-loader.js +120 -120
- package/packages/server/lib/session-archive-store.js +53 -53
- package/packages/server/lib/session-history.js +612 -612
- package/packages/server/lib/system-doctor.js +125 -125
- package/packages/server/lib/ui-preferences-store.js +48 -48
- package/packages/server/lib/workbench-service.js +1696 -1675
- package/packages/server/package.json +2 -2
- package/packages/ui/README.md +30 -30
- package/packages/ui/ai-profile-settings.d.ts +45 -45
- package/packages/ui/ai-profile-settings.js +283 -283
- package/packages/ui/claude-terminal.d.ts +31 -31
- package/packages/ui/claude-terminal.js +343 -343
- package/packages/ui/index.d.ts +18 -18
- package/packages/ui/index.js +51 -51
- package/packages/ui/package.json +2 -2
- package/packages/ui/theme.css +13 -13
- package/src/assets/claude-prompts.json +21 -21
- package/src/backend/http/ai-profile-routes.js +61 -61
- package/src/backend/http/claude-routes.js +170 -170
- package/src/backend/http/node-helpers.js +45 -45
- package/src/backend/index.js +21 -21
- package/src/backend/lib/ai-adapter.js +144 -144
- package/src/backend/lib/ai-profile-store.js +259 -259
- package/src/backend/lib/ai-vendor-presets.js +83 -83
- package/src/backend/lib/ai-workspace.js +140 -140
- package/src/backend/lib/automation-config.js +253 -253
- package/src/backend/lib/claude-cli-runner.js +2392 -2392
- package/src/backend/lib/data-dir.js +169 -169
- package/src/backend/lib/llm-client.js +176 -176
- package/src/backend/lib/model-context-windows.js +82 -82
- package/src/backend/lib/native-task-control.js +217 -217
- package/src/backend/lib/native-task-history-index.js +475 -475
- package/src/backend/lib/native-task-history-reader.js +526 -526
- package/src/backend/lib/native-task-messages.js +457 -457
- package/src/backend/lib/native-task-registry.js +325 -325
- package/src/backend/lib/native-task-runtime.js +242 -242
- package/src/backend/lib/native-task-trace.js +440 -440
- package/src/backend/lib/native-task-transcript.js +393 -393
- package/src/backend/lib/prompt-registry.js +166 -166
- package/src/backend/lib/runtime-config.js +121 -121
- package/src/backend/lib/sdk-session-stream.js +143 -143
- package/src/backend/lib/sdk-turn-stream.js +16 -16
- package/src/backend/lib/store-scope.js +131 -131
- package/src/backend/lib/task-identity.js +104 -104
- package/src/backend/lib/task-state-projection.js +58 -58
- package/src/index.js +7 -7
- package/src/sdk-wave-a.ts +1 -1
- package/src/sdk-wave-b.ts +1 -1
- package/src/sdk-wave-c.ts +1 -1
- package/src/sdk-wave-d.ts +1 -1
- 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 };
|