praxis-agent 0.35.0 → 0.37.0
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/README.md +20 -2
- package/dist/application/background-agent-manager.d.ts +16 -6
- package/dist/application/background-agent-manager.js +139 -97
- package/dist/application/context-engine.d.ts +56 -0
- package/dist/application/context-engine.js +129 -0
- package/dist/application/durable-follow-up.d.ts +14 -0
- package/dist/application/durable-follow-up.js +52 -0
- package/dist/application/native-session-transcript.d.ts +72 -0
- package/dist/application/native-session-transcript.js +351 -0
- package/dist/application/native-sidechain-transcript.d.ts +34 -0
- package/dist/application/native-sidechain-transcript.js +156 -0
- package/dist/application/session-service.d.ts +23 -4
- package/dist/application/session-service.js +1130 -494
- package/dist/application/subagent-service.d.ts +5 -1
- package/dist/application/subagent-service.js +749 -320
- package/dist/application/team-agent-runtime.d.ts +29 -0
- package/dist/application/team-agent-runtime.js +169 -0
- package/dist/application/team-capability.d.ts +27 -0
- package/dist/application/team-capability.js +41 -0
- package/dist/application/team-lead-operations.d.ts +38 -0
- package/dist/application/team-lead-operations.js +155 -0
- package/dist/application/team-mailbox.d.ts +54 -0
- package/dist/application/team-mailbox.js +196 -0
- package/dist/application/team-manager.d.ts +113 -0
- package/dist/application/team-manager.js +681 -0
- package/dist/application/team-workspace.d.ts +29 -0
- package/dist/application/team-workspace.js +197 -0
- package/dist/application/top-level-agent-manager.d.ts +1 -0
- package/dist/application/top-level-agent-manager.js +1025 -403
- package/dist/application/transcript-projection.d.ts +35 -0
- package/dist/application/transcript-projection.js +139 -0
- package/dist/application/turn-lifecycle.d.ts +2 -0
- package/dist/application/turn-memory-coordinator.d.ts +43 -0
- package/dist/application/turn-memory-coordinator.js +98 -0
- package/dist/cli/agents-dashboard.js +10 -5
- package/dist/cli/interactive.d.ts +7 -0
- package/dist/cli/interactive.js +645 -334
- package/dist/cli/tui/claude-style.d.ts +8 -25
- package/dist/cli/tui/claude-style.js +299 -277
- package/dist/cli/tui/decision-surface-model.d.ts +71 -0
- package/dist/cli/tui/decision-surface-model.js +131 -0
- package/dist/cli/tui/decision-surface.d.ts +7 -0
- package/dist/cli/tui/decision-surface.js +41 -0
- package/dist/cli/tui/help-surface-model.d.ts +68 -0
- package/dist/cli/tui/help-surface-model.js +131 -0
- package/dist/cli/tui/mcp-panel.js +1 -2
- package/dist/cli/tui/permission-surface-model.d.ts +195 -0
- package/dist/cli/tui/permission-surface-model.js +360 -0
- package/dist/cli/tui/permission-surface.d.ts +7 -0
- package/dist/cli/tui/permission-surface.js +69 -0
- package/dist/cli/tui/presentation-environment.d.ts +32 -0
- package/dist/cli/tui/presentation-environment.js +149 -0
- package/dist/cli/tui/task-panel.js +10 -1
- package/dist/cli/tui/theme.d.ts +46 -3
- package/dist/cli/tui/theme.js +414 -56
- package/dist/cli/tui/tool-permission.js +16 -8
- package/dist/cli/tui/transcript-presentation.d.ts +19 -1
- package/dist/cli/tui/transcript-presentation.js +26 -6
- package/dist/cli/tui/transcript-viewport.d.ts +18 -0
- package/dist/cli/tui/transcript-viewport.js +517 -111
- package/dist/cli/tui/transcript-window-model.d.ts +108 -0
- package/dist/cli/tui/transcript-window-model.js +772 -0
- package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
- package/dist/cli/tui/tui-interaction-router.js +127 -0
- package/dist/cli/tui/tui-screen-model.d.ts +114 -0
- package/dist/cli/tui/tui-screen-model.js +210 -0
- package/dist/cli/tui/tui-view-model.d.ts +2 -49
- package/dist/cli/tui/tui-view-model.js +1 -68
- package/dist/cli-runtime.d.ts +4 -2
- package/dist/cli-runtime.js +397 -32
- package/dist/compatibility/claude/transcript-codec.js +6 -0
- package/dist/core/agent-orchestration.d.ts +31 -0
- package/dist/core/agent-orchestration.js +235 -0
- package/dist/core/permission-policy.d.ts +4 -0
- package/dist/core/permission-policy.js +21 -0
- package/dist/core/runtime.d.ts +6 -0
- package/dist/core/runtime.js +20 -2
- package/dist/core/team-mailbox.d.ts +54 -0
- package/dist/core/team-mailbox.js +212 -0
- package/dist/core/team-ownership.d.ts +84 -0
- package/dist/core/team-ownership.js +505 -0
- package/dist/core/tool-execution-scheduler.js +6 -1
- package/dist/core/transcript-event.d.ts +5 -1
- package/dist/core/transcript-event.js +12 -0
- package/dist/persistence/claude-job-store.d.ts +58 -0
- package/dist/persistence/claude-job-store.js +483 -1
- package/dist/persistence/claude-transcript-store.d.ts +2 -1
- package/dist/persistence/claude-transcript-store.js +2 -10
- package/dist/persistence/native-team-mailbox.d.ts +68 -0
- package/dist/persistence/native-team-mailbox.js +542 -0
- package/dist/persistence/native-team-store.d.ts +34 -0
- package/dist/persistence/native-team-store.js +257 -0
- package/dist/persistence/native-transcript-reader.d.ts +53 -0
- package/dist/persistence/native-transcript-reader.js +283 -0
- package/dist/persistence/native-transcript-store.d.ts +69 -0
- package/dist/persistence/native-transcript-store.js +321 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
- package/dist/persistence/subagent-lifecycle-store.js +373 -146
- package/dist/persistence/transcript-file-append.d.ts +2 -0
- package/dist/persistence/transcript-file-append.js +10 -0
- package/dist/platform/exclusive-file-lease.d.ts +3 -0
- package/dist/platform/exclusive-file-lease.js +3 -0
- package/dist/tools/claude-capabilities.d.ts +4 -0
- package/dist/tools/claude-capabilities.js +18 -2
- package/dist/tools/team-lead-tools.d.ts +19 -0
- package/dist/tools/team-lead-tools.js +479 -0
- package/dist/tools/team-member-tools.d.ts +21 -0
- package/dist/tools/team-member-tools.js +259 -0
- package/package.json +1 -1
- package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
- package/dist/cli/tui/fullscreen-renderer.js +0 -13
|
@@ -2,20 +2,26 @@ import { readFile, stat } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isClaudeSessionId } from '../compatibility/claude/paths.js';
|
|
4
4
|
import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
|
|
5
|
+
import { createAgentLifecycle, continueLifecycle, isTerminalLifecycleState, markLifecycleOrphaned, parseAgentLifecycleSnapshot, recoverLifecycle, transitionLifecycle, } from '../core/agent-orchestration.js';
|
|
6
|
+
import { ExclusiveFileLease, } from '../platform/exclusive-file-lease.js';
|
|
5
7
|
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
6
|
-
|
|
7
|
-
'
|
|
8
|
-
|
|
9
|
-
'failed'
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
function statusFor(snapshot) {
|
|
9
|
+
if (snapshot.state === 'completed')
|
|
10
|
+
return 'completed';
|
|
11
|
+
if (snapshot.state === 'failed')
|
|
12
|
+
return 'failed';
|
|
13
|
+
if (snapshot.state === 'cancelled')
|
|
14
|
+
return 'killed';
|
|
15
|
+
return 'running';
|
|
16
|
+
}
|
|
17
|
+
function publicRecord(record) {
|
|
18
|
+
return Object.assign({ ...record }, { status: statusFor(record.lifecycle) });
|
|
19
|
+
}
|
|
12
20
|
function assertIdentity(sessionId, agentId) {
|
|
13
|
-
if (!isClaudeSessionId(sessionId))
|
|
21
|
+
if (!isClaudeSessionId(sessionId))
|
|
14
22
|
throw new Error(`Invalid subagent lifecycle session ID: ${sessionId}`);
|
|
15
|
-
|
|
16
|
-
if (!isClaudeAgentId(agentId)) {
|
|
23
|
+
if (!isClaudeAgentId(agentId))
|
|
17
24
|
throw new Error(`Invalid subagent lifecycle agent ID: ${agentId}`);
|
|
18
|
-
}
|
|
19
25
|
}
|
|
20
26
|
function isNonnegativeInteger(value) {
|
|
21
27
|
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
@@ -24,54 +30,43 @@ function isNonnegativeNumber(value) {
|
|
|
24
30
|
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
25
31
|
}
|
|
26
32
|
function isUsage(value) {
|
|
27
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
33
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
28
34
|
return false;
|
|
29
|
-
}
|
|
30
35
|
const usage = value;
|
|
31
36
|
if (!isNonnegativeInteger(usage.inputTokens) ||
|
|
32
|
-
!isNonnegativeInteger(usage.outputTokens))
|
|
37
|
+
!isNonnegativeInteger(usage.outputTokens))
|
|
33
38
|
return false;
|
|
34
|
-
}
|
|
35
39
|
for (const field of [
|
|
36
40
|
'cacheReadInputTokens',
|
|
37
41
|
'cacheCreationInputTokens',
|
|
38
42
|
'webSearchRequests',
|
|
39
|
-
])
|
|
40
|
-
if (usage[field] !== undefined && !isNonnegativeInteger(usage[field]))
|
|
43
|
+
])
|
|
44
|
+
if (usage[field] !== undefined && !isNonnegativeInteger(usage[field]))
|
|
41
45
|
return false;
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
for (const field of ['contextWindow', 'maxOutputTokens']) {
|
|
46
|
+
for (const field of ['contextWindow', 'maxOutputTokens'])
|
|
45
47
|
if (usage[field] !== undefined &&
|
|
46
|
-
(!Number.isSafeInteger(usage[field]) || Number(usage[field]) < 1))
|
|
48
|
+
(!Number.isSafeInteger(usage[field]) || Number(usage[field]) < 1))
|
|
47
49
|
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
52
52
|
function isRunResult(value) {
|
|
53
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
53
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
54
54
|
return false;
|
|
55
|
-
}
|
|
56
55
|
const result = value;
|
|
57
56
|
if (typeof result.text !== 'string' ||
|
|
58
57
|
!isUsage(result.usage) ||
|
|
59
58
|
!isNonnegativeInteger(result.toolUseCount) ||
|
|
60
|
-
!isNonnegativeInteger(result.durationMs))
|
|
59
|
+
!isNonnegativeInteger(result.durationMs))
|
|
61
60
|
return false;
|
|
62
|
-
}
|
|
63
61
|
if (result.modelUsage !== undefined &&
|
|
64
62
|
(typeof result.modelUsage !== 'object' ||
|
|
65
63
|
result.modelUsage === null ||
|
|
66
64
|
Array.isArray(result.modelUsage) ||
|
|
67
|
-
!Object.values(result.modelUsage).every(isUsage)))
|
|
65
|
+
!Object.values(result.modelUsage).every(isUsage)))
|
|
68
66
|
return false;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (result[field] !== undefined && !isNonnegativeNumber(result[field])) {
|
|
67
|
+
for (const field of ['durationApiMs', 'durationApiWithoutRetriesMs'])
|
|
68
|
+
if (result[field] !== undefined && !isNonnegativeNumber(result[field]))
|
|
72
69
|
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
70
|
return ((result.isolationPath === undefined ||
|
|
76
71
|
typeof result.isolationPath === 'string') &&
|
|
77
72
|
(result.isolationRetained === undefined ||
|
|
@@ -80,9 +75,8 @@ function isRunResult(value) {
|
|
|
80
75
|
typeof result.isolationWarning === 'string'));
|
|
81
76
|
}
|
|
82
77
|
function isNotification(value) {
|
|
83
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
78
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
84
79
|
return false;
|
|
85
|
-
}
|
|
86
80
|
const notification = value;
|
|
87
81
|
const accounting = notification.accounting;
|
|
88
82
|
return (typeof notification.id === 'string' &&
|
|
@@ -103,12 +97,76 @@ function isNotification(value) {
|
|
|
103
97
|
0 &&
|
|
104
98
|
typeof accounting.delivered === 'boolean')));
|
|
105
99
|
}
|
|
106
|
-
|
|
100
|
+
function ownerFromLease(lease) {
|
|
101
|
+
return { token: lease.token, pid: lease.pid, acquiredAt: lease.createdAt };
|
|
102
|
+
}
|
|
103
|
+
export class SubagentExecution {
|
|
104
|
+
store;
|
|
105
|
+
lease;
|
|
106
|
+
token;
|
|
107
|
+
generation;
|
|
108
|
+
current;
|
|
109
|
+
released = false;
|
|
110
|
+
constructor(store, lease, token, generation, current) {
|
|
111
|
+
this.store = store;
|
|
112
|
+
this.lease = lease;
|
|
113
|
+
this.token = token;
|
|
114
|
+
this.generation = generation;
|
|
115
|
+
this.current = current;
|
|
116
|
+
}
|
|
117
|
+
get snapshot() {
|
|
118
|
+
return this.current;
|
|
119
|
+
}
|
|
120
|
+
async transition(state) {
|
|
121
|
+
this.ensureActive();
|
|
122
|
+
this.current = await this.store.mutateOwned(this.lease, this.generation, (record) => this.store.withLifecycle(record, transitionLifecycle(record.lifecycle, state, this.token)));
|
|
123
|
+
return this.current;
|
|
124
|
+
}
|
|
125
|
+
async running() {
|
|
126
|
+
return this.transition('running');
|
|
127
|
+
}
|
|
128
|
+
async beginCancellation() {
|
|
129
|
+
return this.transition('cancelling');
|
|
130
|
+
}
|
|
131
|
+
async finish(state, result, detail, notification) {
|
|
132
|
+
this.ensureActive();
|
|
133
|
+
this.current = await this.store.mutateOwned(this.lease, this.generation, (record) => {
|
|
134
|
+
const lifecycle = transitionLifecycle(record.lifecycle, state, this.token);
|
|
135
|
+
const notifications = [...(record.notifications ?? [])];
|
|
136
|
+
if (notification && result)
|
|
137
|
+
notifications.push({ ...notification, result, consumed: false });
|
|
138
|
+
return this.store.withLifecycle(record, lifecycle, {
|
|
139
|
+
...(detail === undefined ? {} : { detail }),
|
|
140
|
+
...(result === undefined ? {} : { result }),
|
|
141
|
+
...(notifications.length === 0 ? {} : { notifications }),
|
|
142
|
+
});
|
|
143
|
+
}, true);
|
|
144
|
+
return this.current;
|
|
145
|
+
}
|
|
146
|
+
async release() {
|
|
147
|
+
if (this.released)
|
|
148
|
+
return;
|
|
149
|
+
try {
|
|
150
|
+
if (!isTerminalLifecycleState(this.current.state))
|
|
151
|
+
this.current = await this.store.mutateOwned(this.lease, this.generation, (record) => this.store.withLifecycle(record, markLifecycleOrphaned(record.lifecycle, this.token)));
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
this.released = true;
|
|
155
|
+
await this.lease.release();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
ensureActive() {
|
|
159
|
+
if (this.released)
|
|
160
|
+
throw new Error('Lifecycle execution has been released');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
107
163
|
export class SubagentLifecycleStore {
|
|
108
164
|
sessionId;
|
|
109
165
|
agentId;
|
|
110
166
|
transcriptPath;
|
|
111
167
|
path;
|
|
168
|
+
ownerLock;
|
|
169
|
+
mutationLock;
|
|
112
170
|
mutation = Promise.resolve();
|
|
113
171
|
constructor(praxisRoot, sessionId, agentId, transcriptPath) {
|
|
114
172
|
this.sessionId = sessionId;
|
|
@@ -116,141 +174,151 @@ export class SubagentLifecycleStore {
|
|
|
116
174
|
this.transcriptPath = transcriptPath;
|
|
117
175
|
assertIdentity(sessionId, agentId);
|
|
118
176
|
this.path = join(praxisRoot, 'subagent-lifecycle', sessionId, `${agentId}.json`);
|
|
177
|
+
this.ownerLock = new ExclusiveFileLease(`${this.path}.owner.lock`);
|
|
178
|
+
this.mutationLock = new ExclusiveFileLease(`${this.path}.mutation.lock`);
|
|
119
179
|
}
|
|
120
180
|
async read() {
|
|
121
|
-
|
|
181
|
+
const record = await this.readStored();
|
|
182
|
+
return record ? publicRecord(record) : null;
|
|
183
|
+
}
|
|
184
|
+
async acquire(mode = 'start') {
|
|
185
|
+
const lease = await this.ownerLock.tryAcquire();
|
|
186
|
+
if (!lease)
|
|
187
|
+
throw new Error(`Subagent lifecycle execution is already owned: ${this.path}`);
|
|
188
|
+
let mutationLease;
|
|
122
189
|
try {
|
|
123
|
-
|
|
190
|
+
mutationLease = await this.acquireMutationLock();
|
|
124
191
|
}
|
|
125
192
|
catch (error) {
|
|
126
|
-
|
|
127
|
-
return null;
|
|
193
|
+
await lease.release();
|
|
128
194
|
throw error;
|
|
129
195
|
}
|
|
130
|
-
let value;
|
|
131
196
|
try {
|
|
132
|
-
|
|
197
|
+
const record = await this.readStored();
|
|
198
|
+
const owner = ownerFromLease(lease);
|
|
199
|
+
let lifecycle;
|
|
200
|
+
if (!record) {
|
|
201
|
+
if (mode !== 'start')
|
|
202
|
+
throw new Error(`Cannot ${mode} missing subagent lifecycle state: ${this.path}`);
|
|
203
|
+
lifecycle = createAgentLifecycle(owner);
|
|
204
|
+
}
|
|
205
|
+
else if (mode === 'start') {
|
|
206
|
+
throw new Error(`Subagent lifecycle already exists: ${this.path}`);
|
|
207
|
+
}
|
|
208
|
+
else if (mode === 'recover') {
|
|
209
|
+
lifecycle = recoverLifecycle(record.lifecycle, owner);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
lifecycle = continueLifecycle(record.lifecycle, owner);
|
|
213
|
+
}
|
|
214
|
+
const next = this.withLifecycle(record ?? this.baseRecord(lifecycle), lifecycle);
|
|
215
|
+
await this.writeStored(next, false);
|
|
216
|
+
return new SubagentExecution(this, lease, owner.token, lifecycle.generation, lifecycle);
|
|
133
217
|
}
|
|
134
218
|
catch (error) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
140
|
-
throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
|
|
219
|
+
await lease.release();
|
|
220
|
+
throw error;
|
|
141
221
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
record.sessionId !== this.sessionId ||
|
|
145
|
-
record.agentId !== this.agentId ||
|
|
146
|
-
typeof record.status !== 'string' ||
|
|
147
|
-
!statuses.has(record.status) ||
|
|
148
|
-
typeof record.updatedAt !== 'string' ||
|
|
149
|
-
Number.isNaN(Date.parse(record.updatedAt)) ||
|
|
150
|
-
(record.transcriptBytes !== undefined &&
|
|
151
|
-
!isNonnegativeInteger(record.transcriptBytes)) ||
|
|
152
|
-
(record.detail !== undefined && typeof record.detail !== 'string') ||
|
|
153
|
-
(record.result !== undefined && !isRunResult(record.result)) ||
|
|
154
|
-
(record.notifications !== undefined &&
|
|
155
|
-
(!Array.isArray(record.notifications) ||
|
|
156
|
-
!record.notifications.every(isNotification)))) {
|
|
157
|
-
throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
|
|
222
|
+
finally {
|
|
223
|
+
await mutationLease.release();
|
|
158
224
|
}
|
|
159
|
-
return record;
|
|
160
225
|
}
|
|
161
|
-
|
|
162
|
-
return this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
226
|
+
start() {
|
|
227
|
+
return this.acquire('start');
|
|
228
|
+
}
|
|
229
|
+
continue() {
|
|
230
|
+
return this.acquire('continue');
|
|
231
|
+
}
|
|
232
|
+
recover() {
|
|
233
|
+
return this.acquire('recover');
|
|
234
|
+
}
|
|
235
|
+
async reconcileOwnerLoss() {
|
|
236
|
+
const current = await this.readStored();
|
|
237
|
+
if (!current || isTerminalLifecycleState(current.lifecycle.state)) {
|
|
238
|
+
return { owned: false, snapshot: current?.lifecycle ?? null };
|
|
239
|
+
}
|
|
240
|
+
const probe = await this.ownerLock.tryAcquire();
|
|
241
|
+
if (!probe) {
|
|
242
|
+
const reread = await this.readStored();
|
|
243
|
+
if (!reread || isTerminalLifecycleState(reread.lifecycle.state)) {
|
|
244
|
+
return { owned: false, snapshot: reread?.lifecycle ?? null };
|
|
170
245
|
}
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
246
|
+
return { owned: true, snapshot: reread.lifecycle };
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const observed = await this.readStored();
|
|
250
|
+
if (!observed || isTerminalLifecycleState(observed.lifecycle.state)) {
|
|
251
|
+
return { owned: false, snapshot: observed?.lifecycle ?? null };
|
|
252
|
+
}
|
|
253
|
+
const reconciled = await this.mutate(async (latest) => {
|
|
254
|
+
if (!latest)
|
|
255
|
+
return observed;
|
|
256
|
+
if (latest.lifecycle.generation !== observed.lifecycle.generation ||
|
|
257
|
+
latest.lifecycle.owner?.token !== observed.lifecycle.owner?.token)
|
|
258
|
+
return latest;
|
|
259
|
+
return this.withLifecycle(latest, markLifecycleOrphaned(latest.lifecycle, latest.lifecycle.owner?.token ?? ''));
|
|
176
260
|
});
|
|
177
|
-
|
|
261
|
+
return { owned: false, snapshot: reconciled.lifecycle };
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
await probe.release();
|
|
265
|
+
}
|
|
178
266
|
}
|
|
179
267
|
acknowledgeNotification(id) {
|
|
180
268
|
return this.mutate(async (current) => {
|
|
181
|
-
if (!current)
|
|
269
|
+
if (!current)
|
|
182
270
|
throw new Error(`Missing subagent lifecycle state: ${this.path}`);
|
|
183
|
-
}
|
|
184
271
|
const notifications = current.notifications ?? [];
|
|
185
|
-
|
|
186
|
-
if (!matched)
|
|
272
|
+
if (!notifications.some((n) => n.id === id && !n.consumed))
|
|
187
273
|
return current;
|
|
188
|
-
return this.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
notifications: notifications.map((notification) => notification.id === id
|
|
193
|
-
? { ...notification, consumed: true }
|
|
194
|
-
: notification),
|
|
195
|
-
}, { transcriptBytes: current.transcriptBytes });
|
|
196
|
-
});
|
|
274
|
+
return this.withLifecycle(current, current.lifecycle, {
|
|
275
|
+
notifications: notifications.map((n) => n.id === id ? { ...n, consumed: true } : n),
|
|
276
|
+
});
|
|
277
|
+
}).then(() => undefined);
|
|
197
278
|
}
|
|
198
279
|
prepareNotificationDetached(id, model) {
|
|
199
|
-
if (model.trim().length === 0)
|
|
280
|
+
if (model.trim().length === 0)
|
|
200
281
|
throw new Error('Detached notification model must not be blank');
|
|
201
|
-
}
|
|
202
282
|
return this.mutate(async (current) => {
|
|
203
|
-
if (!current)
|
|
283
|
+
if (!current)
|
|
204
284
|
throw new Error(`Missing subagent lifecycle state: ${this.path}`);
|
|
205
|
-
}
|
|
206
285
|
const notifications = current.notifications ?? [];
|
|
207
|
-
const matched = notifications.find((
|
|
286
|
+
const matched = notifications.find((n) => n.id === id);
|
|
208
287
|
if (!matched || matched.accounting?.kind === 'detached')
|
|
209
288
|
return current;
|
|
210
|
-
return this.
|
|
211
|
-
|
|
212
|
-
...(current.detail === undefined ? {} : { detail: current.detail }),
|
|
213
|
-
...(current.result === undefined ? {} : { result: current.result }),
|
|
214
|
-
notifications: notifications.map((notification) => notification.id === id
|
|
289
|
+
return this.withLifecycle(current, current.lifecycle, {
|
|
290
|
+
notifications: notifications.map((n) => n.id === id
|
|
215
291
|
? {
|
|
216
|
-
...
|
|
292
|
+
...n,
|
|
217
293
|
accounting: {
|
|
218
294
|
kind: 'detached',
|
|
219
295
|
model,
|
|
220
296
|
delivered: false,
|
|
221
297
|
},
|
|
222
298
|
}
|
|
223
|
-
:
|
|
224
|
-
}
|
|
225
|
-
});
|
|
299
|
+
: n),
|
|
300
|
+
});
|
|
301
|
+
}).then(() => undefined);
|
|
226
302
|
}
|
|
227
303
|
confirmNotificationDetached(id) {
|
|
228
304
|
return this.mutate(async (current) => {
|
|
229
|
-
if (!current)
|
|
305
|
+
if (!current)
|
|
230
306
|
throw new Error(`Missing subagent lifecycle state: ${this.path}`);
|
|
231
|
-
}
|
|
232
307
|
const notifications = current.notifications ?? [];
|
|
233
|
-
const matched = notifications.find((
|
|
308
|
+
const matched = notifications.find((n) => n.id === id);
|
|
234
309
|
if (!matched?.accounting || matched.accounting.delivered)
|
|
235
310
|
return current;
|
|
236
|
-
return this.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
...notification,
|
|
243
|
-
accounting: { ...notification.accounting, delivered: true },
|
|
244
|
-
}
|
|
245
|
-
: notification),
|
|
246
|
-
}, { transcriptBytes: current.transcriptBytes });
|
|
247
|
-
});
|
|
311
|
+
return this.withLifecycle(current, current.lifecycle, {
|
|
312
|
+
notifications: notifications.map((n) => n.id === id && n.accounting
|
|
313
|
+
? { ...n, accounting: { ...n.accounting, delivered: true } }
|
|
314
|
+
: n),
|
|
315
|
+
});
|
|
316
|
+
}).then(() => undefined);
|
|
248
317
|
}
|
|
249
318
|
async matchesTranscript(record) {
|
|
250
319
|
if (record.transcriptBytes === undefined ||
|
|
251
|
-
this.transcriptPath === undefined)
|
|
320
|
+
this.transcriptPath === undefined)
|
|
252
321
|
return true;
|
|
253
|
-
}
|
|
254
322
|
try {
|
|
255
323
|
return (await stat(this.transcriptPath)).size === record.transcriptBytes;
|
|
256
324
|
}
|
|
@@ -260,40 +328,199 @@ export class SubagentLifecycleStore {
|
|
|
260
328
|
throw error;
|
|
261
329
|
}
|
|
262
330
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
331
|
+
baseRecord(lifecycle) {
|
|
332
|
+
return {
|
|
333
|
+
version: 2,
|
|
334
|
+
sessionId: this.sessionId,
|
|
335
|
+
agentId: this.agentId,
|
|
336
|
+
lifecycle,
|
|
337
|
+
updatedAt: new Date().toISOString(),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
withLifecycle(current, lifecycle, extra = {}) {
|
|
341
|
+
return {
|
|
342
|
+
version: 2,
|
|
343
|
+
sessionId: this.sessionId,
|
|
344
|
+
agentId: this.agentId,
|
|
345
|
+
lifecycle,
|
|
346
|
+
updatedAt: new Date().toISOString(),
|
|
347
|
+
...(extra.detail === undefined
|
|
348
|
+
? current.detail === undefined
|
|
349
|
+
? {}
|
|
350
|
+
: { detail: current.detail }
|
|
351
|
+
: { detail: extra.detail }),
|
|
352
|
+
...(extra.result === undefined
|
|
353
|
+
? current.result === undefined
|
|
354
|
+
? {}
|
|
355
|
+
: { result: current.result }
|
|
356
|
+
: { result: extra.result }),
|
|
357
|
+
...(extra.notifications === undefined
|
|
358
|
+
? current.notifications === undefined
|
|
359
|
+
? {}
|
|
360
|
+
: { notifications: current.notifications }
|
|
361
|
+
: { notifications: extra.notifications }),
|
|
362
|
+
...(current.transcriptBytes === undefined
|
|
363
|
+
? {}
|
|
364
|
+
: { transcriptBytes: current.transcriptBytes }),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async mutateOwned(lease, generation, operation, refreshTranscript = false) {
|
|
368
|
+
const record = await this.mutate(async (current) => {
|
|
369
|
+
if (!current)
|
|
370
|
+
throw new Error(`Missing subagent lifecycle state: ${this.path}`);
|
|
371
|
+
if (current.lifecycle.generation !== generation ||
|
|
372
|
+
current.lifecycle.owner?.token !== lease.token)
|
|
373
|
+
throw new Error('Lifecycle execution owner token is stale or missing');
|
|
374
|
+
return operation(current);
|
|
375
|
+
}, refreshTranscript);
|
|
376
|
+
return record.lifecycle;
|
|
377
|
+
}
|
|
378
|
+
mutate(operation, refreshTranscript = false) {
|
|
379
|
+
const queued = this.mutation.then(async () => {
|
|
380
|
+
const lock = await this.acquireMutationLock();
|
|
266
381
|
try {
|
|
267
|
-
|
|
382
|
+
const result = await operation(await this.readStored());
|
|
383
|
+
await this.writeStored(result, refreshTranscript);
|
|
384
|
+
return result;
|
|
268
385
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
throw error;
|
|
386
|
+
finally {
|
|
387
|
+
await lock.release();
|
|
272
388
|
}
|
|
389
|
+
});
|
|
390
|
+
this.mutation = queued.then(() => undefined, () => undefined);
|
|
391
|
+
return queued;
|
|
392
|
+
}
|
|
393
|
+
async acquireMutationLock() {
|
|
394
|
+
const deadline = Date.now() + 5_000;
|
|
395
|
+
for (;;) {
|
|
396
|
+
const lock = await this.mutationLock.tryAcquire();
|
|
397
|
+
if (lock)
|
|
398
|
+
return lock;
|
|
399
|
+
if (Date.now() >= deadline)
|
|
400
|
+
throw new Error(`Subagent lifecycle mutation is busy: ${this.path}`);
|
|
401
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async readStored() {
|
|
405
|
+
let source;
|
|
406
|
+
try {
|
|
407
|
+
source = await readFile(this.path, 'utf8');
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (error.code === 'ENOENT')
|
|
411
|
+
return null;
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
let value;
|
|
415
|
+
try {
|
|
416
|
+
value = JSON.parse(source);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
throw new Error(`Corrupt subagent lifecycle state: ${this.path}`, {
|
|
420
|
+
cause: error,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
424
|
+
throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
|
|
425
|
+
const record = value;
|
|
426
|
+
if (record.version === 1)
|
|
427
|
+
return this.normalizeLegacy(record);
|
|
428
|
+
let lifecycle;
|
|
429
|
+
try {
|
|
430
|
+
lifecycle = parseAgentLifecycleSnapshot(record.lifecycle);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
|
|
434
|
+
}
|
|
435
|
+
if (record.version !== 2 ||
|
|
436
|
+
record.sessionId !== this.sessionId ||
|
|
437
|
+
record.agentId !== this.agentId ||
|
|
438
|
+
typeof record.updatedAt !== 'string' ||
|
|
439
|
+
Number.isNaN(Date.parse(record.updatedAt)) ||
|
|
440
|
+
(record.transcriptBytes !== undefined &&
|
|
441
|
+
!isNonnegativeInteger(record.transcriptBytes)) ||
|
|
442
|
+
(record.detail !== undefined && typeof record.detail !== 'string') ||
|
|
443
|
+
(record.result !== undefined && !isRunResult(record.result)) ||
|
|
444
|
+
(record.notifications !== undefined &&
|
|
445
|
+
(!Array.isArray(record.notifications) ||
|
|
446
|
+
!record.notifications.every(isNotification))))
|
|
447
|
+
throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
|
|
448
|
+
return { ...record, lifecycle };
|
|
449
|
+
}
|
|
450
|
+
normalizeLegacy(record) {
|
|
451
|
+
if (record.sessionId !== this.sessionId ||
|
|
452
|
+
record.agentId !== this.agentId ||
|
|
453
|
+
typeof record.status !== 'string' ||
|
|
454
|
+
!['running', 'completed', 'failed', 'killed'].includes(record.status) ||
|
|
455
|
+
typeof record.updatedAt !== 'string' ||
|
|
456
|
+
Number.isNaN(Date.parse(record.updatedAt)) ||
|
|
457
|
+
(record.transcriptBytes !== undefined &&
|
|
458
|
+
!isNonnegativeInteger(record.transcriptBytes)) ||
|
|
459
|
+
(record.detail !== undefined && typeof record.detail !== 'string') ||
|
|
460
|
+
(record.result !== undefined && !isRunResult(record.result)) ||
|
|
461
|
+
(record.notifications !== undefined &&
|
|
462
|
+
(!Array.isArray(record.notifications) ||
|
|
463
|
+
!record.notifications.every(isNotification))))
|
|
464
|
+
throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
|
|
465
|
+
const status = record.status;
|
|
466
|
+
const state = status === 'killed'
|
|
467
|
+
? 'cancelled'
|
|
468
|
+
: status === 'running'
|
|
469
|
+
? 'orphaned'
|
|
470
|
+
: status;
|
|
471
|
+
const terminal = isTerminalLifecycleState(state);
|
|
472
|
+
let lifecycle;
|
|
473
|
+
try {
|
|
474
|
+
lifecycle = parseAgentLifecycleSnapshot({
|
|
475
|
+
generation: 1,
|
|
476
|
+
revision: 0,
|
|
477
|
+
state,
|
|
478
|
+
owner: null,
|
|
479
|
+
previousOwnerToken: null,
|
|
480
|
+
terminalAt: terminal ? record.updatedAt : null,
|
|
481
|
+
acceptance: 'pending',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
throw new Error(`Invalid subagent lifecycle state: ${this.path}`);
|
|
273
486
|
}
|
|
274
487
|
return {
|
|
275
|
-
version:
|
|
488
|
+
version: 2,
|
|
276
489
|
sessionId: this.sessionId,
|
|
277
490
|
agentId: this.agentId,
|
|
278
|
-
|
|
279
|
-
updatedAt:
|
|
280
|
-
...(transcriptBytes === undefined
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
...(
|
|
491
|
+
lifecycle,
|
|
492
|
+
updatedAt: record.updatedAt,
|
|
493
|
+
...(record.transcriptBytes === undefined
|
|
494
|
+
? {}
|
|
495
|
+
: { transcriptBytes: record.transcriptBytes }),
|
|
496
|
+
...(record.detail === undefined
|
|
497
|
+
? {}
|
|
498
|
+
: { detail: record.detail }),
|
|
499
|
+
...(record.result === undefined
|
|
284
500
|
? {}
|
|
285
|
-
: {
|
|
501
|
+
: { result: record.result }),
|
|
502
|
+
...(record.notifications === undefined
|
|
503
|
+
? {}
|
|
504
|
+
: {
|
|
505
|
+
notifications: record.notifications,
|
|
506
|
+
}),
|
|
286
507
|
};
|
|
287
508
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
509
|
+
async writeStored(record, refreshTranscript) {
|
|
510
|
+
let transcriptBytes = record.transcriptBytes;
|
|
511
|
+
if (refreshTranscript && this.transcriptPath !== undefined) {
|
|
512
|
+
try {
|
|
513
|
+
transcriptBytes = (await stat(this.transcriptPath)).size;
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
if (error.code !== 'ENOENT')
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const next = transcriptBytes === undefined ? record : { ...record, transcriptBytes };
|
|
521
|
+
await writeFileAtomically(this.path, `${JSON.stringify(next)}\n`, {
|
|
522
|
+
mode: 0o600,
|
|
294
523
|
});
|
|
295
|
-
this.mutation = operation.catch(() => undefined);
|
|
296
|
-
return operation;
|
|
297
524
|
}
|
|
298
525
|
}
|
|
299
526
|
//# sourceMappingURL=subagent-lifecycle-store.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function classifyTranscriptAppend(written, previousByteLength, encodedLine) {
|
|
2
|
+
const expectedEnd = previousByteLength + encodedLine.length;
|
|
3
|
+
if (written.length === expectedEnd &&
|
|
4
|
+
Buffer.from(written)
|
|
5
|
+
.subarray(previousByteLength, expectedEnd)
|
|
6
|
+
.equals(encodedLine))
|
|
7
|
+
return 'appended';
|
|
8
|
+
return 'interleaved-write';
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=transcript-file-append.js.map
|