@myagentroam/node 0.11.39 → 0.11.40
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/dist/connector.js +176 -21
- package/dist/database.js +35 -11
- package/dist/main.js +12 -1
- package/dist/marctl-guidance.js +18 -27
- package/dist/migrations/v022.js +58 -0
- package/dist/runner/abstract-runner.js +14 -0
- package/dist/runner/claude/tool-normalizer.js +6 -1
- package/dist/runner/codex/conversation-parser.js +23 -4
- package/dist/runner/codex/managed-run-controller.js +4 -2
- package/dist/runner/codex/tool-normalizer.js +9 -0
- package/dist/runner/execution-presentation.js +56 -0
- package/dist/runner/mar-agent/agent-network-extension.js +168 -0
- package/dist/runner/mar-agent/agent-network-wait-output.js +72 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +233 -119
- package/dist/runner/mar-agent/managed-run-controller.js +5 -0
- package/dist/runner/mar-agent/marctl-skill.js +3 -3
- package/dist/runner/mar-agent/runtime-model-factory.js +1 -0
- package/dist/runner/mar-agent/session-runtime.js +34 -1
- package/dist/runner/mar-agent-runner.js +36 -0
- package/dist/runner/opencode/tool-normalizer.js +6 -1
- package/dist/runtime-state.js +57 -0
- package/dist/service/agent-network-bindings.js +483 -0
- package/dist/service/agent-network-goal.js +19 -0
- package/dist/service/agent-network-invoke.js +179 -0
- package/dist/service/agent-network-observations.js +377 -0
- package/dist/service/agent-network-service.js +315 -0
- package/dist/service/agent-network-wait.js +130 -0
- package/dist/service/composer-message-input.js +9 -2
- package/dist/service/continuous-run-coordinator.js +24 -1
- package/dist/service/goal-continuation-coordinator.js +12 -2
- package/dist/service/marctl-caller.js +80 -0
- package/dist/service/marctl-execution-credential-control.js +65 -0
- package/dist/service/model-usage-service.js +9 -0
- package/dist/service/node-control-message-service.js +10 -0
- package/dist/service/node-request-service.js +52 -7
- package/dist/service/node-upgrade-coordinator.js +79 -8
- package/dist/service/node-upgrade-credential-control.js +57 -0
- package/dist/service/node-upgrade-reporter.js +108 -0
- package/dist/service/run-event-service.js +8 -0
- package/dist/service/run-workbench-service.js +11 -0
- package/dist/service/supervised-node-child.js +153 -0
- package/dist/service/supervisor-upgrade-handoff.js +104 -0
- package/dist/session/native-session-projection-service.js +69 -6
- package/dist/session/operation-admission.js +7 -0
- package/dist/session/runtime/conversation-projection.js +25 -3
- package/dist/session/session-catalog-service.js +156 -32
- package/dist/session/session-lifecycle-service.js +68 -42
- package/dist/session/session-message-service.js +16 -6
- package/dist/supervisor.js +209 -20
- package/dist/terminal.js +41 -21
- package/package.json +4 -4
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { terminateProcessTree } from '../process-tree.js';
|
|
3
|
+
/** Wait for the child process itself, not inherited output handles held by descendants. */
|
|
4
|
+
export function runSupervisedNodeChild(entrypoint, configPath, accept, accepted, upgradeAttemptId) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const child = spawn(process.execPath, [entrypoint, 'run', '--config', configPath], {
|
|
7
|
+
windowsHide: true,
|
|
8
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
9
|
+
env: {
|
|
10
|
+
...process.env,
|
|
11
|
+
MAR_NODE_SUPERVISED: '1',
|
|
12
|
+
...(upgradeAttemptId === undefined ? {} : { MAR_NODE_UPGRADE_ATTEMPT_ID: upgradeAttemptId })
|
|
13
|
+
},
|
|
14
|
+
detached: process.platform !== 'win32'
|
|
15
|
+
});
|
|
16
|
+
let done = false;
|
|
17
|
+
let signalled = false;
|
|
18
|
+
let channelId;
|
|
19
|
+
let accepting = false;
|
|
20
|
+
let handoff;
|
|
21
|
+
let forced = false;
|
|
22
|
+
let reclaiming = false;
|
|
23
|
+
let reclaimed = false;
|
|
24
|
+
let exited;
|
|
25
|
+
let deadline;
|
|
26
|
+
let reapDeadline;
|
|
27
|
+
const finish = (code, reclaimFailed = false) => {
|
|
28
|
+
if (done)
|
|
29
|
+
return;
|
|
30
|
+
done = true;
|
|
31
|
+
if (deadline !== undefined)
|
|
32
|
+
clearTimeout(deadline);
|
|
33
|
+
if (reapDeadline !== undefined)
|
|
34
|
+
clearTimeout(reapDeadline);
|
|
35
|
+
process.off('SIGINT', onSigint);
|
|
36
|
+
process.off('SIGTERM', onSigterm);
|
|
37
|
+
if (child.connected)
|
|
38
|
+
child.disconnect();
|
|
39
|
+
if (reclaimFailed)
|
|
40
|
+
child.unref();
|
|
41
|
+
resolve({
|
|
42
|
+
code,
|
|
43
|
+
signalled,
|
|
44
|
+
...(handoff === undefined ? {} : { handoff }),
|
|
45
|
+
...(forced ? { forced: true } : {}),
|
|
46
|
+
...(reclaimFailed ? { reclaimFailed: true } : {})
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
const forward = (signal) => {
|
|
50
|
+
if (signalled)
|
|
51
|
+
return;
|
|
52
|
+
signalled = true;
|
|
53
|
+
child.kill(signal);
|
|
54
|
+
};
|
|
55
|
+
const onSigint = () => forward('SIGINT');
|
|
56
|
+
const onSigterm = () => forward('SIGTERM');
|
|
57
|
+
process.on('SIGINT', onSigint);
|
|
58
|
+
process.on('SIGTERM', onSigterm);
|
|
59
|
+
child.on('error', () => {
|
|
60
|
+
if (child.pid === undefined)
|
|
61
|
+
finish(null);
|
|
62
|
+
});
|
|
63
|
+
child.once('exit', (code) => {
|
|
64
|
+
exited = { code };
|
|
65
|
+
if (!reclaiming || reclaimed)
|
|
66
|
+
finish(code);
|
|
67
|
+
});
|
|
68
|
+
child.on('message', (message) => {
|
|
69
|
+
if (done || signalled || typeof message !== 'object' || message === null)
|
|
70
|
+
return;
|
|
71
|
+
const value = message;
|
|
72
|
+
if (value['type'] === 'node-upgrade-hello' &&
|
|
73
|
+
channelId === undefined &&
|
|
74
|
+
typeof value['channelId'] === 'string') {
|
|
75
|
+
channelId = value['channelId'];
|
|
76
|
+
send(child, { type: 'node-upgrade-ready', channelId });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (value['type'] !== 'node-upgrade-handoff' ||
|
|
80
|
+
value['channelId'] !== channelId ||
|
|
81
|
+
channelId === undefined ||
|
|
82
|
+
accepting ||
|
|
83
|
+
handoff !== undefined ||
|
|
84
|
+
typeof value['id'] !== 'string' ||
|
|
85
|
+
typeof value['attemptId'] !== 'string' ||
|
|
86
|
+
typeof value['targetVersion'] !== 'string' ||
|
|
87
|
+
typeof value['credential'] !== 'string')
|
|
88
|
+
return;
|
|
89
|
+
const id = value['id'];
|
|
90
|
+
const request = {
|
|
91
|
+
attemptId: value['attemptId'],
|
|
92
|
+
targetVersion: value['targetVersion'],
|
|
93
|
+
credential: value['credential']
|
|
94
|
+
};
|
|
95
|
+
accepting = true;
|
|
96
|
+
void accept(request)
|
|
97
|
+
.then((ok) => {
|
|
98
|
+
if (done || signalled)
|
|
99
|
+
return;
|
|
100
|
+
if (!ok) {
|
|
101
|
+
accepting = false;
|
|
102
|
+
send(child, { type: 'node-upgrade-ack', channelId, id, accepted: false });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
accepted(request);
|
|
106
|
+
handoff = request;
|
|
107
|
+
deadline = setTimeout(() => {
|
|
108
|
+
if (done || signalled)
|
|
109
|
+
return;
|
|
110
|
+
forced = true;
|
|
111
|
+
reclaiming = true;
|
|
112
|
+
reapDeadline = setTimeout(() => finish(null, true), 10_000);
|
|
113
|
+
void terminateProcessTree(child)
|
|
114
|
+
.then(() => {
|
|
115
|
+
reclaimed = true;
|
|
116
|
+
if (exited !== undefined)
|
|
117
|
+
finish(exited.code);
|
|
118
|
+
})
|
|
119
|
+
.catch(() => finish(null, true));
|
|
120
|
+
if (process.platform !== 'win32' && child.pid !== undefined) {
|
|
121
|
+
try {
|
|
122
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
try {
|
|
126
|
+
child.kill('SIGKILL');
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* Wait for exit or timeout. */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, 25_000);
|
|
134
|
+
send(child, { type: 'node-upgrade-ack', channelId, id, accepted: true });
|
|
135
|
+
})
|
|
136
|
+
.catch(() => {
|
|
137
|
+
accepting = false;
|
|
138
|
+
if (!done && !signalled)
|
|
139
|
+
send(child, { type: 'node-upgrade-ack', channelId, id, accepted: false });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function send(child, message) {
|
|
145
|
+
if (!child.connected)
|
|
146
|
+
return;
|
|
147
|
+
try {
|
|
148
|
+
child.send(message, () => undefined);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* An absent ACK leaves the child blocked. */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/** Parent capability is established once at startup; a handoff is never retried. */
|
|
3
|
+
export class SupervisorUpgradeHandoff {
|
|
4
|
+
parent;
|
|
5
|
+
channelId = randomUUID();
|
|
6
|
+
supported = false;
|
|
7
|
+
uncertain = false;
|
|
8
|
+
handshakeResolve;
|
|
9
|
+
pending;
|
|
10
|
+
constructor(parent = process) {
|
|
11
|
+
this.parent = parent;
|
|
12
|
+
parent.on('message', this.onMessage);
|
|
13
|
+
}
|
|
14
|
+
async handshake() {
|
|
15
|
+
if (!this.parent.connected || this.parent.send === undefined)
|
|
16
|
+
return false;
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
this.handshakeResolve = undefined;
|
|
20
|
+
resolve(false);
|
|
21
|
+
}, 1_000);
|
|
22
|
+
timer.unref();
|
|
23
|
+
this.handshakeResolve = (supported) => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
this.handshakeResolve = undefined;
|
|
26
|
+
resolve(supported);
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
this.parent.send({ type: 'node-upgrade-hello', channelId: this.channelId }, (error) => {
|
|
30
|
+
if (error !== null)
|
|
31
|
+
this.handshakeResolve?.(false);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
this.handshakeResolve?.(false);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async handoff(input) {
|
|
40
|
+
if (!this.supported || !this.parent.connected || this.parent.send === undefined)
|
|
41
|
+
throw new Error('NODE_UPGRADE_HANDOFF_UNAVAILABLE');
|
|
42
|
+
if (this.uncertain)
|
|
43
|
+
throw new Error('NODE_UPGRADE_HANDOFF_UNCERTAIN');
|
|
44
|
+
if (this.pending !== undefined)
|
|
45
|
+
throw new Error('NODE_UPGRADE_HANDOFF_ALREADY_PENDING');
|
|
46
|
+
const id = randomUUID();
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
this.pending = undefined;
|
|
50
|
+
this.uncertain = true;
|
|
51
|
+
reject(new Error('NODE_UPGRADE_HANDOFF_UNCERTAIN'));
|
|
52
|
+
}, 5_000);
|
|
53
|
+
timer.unref();
|
|
54
|
+
this.pending = {
|
|
55
|
+
id,
|
|
56
|
+
resolve: () => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
this.pending = undefined;
|
|
59
|
+
resolve();
|
|
60
|
+
},
|
|
61
|
+
reject: (error) => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
this.pending = undefined;
|
|
64
|
+
reject(error);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
this.parent.send({ type: 'node-upgrade-handoff', channelId: this.channelId, id, ...input }, (error) => {
|
|
69
|
+
if (error !== null && this.pending?.id === id)
|
|
70
|
+
this.pending.reject(new Error('NODE_UPGRADE_HANDOFF_UNAVAILABLE'));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
this.pending?.reject(new Error('NODE_UPGRADE_HANDOFF_UNAVAILABLE'));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
close() {
|
|
79
|
+
this.parent.off('message', this.onMessage);
|
|
80
|
+
this.handshakeResolve?.(false);
|
|
81
|
+
this.pending?.reject(new Error('NODE_UPGRADE_HANDOFF_UNCERTAIN'));
|
|
82
|
+
}
|
|
83
|
+
onMessage = (message) => {
|
|
84
|
+
if (typeof message !== 'object' || message === null)
|
|
85
|
+
return;
|
|
86
|
+
const value = message;
|
|
87
|
+
if (value['channelId'] !== this.channelId)
|
|
88
|
+
return;
|
|
89
|
+
if (value['type'] === 'node-upgrade-ready' && this.handshakeResolve !== undefined) {
|
|
90
|
+
this.supported = true;
|
|
91
|
+
this.handshakeResolve?.(true);
|
|
92
|
+
this.handshakeResolve = undefined;
|
|
93
|
+
}
|
|
94
|
+
else if (value['type'] === 'node-upgrade-ack') {
|
|
95
|
+
const pending = this.pending;
|
|
96
|
+
if (pending === undefined || value['id'] !== pending.id)
|
|
97
|
+
return;
|
|
98
|
+
if (value['accepted'] === true)
|
|
99
|
+
pending.resolve();
|
|
100
|
+
else
|
|
101
|
+
pending.reject(new Error('NODE_UPGRADE_HANDOFF_REJECTED'));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -22,9 +22,19 @@ export class NativeSessionProjectionService {
|
|
|
22
22
|
createProjection: (input) => this.project(input)
|
|
23
23
|
}, workspace);
|
|
24
24
|
const ordered = deduplicateDirectSessions(sessions);
|
|
25
|
-
if (!cleanup)
|
|
25
|
+
if (!cleanup) {
|
|
26
26
|
this.remember(ordered);
|
|
27
|
-
|
|
27
|
+
return ordered.flatMap((session) => {
|
|
28
|
+
const accepted = this.directNative.get(session.id);
|
|
29
|
+
return accepted === undefined ? [] : [accepted];
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return ordered.flatMap((session) => {
|
|
33
|
+
const record = database.getSessionRecord(session.id);
|
|
34
|
+
if (record === undefined && session.parentSessionId != null)
|
|
35
|
+
return [];
|
|
36
|
+
return [{ ...session, parentSessionId: record?.metadata.parentSessionId ?? null }];
|
|
37
|
+
});
|
|
28
38
|
}
|
|
29
39
|
async discoverCodex(workspaceId) {
|
|
30
40
|
const sessions = await this.discover('codex', workspaceId);
|
|
@@ -209,7 +219,9 @@ export class NativeSessionProjectionService {
|
|
|
209
219
|
access: metadata?.access ?? null,
|
|
210
220
|
customTitle: metadata?.customTitle ?? null,
|
|
211
221
|
sessionPrompt: metadata?.sessionPrompt ?? null,
|
|
212
|
-
parentSessionId: metadata
|
|
222
|
+
parentSessionId: metadata === undefined
|
|
223
|
+
? (input.parentSessionId ?? null)
|
|
224
|
+
: (metadata.parentSessionId ?? null),
|
|
213
225
|
runnerTitle,
|
|
214
226
|
pinnedAt: metadata?.pinnedAt ?? null,
|
|
215
227
|
pinOrder: metadata?.pinOrder ?? null,
|
|
@@ -227,7 +239,57 @@ export class NativeSessionProjectionService {
|
|
|
227
239
|
}
|
|
228
240
|
remember(sessions) {
|
|
229
241
|
const database = this.options.database();
|
|
230
|
-
|
|
242
|
+
const candidates = new Map(sessions.map((session) => [session.id, session]));
|
|
243
|
+
const validParent = (session) => {
|
|
244
|
+
const visited = new Set([session.id]);
|
|
245
|
+
let parentId = session.parentSessionId ?? null;
|
|
246
|
+
let depth = 1;
|
|
247
|
+
while (parentId !== null) {
|
|
248
|
+
if (visited.has(parentId) || ++depth > 3)
|
|
249
|
+
return false;
|
|
250
|
+
visited.add(parentId);
|
|
251
|
+
const stored = database?.getSessionRecord(parentId);
|
|
252
|
+
const candidate = stored === undefined ? candidates.get(parentId) : undefined;
|
|
253
|
+
if ((stored?.workspaceId ?? candidate?.workspaceId) !== session.workspaceId)
|
|
254
|
+
return false;
|
|
255
|
+
parentId =
|
|
256
|
+
stored === undefined
|
|
257
|
+
? (candidate?.parentSessionId ?? null)
|
|
258
|
+
: (stored.metadata.parentSessionId ?? null);
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
};
|
|
262
|
+
const valid = new Map(sessions.map((session) => [session.id, validParent(session)]));
|
|
263
|
+
const accepted = sessions.flatMap((proposed) => {
|
|
264
|
+
const metadata = database === undefined || proposed.externalSessionId === null
|
|
265
|
+
? undefined
|
|
266
|
+
: database.getNativeSessionMetadata(proposed.runner, proposed.externalSessionId, proposed.workspaceId);
|
|
267
|
+
if (metadata === undefined &&
|
|
268
|
+
proposed.parentSessionId != null &&
|
|
269
|
+
(database === undefined || !valid.get(proposed.id)))
|
|
270
|
+
return [];
|
|
271
|
+
const parentSessionId = metadata !== undefined
|
|
272
|
+
? (metadata.parentSessionId ?? null)
|
|
273
|
+
: database !== undefined && valid.get(proposed.id)
|
|
274
|
+
? (proposed.parentSessionId ?? null)
|
|
275
|
+
: null;
|
|
276
|
+
return [{ ...proposed, parentSessionId }];
|
|
277
|
+
});
|
|
278
|
+
const acceptedById = new Map(accepted.map((session) => [session.id, session]));
|
|
279
|
+
const ordered = [];
|
|
280
|
+
const visited = new Set();
|
|
281
|
+
const visit = (session) => {
|
|
282
|
+
if (visited.has(session.id))
|
|
283
|
+
return;
|
|
284
|
+
visited.add(session.id);
|
|
285
|
+
const parent = session.parentSessionId == null ? undefined : acceptedById.get(session.parentSessionId);
|
|
286
|
+
if (parent !== undefined)
|
|
287
|
+
visit(parent);
|
|
288
|
+
ordered.push(session);
|
|
289
|
+
};
|
|
290
|
+
for (const session of accepted)
|
|
291
|
+
visit(session);
|
|
292
|
+
for (const session of ordered) {
|
|
231
293
|
this.directNative.set(session.id, session);
|
|
232
294
|
if (database === undefined || session.externalSessionId === null)
|
|
233
295
|
continue;
|
|
@@ -239,6 +301,9 @@ export class NativeSessionProjectionService {
|
|
|
239
301
|
runner: session.runner,
|
|
240
302
|
externalSessionId: session.externalSessionId,
|
|
241
303
|
workspaceId: session.workspaceId,
|
|
304
|
+
...(metadata === undefined && session.parentSessionId != null
|
|
305
|
+
? { parentSessionId: session.parentSessionId }
|
|
306
|
+
: {}),
|
|
242
307
|
...(title === undefined || metadata?.runnerTitlePending ? {} : { runnerTitle: title })
|
|
243
308
|
});
|
|
244
309
|
}
|
|
@@ -327,8 +392,6 @@ export class NativeSessionProjectionService {
|
|
|
327
392
|
return ids;
|
|
328
393
|
}
|
|
329
394
|
forget(session, knownNativeIds) {
|
|
330
|
-
if (session.externalSessionId === null)
|
|
331
|
-
return;
|
|
332
395
|
const projectionIds = new Set([session.id]);
|
|
333
396
|
const nativeIds = new Set(knownNativeIds);
|
|
334
397
|
const runner = this.options.runners.require(session.runner);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Internal-only signal: caller cancellation is checked until the Session owner accepts work. */
|
|
2
|
+
export const SESSION_ADMISSION_SIGNAL = Symbol('session-admission-signal');
|
|
3
|
+
export function checkSessionAdmission(input) {
|
|
4
|
+
const signal = input[SESSION_ADMISSION_SIGNAL];
|
|
5
|
+
if (signal instanceof AbortSignal)
|
|
6
|
+
signal.throwIfAborted();
|
|
7
|
+
}
|
|
@@ -218,7 +218,10 @@ export function conversationItemFromEvent(event) {
|
|
|
218
218
|
: {}),
|
|
219
219
|
...(permissionStop.success ? { permissionStop: permissionStop.data } : {}),
|
|
220
220
|
merge: event.payload.merge === true,
|
|
221
|
-
...(append === 'sections' ||
|
|
221
|
+
...(append === 'sections' ||
|
|
222
|
+
append === 'outputPreview' ||
|
|
223
|
+
append === 'outputSummary' ||
|
|
224
|
+
append === 'output'
|
|
222
225
|
? { append }
|
|
223
226
|
: {})
|
|
224
227
|
};
|
|
@@ -257,7 +260,7 @@ function projectableConversationStatus(value) {
|
|
|
257
260
|
'INTERRUPTED'
|
|
258
261
|
].includes(value);
|
|
259
262
|
}
|
|
260
|
-
function mergeConversationItemPayload(existing, next) {
|
|
263
|
+
export function mergeConversationItemPayload(existing, next) {
|
|
261
264
|
const base = next.merge && isRecord(existing) ? existing : {};
|
|
262
265
|
const value = { ...base, ...next.payload };
|
|
263
266
|
if (next.append === 'sections') {
|
|
@@ -287,11 +290,30 @@ function mergeConversationItemPayload(existing, next) {
|
|
|
287
290
|
const incoming = typeof next.payload.outputSummary === 'string' ? next.payload.outputSummary : '';
|
|
288
291
|
value.outputSummary = appendProjectionText(prior, incoming);
|
|
289
292
|
}
|
|
293
|
+
if (next.append === 'output' &&
|
|
294
|
+
isRecord(next.payload.output) &&
|
|
295
|
+
typeof next.payload.output.text === 'string') {
|
|
296
|
+
const prior = isRecord(base.output) && typeof base.output.text === 'string' ? base.output : undefined;
|
|
297
|
+
const text = `${prior?.text ?? ''}${next.payload.output.text}`;
|
|
298
|
+
value.output = {
|
|
299
|
+
text: boundedOutputTail(text),
|
|
300
|
+
truncated: prior?.truncated === true || next.payload.output.truncated === true || text.length > 10_000
|
|
301
|
+
};
|
|
302
|
+
if (typeof next.payload.outputPreview === 'string') {
|
|
303
|
+
value.outputPreview = boundedOutputTail(`${typeof base.outputPreview === 'string' ? base.outputPreview : ''}${next.payload.outputPreview}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
290
306
|
return value;
|
|
291
307
|
}
|
|
308
|
+
function boundedOutputTail(text) {
|
|
309
|
+
if (text.length <= 10_000)
|
|
310
|
+
return text;
|
|
311
|
+
const tail = text.slice(-10_000);
|
|
312
|
+
return /^[\uDC00-\uDFFF]/u.test(tail) ? tail.slice(1) : tail;
|
|
313
|
+
}
|
|
292
314
|
function appendProjectionText(previous, next) {
|
|
293
315
|
const text = `${previous}${next}`;
|
|
294
|
-
return text.length <= 10_000 ? text : `${text.slice(0, 9_999)}…`;
|
|
316
|
+
return text.length <= 10_000 ? text : `${text.slice(0, 9_999).replace(/[\uD800-\uDBFF]$/, '')}…`;
|
|
295
317
|
}
|
|
296
318
|
export function activeConversationItem(status) {
|
|
297
319
|
return status === 'PENDING' || status === 'IN_PROGRESS';
|
|
@@ -57,34 +57,47 @@ export class SessionCatalogService {
|
|
|
57
57
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
58
58
|
if (typeof input.sessionId !== 'string')
|
|
59
59
|
throw new Error('SESSION_INVALID');
|
|
60
|
+
if (input.includeAncestors !== undefined && typeof input.includeAncestors !== 'boolean')
|
|
61
|
+
throw new Error('SESSION_INVALID');
|
|
60
62
|
const limit = input.limit === undefined ? 100 : input.limit;
|
|
61
63
|
if (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
62
64
|
throw new Error('SESSION_INVALID');
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
if (root === undefined)
|
|
65
|
+
const database = this.options.database();
|
|
66
|
+
let root = database.getSessionRecord(input.sessionId);
|
|
67
|
+
if (root === undefined || root.workspaceId !== input.workspaceId)
|
|
66
68
|
throw new Error('SESSION_NOT_FOUND');
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
const ancestors = new Set([root.id]);
|
|
70
|
+
while (input.includeAncestors === true && typeof root.metadata.parentSessionId === 'string') {
|
|
71
|
+
const parent = database.getSessionRecord(root.metadata.parentSessionId);
|
|
72
|
+
if (parent === undefined ||
|
|
73
|
+
parent.workspaceId !== input.workspaceId ||
|
|
74
|
+
ancestors.has(parent.id))
|
|
75
|
+
throw new Error('SESSION_PARENT_INVALID');
|
|
76
|
+
ancestors.add(parent.id);
|
|
77
|
+
root = parent;
|
|
74
78
|
}
|
|
75
79
|
const queue = [root];
|
|
76
80
|
const visited = new Set([root.id]);
|
|
77
81
|
const nodes = [];
|
|
78
82
|
for (let index = 0; index < queue.length && index < limit; index += 1) {
|
|
79
83
|
const session = queue[index];
|
|
84
|
+
const active = this.options.runtime.getAgentSession(session.id);
|
|
85
|
+
const live = active === undefined ? undefined : this.options.present(active);
|
|
80
86
|
nodes.push({
|
|
81
87
|
id: session.id,
|
|
82
|
-
parentSessionId: session.parentSessionId ?? null,
|
|
83
|
-
title:
|
|
88
|
+
parentSessionId: session.metadata.parentSessionId ?? null,
|
|
89
|
+
title: live?.customTitle ??
|
|
90
|
+
session.metadata.customTitle ??
|
|
91
|
+
live?.runnerTitle ??
|
|
92
|
+
session.metadata.runnerTitle ??
|
|
93
|
+
'Untitled session',
|
|
84
94
|
runner: session.runner,
|
|
85
|
-
|
|
95
|
+
activity: live && 'activity' in live ? live.activity : 'IDLE'
|
|
86
96
|
});
|
|
87
|
-
|
|
97
|
+
const children = [...database.listChildSessionRecords(input.workspaceId, session.id)];
|
|
98
|
+
if (input.includeAncestors === true)
|
|
99
|
+
children.sort((left, right) => Number(ancestors.has(right.id)) - Number(ancestors.has(left.id)));
|
|
100
|
+
for (const child of children) {
|
|
88
101
|
if (visited.has(child.id))
|
|
89
102
|
continue;
|
|
90
103
|
visited.add(child.id);
|
|
@@ -302,13 +315,98 @@ export class SessionCatalogService {
|
|
|
302
315
|
if (session === undefined)
|
|
303
316
|
throw new Error('SESSION_NOT_FOUND');
|
|
304
317
|
try {
|
|
305
|
-
await this.
|
|
318
|
+
await this.removeTree(session);
|
|
306
319
|
return { deleted: true };
|
|
307
320
|
}
|
|
308
321
|
finally {
|
|
309
322
|
this.options.invalidateWorkspaceSessions(session.workspaceId);
|
|
310
323
|
}
|
|
311
324
|
}
|
|
325
|
+
async removeTree(root) {
|
|
326
|
+
if (root.externalSessionId === null)
|
|
327
|
+
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
328
|
+
const database = this.options.database();
|
|
329
|
+
const live = this.options.runtime
|
|
330
|
+
.listAgentSessions()
|
|
331
|
+
.filter((session) => session.workspaceId === root.workspaceId);
|
|
332
|
+
const liveChildren = new Map();
|
|
333
|
+
for (const session of live) {
|
|
334
|
+
if (session.parentSessionId == null)
|
|
335
|
+
continue;
|
|
336
|
+
const children = liveChildren.get(session.parentSessionId) ?? [];
|
|
337
|
+
children.push(session.externalSessionId ?? session.id);
|
|
338
|
+
liveChildren.set(session.parentSessionId, children);
|
|
339
|
+
}
|
|
340
|
+
const ids = [root.externalSessionId];
|
|
341
|
+
const seen = new Set(ids);
|
|
342
|
+
const parents = new Map();
|
|
343
|
+
const releases = [];
|
|
344
|
+
const locked = new Set();
|
|
345
|
+
const lock = (id) => {
|
|
346
|
+
const actual = this.options.runtime.getAgentSession(id)?.id ?? id;
|
|
347
|
+
if (locked.has(actual))
|
|
348
|
+
return;
|
|
349
|
+
releases.push(this.options.runtime.beginSessionHistoryMutation(actual, { allowQueued: true }));
|
|
350
|
+
locked.add(actual);
|
|
351
|
+
};
|
|
352
|
+
try {
|
|
353
|
+
lock(root.id);
|
|
354
|
+
for (let index = 0; index < ids.length; index += 1) {
|
|
355
|
+
const parent = ids[index];
|
|
356
|
+
const children = [
|
|
357
|
+
...database.listChildSessionRecords(root.workspaceId, parent).map((record) => record.id),
|
|
358
|
+
...(liveChildren.get(parent) ?? [])
|
|
359
|
+
];
|
|
360
|
+
for (const child of children) {
|
|
361
|
+
if (seen.has(child)) {
|
|
362
|
+
if (parents.get(child) !== parent)
|
|
363
|
+
throw new Error('SESSION_RELATION_INVALID');
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
seen.add(child);
|
|
367
|
+
parents.set(child, parent);
|
|
368
|
+
lock(child);
|
|
369
|
+
ids.push(child);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const sessions = [];
|
|
373
|
+
for (const id of ids) {
|
|
374
|
+
const session = this.options.runtime.getAgentSession(id) ??
|
|
375
|
+
(await this.options.projection.resolve(id, true));
|
|
376
|
+
if (session === undefined || session.workspaceId !== root.workspaceId)
|
|
377
|
+
throw new Error('SESSION_NOT_FOUND');
|
|
378
|
+
sessions.push(session);
|
|
379
|
+
}
|
|
380
|
+
// Validate every member before performing any irreversible native deletion.
|
|
381
|
+
for (const session of sessions)
|
|
382
|
+
await this.assertRemovable(session);
|
|
383
|
+
const batches = new Map();
|
|
384
|
+
for (const session of [...sessions].reverse()) {
|
|
385
|
+
if (session.externalSessionId === null)
|
|
386
|
+
continue;
|
|
387
|
+
const runner = this.options.runners.require(session.runner);
|
|
388
|
+
if (runner.supportsNativeBatchRemoval?.() === true) {
|
|
389
|
+
const members = batches.get(session.runner) ?? [];
|
|
390
|
+
members.push(session);
|
|
391
|
+
batches.set(session.runner, members);
|
|
392
|
+
}
|
|
393
|
+
else
|
|
394
|
+
await this.removeNativeHistories(session, this.options.nativeIds(session));
|
|
395
|
+
}
|
|
396
|
+
for (const [runner, members] of batches)
|
|
397
|
+
await this.options.runners.require(runner).removeNativeMembers(members);
|
|
398
|
+
// Preserve every relationship if any native owner fails, so retries cannot
|
|
399
|
+
// lose sight of descendants whose native cleanup was already attempted.
|
|
400
|
+
for (const session of sessions.reverse()) {
|
|
401
|
+
this.options.database().deleteSessionRecord(session.id);
|
|
402
|
+
this.options.forget(session, this.options.nativeIds(session));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
for (const release of releases.reverse())
|
|
407
|
+
release();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
312
410
|
async cleanupWorkspaceSessions(input) {
|
|
313
411
|
const inactiveDays = input.inactiveDays;
|
|
314
412
|
if (typeof input.workspaceId !== 'string' ||
|
|
@@ -360,6 +458,7 @@ export class SessionCatalogService {
|
|
|
360
458
|
candidates.set(session.id, session);
|
|
361
459
|
}
|
|
362
460
|
this.mergePersisted(candidates, workspaceId, false);
|
|
461
|
+
const candidateParents = new Set([...candidates.values()].flatMap((candidate) => candidate.parentSessionId === null ? [] : [candidate.parentSessionId]));
|
|
363
462
|
candidateCount = candidates.size;
|
|
364
463
|
for (const session of candidates.values()) {
|
|
365
464
|
budget.check();
|
|
@@ -373,6 +472,14 @@ export class SessionCatalogService {
|
|
|
373
472
|
session.lastActivityAt > 0 &&
|
|
374
473
|
session.lastActivityAt >= cutoff) ||
|
|
375
474
|
(session.pinOrder !== null && session.pinOrder !== undefined) ||
|
|
475
|
+
(session.externalSessionId !== null &&
|
|
476
|
+
(this.options.database().listChildSessionRecords(workspaceId, session.externalSessionId)
|
|
477
|
+
.length > 0 ||
|
|
478
|
+
candidateParents.has(session.externalSessionId) ||
|
|
479
|
+
this.options.runtime
|
|
480
|
+
.listAgentSessions()
|
|
481
|
+
.some((child) => child.workspaceId === workspaceId &&
|
|
482
|
+
child.parentSessionId === session.externalSessionId))) ||
|
|
376
483
|
this.options.isWatching(session.id) ||
|
|
377
484
|
this.options.isRecovering?.(session.id) ||
|
|
378
485
|
this.options.runtime.sessionQueue(session.id).itemIds.length > 0 ||
|
|
@@ -452,6 +559,39 @@ export class SessionCatalogService {
|
|
|
452
559
|
lastActivityAt: latestSessionActivityAt(projected.lastActivityAt, native.lastActivityAt)
|
|
453
560
|
};
|
|
454
561
|
}
|
|
562
|
+
async assertRemovable(session) {
|
|
563
|
+
if (this.options.isRecovering?.(session.id))
|
|
564
|
+
throw new Error('SESSION_RESTART_PENDING');
|
|
565
|
+
if (this.options.runtime
|
|
566
|
+
.listRunsForSession(session.id)
|
|
567
|
+
.some((run) => !isTerminalRunStatus(run.status)))
|
|
568
|
+
throw new Error('SESSION_ACTIVE');
|
|
569
|
+
if (session.externalSessionId !== null && session.nativeControl === 'EXTERNAL') {
|
|
570
|
+
await this.options.refreshActivity(session);
|
|
571
|
+
if (this.options.externalActivity(session.id) === 'BUSY')
|
|
572
|
+
throw new Error('SESSION_ACTIVE');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async removeNativeHistories(session, nativeIds) {
|
|
576
|
+
const runner = this.options.runners.require(session.runner);
|
|
577
|
+
let deleted = false;
|
|
578
|
+
for (const externalSessionId of nativeIds) {
|
|
579
|
+
try {
|
|
580
|
+
const result = await runner.removeNative(session, externalSessionId);
|
|
581
|
+
if (result === true)
|
|
582
|
+
deleted = true;
|
|
583
|
+
else if (result === false) {
|
|
584
|
+
await removeNativeSession(session.runner, session.cwd, externalSessionId);
|
|
585
|
+
deleted = true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
if (!(error instanceof Error) || error.message !== 'NATIVE_TRANSCRIPT_UNAVAILABLE')
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return deleted;
|
|
594
|
+
}
|
|
455
595
|
async removeResolved(session, requireCleanupSafety = false, budget) {
|
|
456
596
|
if (session.externalSessionId === null)
|
|
457
597
|
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
@@ -493,23 +633,7 @@ export class SessionCatalogService {
|
|
|
493
633
|
this.options.forget(session, nativeIds);
|
|
494
634
|
return 'deleted';
|
|
495
635
|
}
|
|
496
|
-
const
|
|
497
|
-
let deleted = false;
|
|
498
|
-
for (const externalSessionId of nativeIds) {
|
|
499
|
-
try {
|
|
500
|
-
const result = await runner.removeNative(session, externalSessionId);
|
|
501
|
-
if (result === true)
|
|
502
|
-
deleted = true;
|
|
503
|
-
else if (result === false) {
|
|
504
|
-
await removeNativeSession(session.runner, session.cwd, externalSessionId);
|
|
505
|
-
deleted = true;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
catch (error) {
|
|
509
|
-
if (!(error instanceof Error) || error.message !== 'NATIVE_TRANSCRIPT_UNAVAILABLE')
|
|
510
|
-
throw error;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
636
|
+
const deleted = await this.removeNativeHistories(session, nativeIds);
|
|
513
637
|
this.options.database().deleteSessionRecord(session.id);
|
|
514
638
|
this.options.forget(session, nativeIds);
|
|
515
639
|
return deleted ? 'deleted' : 'skipped';
|