@myagentroam/node 0.9.2 → 0.9.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/dist/connector.js +12 -13
- package/dist/control-outbound-scheduler.d.ts +4 -1
- package/dist/control-outbound-scheduler.js +39 -6
- package/dist/database.d.ts +0 -19
- package/dist/database.js +1 -62
- package/dist/event-buffer.d.ts +2 -5
- package/dist/event-buffer.js +3 -12
- package/dist/native-session-history.js +33 -12
- package/dist/runner/claude/managed-run-controller.d.ts +0 -1
- package/dist/runner/claude/managed-run-controller.js +17 -12
- package/dist/runner/codex/conversation-parser.d.ts +1 -1
- package/dist/runner/codex/conversation-parser.js +3 -0
- package/dist/runner/codex/managed-run-controller.d.ts +2 -2
- package/dist/runner/codex/managed-run-controller.js +22 -7
- package/dist/runner/codex-runner.d.ts +2 -0
- package/dist/runner/codex-runner.js +30 -2
- package/dist/runner/opencode/conversation-parser.js +1 -1
- package/dist/runner/opencode/managed-run-controller.d.ts +7 -2
- package/dist/runner/opencode/managed-run-controller.js +72 -14
- package/dist/runner/opencode-runner.d.ts +3 -0
- package/dist/runner/opencode-runner.js +25 -0
- package/dist/runner-profiles.js +10 -6
- package/dist/runtime-state.d.ts +5 -12
- package/dist/runtime-state.js +32 -46
- package/dist/service/attachment-domain-state.d.ts +2 -0
- package/dist/service/attachment-domain-state.js +2 -0
- package/dist/service/conversation-segment-service.js +10 -9
- package/dist/service/external-session-resume-service.js +9 -0
- package/dist/service/fake-managed-run-service.d.ts +0 -1
- package/dist/service/fake-managed-run-service.js +0 -2
- package/dist/service/native-session-projection-service.js +5 -0
- package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
- package/dist/service/node-connection-lifecycle-service.js +0 -1
- package/dist/service/node-control-channel.js +1 -1
- package/dist/service/node-control-message-service.d.ts +0 -1
- package/dist/service/node-control-message-service.js +9 -6
- package/dist/service/node-workspace-coordinator.d.ts +1 -4
- package/dist/service/node-workspace-coordinator.js +2 -12
- package/dist/service/run-attachment-service.js +30 -6
- package/dist/service/run-event-service.d.ts +1 -0
- package/dist/service/run-event-service.js +69 -0
- package/dist/service/run-workbench-service.d.ts +1 -1
- package/dist/service/run-workbench-service.js +1 -5
- package/dist/service/runner-interaction-service.d.ts +1 -2
- package/dist/service/runner-interaction-service.js +1 -4
- package/dist/service/session-context-service.d.ts +1 -0
- package/dist/service/session-context-service.js +14 -0
- package/dist/service/session-domain-state.d.ts +1 -0
- package/dist/service/session-domain-state.js +1 -0
- package/dist/service/session-message-service.js +5 -10
- package/dist/service/skill-directory-service.d.ts +3 -1
- package/dist/service/skill-directory-service.js +28 -4
- package/dist/service/skill-node-operation-service.d.ts +1 -1
- package/dist/service/skill-node-operation-service.js +4 -2
- package/dist/service/workbench-event-service.d.ts +1 -13
- package/dist/service/workbench-event-service.js +2 -36
- package/dist/service/workbench-manifest-service.d.ts +0 -1
- package/dist/service/workbench-manifest-service.js +0 -1
- package/dist/service/workspace-domain-state.d.ts +1 -0
- package/dist/service/workspace-queue-workbench-service.d.ts +1 -0
- package/dist/service/workspace-queue-workbench-service.js +1 -0
- package/dist/service/workspace-upload-service.d.ts +2 -0
- package/dist/service/workspace-upload-service.js +28 -6
- package/dist/service/workspace-workbench-service.d.ts +0 -3
- package/dist/service/workspace-workbench-service.js +0 -17
- package/dist/util/runner-error.d.ts +6 -0
- package/dist/util/runner-error.js +52 -0
- package/dist/util/runner-native-session-parsers.js +43 -6
- package/package.json +2 -2
package/dist/runtime-state.js
CHANGED
|
@@ -9,22 +9,22 @@ export class NodeRuntimeState {
|
|
|
9
9
|
nodeId;
|
|
10
10
|
getWorkspace;
|
|
11
11
|
now;
|
|
12
|
+
static MAX_COMPLETED_TURNS_PER_SESSION = 10;
|
|
12
13
|
sessions = new Map();
|
|
13
14
|
runs = new Map();
|
|
14
15
|
messageRuns = new Map();
|
|
15
16
|
events = new Map();
|
|
16
17
|
turns = new Map();
|
|
17
18
|
approvals = new Map();
|
|
18
|
-
workspaceEvents = new Map();
|
|
19
19
|
/** A Session owns at most one running turn; Workspace sessions may run concurrently. */
|
|
20
20
|
leases = new Map();
|
|
21
21
|
/** Session FIFO metadata is deliberately process-local with the rest of run state. */
|
|
22
22
|
queuedRuns = new Map();
|
|
23
23
|
queueVersions = new Map();
|
|
24
24
|
pausedQueues = new Set();
|
|
25
|
-
|
|
25
|
+
/** Bounded tombstones reject stale internal start envelopes after terminal cleanup. */
|
|
26
|
+
terminalRunIds = new Set();
|
|
26
27
|
nextEventId = 1;
|
|
27
|
-
nextWorkspaceEventId = 1;
|
|
28
28
|
constructor(nodeId, getWorkspace, now = Date.now) {
|
|
29
29
|
this.nodeId = nodeId;
|
|
30
30
|
this.getWorkspace = getWorkspace;
|
|
@@ -73,6 +73,8 @@ export class NodeRuntimeState {
|
|
|
73
73
|
}
|
|
74
74
|
/** Internal envelope compatibility for a Run already accepted in this process. */
|
|
75
75
|
adoptRun(input) {
|
|
76
|
+
if (this.terminalRunIds.has(input.runId))
|
|
77
|
+
throw new Error('RUN_TERMINAL');
|
|
76
78
|
let session = this.sessions.get(input.sessionId);
|
|
77
79
|
if (session === undefined) {
|
|
78
80
|
const now = this.now();
|
|
@@ -316,11 +318,6 @@ export class NodeRuntimeState {
|
|
|
316
318
|
this.queuedRuns.set(session.id, [...(this.queuedRuns.get(session.id) ?? []), run.id]);
|
|
317
319
|
this.bumpQueueVersion(session.id);
|
|
318
320
|
this.turns.set(session.id, [...(this.turns.get(session.id) ?? []), fixedTurn]);
|
|
319
|
-
this.recordWorkspaceActivity(run.workspaceId, 'run.created', {
|
|
320
|
-
runId: run.id,
|
|
321
|
-
sessionId: session.id,
|
|
322
|
-
runner: run.runner
|
|
323
|
-
});
|
|
324
321
|
this.replaceSession(session.id, { lastActivityAt: now });
|
|
325
322
|
return { run, created: true };
|
|
326
323
|
}
|
|
@@ -402,7 +399,6 @@ export class NodeRuntimeState {
|
|
|
402
399
|
this.bumpQueueVersion(run.sessionId);
|
|
403
400
|
this.runs.delete(runId);
|
|
404
401
|
this.events.delete(runId);
|
|
405
|
-
this.snapshots.delete(runId);
|
|
406
402
|
this.turns.set(run.sessionId, (this.turns.get(run.sessionId) ?? []).filter((turn) => turn.runId !== runId));
|
|
407
403
|
for (const [key, candidate] of this.messageRuns)
|
|
408
404
|
if (candidate === runId)
|
|
@@ -468,12 +464,6 @@ export class NodeRuntimeState {
|
|
|
468
464
|
if (input.status !== undefined)
|
|
469
465
|
this.transitionRun(input.runId, input.status);
|
|
470
466
|
this.projectEvent(value);
|
|
471
|
-
const run = this.requireRun(input.runId);
|
|
472
|
-
this.recordWorkspaceActivity(run.workspaceId, 'run.updated', {
|
|
473
|
-
runId: run.id,
|
|
474
|
-
eventType: input.eventType,
|
|
475
|
-
status: input.status ?? null
|
|
476
|
-
});
|
|
477
467
|
return value;
|
|
478
468
|
}
|
|
479
469
|
listRunEvents(id, after = 0) {
|
|
@@ -559,16 +549,6 @@ export class NodeRuntimeState {
|
|
|
559
549
|
this.replaceSession(session.id, { lastActivityAt: now });
|
|
560
550
|
return completedTurn;
|
|
561
551
|
}
|
|
562
|
-
listWorkspaceActivity(input) {
|
|
563
|
-
const all = this.workspaceEvents.get(input.workspaceId) ?? [];
|
|
564
|
-
const start = decodeCursor(input.cursor, input.workspaceId);
|
|
565
|
-
const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
|
|
566
|
-
const events = all.slice(start, start + limit);
|
|
567
|
-
return {
|
|
568
|
-
events,
|
|
569
|
-
nextCursor: start + limit < all.length ? encodeCursor(input.workspaceId, start + limit) : null
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
552
|
createApproval(input) {
|
|
573
553
|
this.requireRun(input.runId);
|
|
574
554
|
if (this.approvals.has(input.approvalId))
|
|
@@ -631,11 +611,30 @@ export class NodeRuntimeState {
|
|
|
631
611
|
lastRunSequence(id) {
|
|
632
612
|
return this.events.get(id)?.at(-1)?.sequence ?? 0;
|
|
633
613
|
}
|
|
634
|
-
|
|
635
|
-
this.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
614
|
+
removeTerminalRun(runId) {
|
|
615
|
+
const run = this.requireRun(runId);
|
|
616
|
+
if (!terminal(run.status))
|
|
617
|
+
throw new Error('RUN_ACTIVE');
|
|
618
|
+
this.terminalRunIds.add(runId);
|
|
619
|
+
if (this.terminalRunIds.size > 1_024)
|
|
620
|
+
this.terminalRunIds.delete(this.terminalRunIds.values().next().value);
|
|
621
|
+
const queue = this.queuedRuns.get(run.sessionId) ?? [];
|
|
622
|
+
if (queue.includes(runId)) {
|
|
623
|
+
this.queuedRuns.set(run.sessionId, queue.filter((candidate) => candidate !== runId));
|
|
624
|
+
this.bumpQueueVersion(run.sessionId);
|
|
625
|
+
}
|
|
626
|
+
this.runs.delete(runId);
|
|
627
|
+
this.events.delete(runId);
|
|
628
|
+
const turns = this.turns.get(run.sessionId) ?? [];
|
|
629
|
+
const completed = turns.filter((turn) => terminalTurn(turn.status));
|
|
630
|
+
const retainedCompleted = new Set(completed.slice(-NodeRuntimeState.MAX_COMPLETED_TURNS_PER_SESSION).map((turn) => turn.id));
|
|
631
|
+
this.turns.set(run.sessionId, turns.filter((turn) => !terminalTurn(turn.status) || retainedCompleted.has(turn.id)));
|
|
632
|
+
for (const [key, candidate] of this.messageRuns)
|
|
633
|
+
if (candidate === runId)
|
|
634
|
+
this.messageRuns.delete(key);
|
|
635
|
+
for (const [approvalId, approval] of this.approvals)
|
|
636
|
+
if (approval.runId === runId)
|
|
637
|
+
this.approvals.delete(approvalId);
|
|
639
638
|
}
|
|
640
639
|
hasActiveWorkspaceLease(workspaceId) {
|
|
641
640
|
return [...this.leases.values()]
|
|
@@ -658,7 +657,6 @@ export class NodeRuntimeState {
|
|
|
658
657
|
for (const runId of runIds) {
|
|
659
658
|
this.runs.delete(runId);
|
|
660
659
|
this.events.delete(runId);
|
|
661
|
-
this.snapshots.delete(runId);
|
|
662
660
|
}
|
|
663
661
|
for (const [key, runId] of this.messageRuns)
|
|
664
662
|
if (runIds.has(runId))
|
|
@@ -681,7 +679,6 @@ export class NodeRuntimeState {
|
|
|
681
679
|
for (const runId of runIds) {
|
|
682
680
|
this.runs.delete(runId);
|
|
683
681
|
this.events.delete(runId);
|
|
684
|
-
this.snapshots.delete(runId);
|
|
685
682
|
}
|
|
686
683
|
for (const [key, runId] of this.messageRuns)
|
|
687
684
|
if (runIds.has(runId))
|
|
@@ -689,7 +686,6 @@ export class NodeRuntimeState {
|
|
|
689
686
|
for (const [approvalId, approval] of this.approvals)
|
|
690
687
|
if (runIds.has(approval.runId))
|
|
691
688
|
this.approvals.delete(approvalId);
|
|
692
|
-
this.workspaceEvents.delete(workspaceId);
|
|
693
689
|
for (const sessionId of sessionIds) {
|
|
694
690
|
this.queuedRuns.delete(sessionId);
|
|
695
691
|
this.queueVersions.delete(sessionId);
|
|
@@ -730,19 +726,6 @@ export class NodeRuntimeState {
|
|
|
730
726
|
throw new Error('APPROVAL_NOT_FOUND');
|
|
731
727
|
return value;
|
|
732
728
|
}
|
|
733
|
-
recordWorkspaceActivity(workspaceId, eventType, payload) {
|
|
734
|
-
const event = {
|
|
735
|
-
id: this.nextWorkspaceEventId++,
|
|
736
|
-
workspaceId,
|
|
737
|
-
eventType,
|
|
738
|
-
payload,
|
|
739
|
-
createdAt: this.now()
|
|
740
|
-
};
|
|
741
|
-
this.workspaceEvents.set(workspaceId, [
|
|
742
|
-
...(this.workspaceEvents.get(workspaceId) ?? []),
|
|
743
|
-
event
|
|
744
|
-
]);
|
|
745
|
-
}
|
|
746
729
|
updateTurnForRun(runId, update) {
|
|
747
730
|
const run = this.requireRun(runId);
|
|
748
731
|
const current = this.turns.get(run.sessionId) ?? [];
|
|
@@ -840,6 +823,9 @@ export class NodeRuntimeState {
|
|
|
840
823
|
function terminal(status) {
|
|
841
824
|
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
|
|
842
825
|
}
|
|
826
|
+
function terminalTurn(status) {
|
|
827
|
+
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED'].includes(status);
|
|
828
|
+
}
|
|
843
829
|
function transitionAllowed(from, to) {
|
|
844
830
|
return !terminal(from) || from === to;
|
|
845
831
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare class AttachmentDomainState<TCachedRunImages, TNativeImageSource> {
|
|
2
2
|
readonly runDirectories: Map<string, string>;
|
|
3
|
+
readonly runFileDirectories: Map<string, string>;
|
|
3
4
|
readonly runImages: Map<string, TCachedRunImages>;
|
|
4
5
|
readonly nativeHistoryImages: Map<string, TNativeImageSource[]>;
|
|
6
|
+
readonly nativeHistoryImageTimers: Map<string, NodeJS.Timeout>;
|
|
5
7
|
}
|
|
@@ -3,11 +3,11 @@ export function conversationSegments(turn) {
|
|
|
3
3
|
const groups = [];
|
|
4
4
|
let current = [];
|
|
5
5
|
for (const item of turn.items) {
|
|
6
|
+
if (current.at(-1)?.kind === 'assistant_message' && item.kind !== 'assistant_message') {
|
|
7
|
+
groups.push(current);
|
|
8
|
+
current = [];
|
|
9
|
+
}
|
|
6
10
|
current.push(item);
|
|
7
|
-
if (item.kind !== 'assistant_message')
|
|
8
|
-
continue;
|
|
9
|
-
groups.push(current);
|
|
10
|
-
current = [];
|
|
11
11
|
}
|
|
12
12
|
if (current.length > 0 || groups.length === 0)
|
|
13
13
|
groups.push(current);
|
|
@@ -36,14 +36,15 @@ export function conversationSegments(turn) {
|
|
|
36
36
|
export function conversationSegmentIdentityForItem(turn, itemId, itemKind) {
|
|
37
37
|
let index = 0;
|
|
38
38
|
const runtimeItemId = turn.runId === null ? itemId : `${turn.runId}:${itemId}`;
|
|
39
|
-
for (const item of turn.items) {
|
|
39
|
+
for (const [itemIndex, item] of turn.items.entries()) {
|
|
40
|
+
const previous = turn.items[itemIndex - 1];
|
|
41
|
+
if (previous?.kind === 'assistant_message' && item.kind !== 'assistant_message')
|
|
42
|
+
index += 1;
|
|
40
43
|
if (item.id === itemId || item.id === runtimeItemId)
|
|
41
44
|
return { id: `${turn.id}:segment:${index}`, index };
|
|
42
|
-
if (item.kind === 'assistant_message')
|
|
43
|
-
index += 1;
|
|
44
45
|
}
|
|
45
|
-
if (
|
|
46
|
-
index
|
|
46
|
+
if (turn.items.at(-1)?.kind === 'assistant_message' && itemKind !== 'assistant_message')
|
|
47
|
+
index += 1;
|
|
47
48
|
return { id: `${turn.id}:segment:${index}`, index };
|
|
48
49
|
}
|
|
49
50
|
export class ConversationSegmentService {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const RESUME_FAILURE_TTL_MS = 30 * 60_000;
|
|
1
2
|
export class ExternalSessionResumeService {
|
|
2
3
|
options;
|
|
3
4
|
constructor(options) {
|
|
@@ -84,15 +85,23 @@ export class ExternalSessionResumeService {
|
|
|
84
85
|
: 'REJECTED';
|
|
85
86
|
}
|
|
86
87
|
clearFailure(sessionId) {
|
|
88
|
+
const timer = this.options.state.failureTimers.get(sessionId);
|
|
89
|
+
if (timer !== undefined)
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
this.options.state.failureTimers.delete(sessionId);
|
|
87
92
|
this.options.state.resumeFailures.delete(sessionId);
|
|
88
93
|
this.options.state.resumeFailureDetails.delete(sessionId);
|
|
89
94
|
this.options.state.messageResumeFailures.delete(sessionId);
|
|
90
95
|
}
|
|
91
96
|
setFailure(sessionId, code, detail) {
|
|
97
|
+
this.clearFailure(sessionId);
|
|
92
98
|
this.options.state.resumeFailures.set(sessionId, code);
|
|
93
99
|
if (detail === undefined)
|
|
94
100
|
this.options.state.resumeFailureDetails.delete(sessionId);
|
|
95
101
|
else
|
|
96
102
|
this.options.state.resumeFailureDetails.set(sessionId, detail);
|
|
103
|
+
const timer = setTimeout(() => this.clearFailure(sessionId), RESUME_FAILURE_TTL_MS);
|
|
104
|
+
timer.unref();
|
|
105
|
+
this.options.state.failureTimers.set(sessionId, timer);
|
|
97
106
|
}
|
|
98
107
|
}
|
|
@@ -17,7 +17,6 @@ interface FakeManagedRunServiceOptions {
|
|
|
17
17
|
readonly payload: unknown;
|
|
18
18
|
readonly expiresAt: number;
|
|
19
19
|
}) => void;
|
|
20
|
-
readonly captureSnapshot: (runId: string, cwd: string) => Promise<void>;
|
|
21
20
|
readonly disconnect: () => void;
|
|
22
21
|
}
|
|
23
22
|
/** Test-only managed Run adapter; production Runner paths never depend on it. */
|
|
@@ -32,8 +32,6 @@ export class FakeManagedRunService {
|
|
|
32
32
|
this.options.emitRun(runId, eventType, eventPayload, status);
|
|
33
33
|
if (status !== undefined && isTerminalRunStatus(status)) {
|
|
34
34
|
this.options.state.active.delete(runId);
|
|
35
|
-
if (cwd !== undefined)
|
|
36
|
-
void this.options.captureSnapshot(runId, cwd);
|
|
37
35
|
}
|
|
38
36
|
},
|
|
39
37
|
approval: (approvalId, approvalPayload, expiresAt) => {
|
|
@@ -238,7 +238,12 @@ export class NativeSessionProjectionService {
|
|
|
238
238
|
this.options.state.externalActivity.delete(directId);
|
|
239
239
|
this.options.state.resumedByDirectId.delete(directId);
|
|
240
240
|
this.options.state.resumeFailures.delete(directId);
|
|
241
|
+
this.options.state.messageResumeFailures.delete(directId);
|
|
241
242
|
this.options.state.resumeFailureDetails.delete(directId);
|
|
243
|
+
const timer = this.options.state.failureTimers.get(directId);
|
|
244
|
+
if (timer !== undefined)
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
this.options.state.failureTimers.delete(directId);
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
249
|
for (const [directId, resumedId] of this.options.state.resumedByDirectId)
|
|
@@ -3,7 +3,6 @@ import { type NodeConfig } from '../config.js';
|
|
|
3
3
|
import type { NodeMetrics } from '../operational.js';
|
|
4
4
|
import type { NodeControlChannel } from './node-control-channel.js';
|
|
5
5
|
import type { NodeTerminalChannel } from './node-terminal-channel.js';
|
|
6
|
-
import type { WorkbenchEventService } from './workbench-event-service.js';
|
|
7
6
|
interface NodeConnectionLifecycleServiceOptions {
|
|
8
7
|
readonly config: () => NodeConfig | undefined;
|
|
9
8
|
readonly setConfig: (config: NodeConfig) => void;
|
|
@@ -15,7 +14,6 @@ interface NodeConnectionLifecycleServiceOptions {
|
|
|
15
14
|
readonly setCapabilities: (capabilities: NodeCapabilities) => void;
|
|
16
15
|
readonly control: NodeControlChannel;
|
|
17
16
|
readonly terminal: NodeTerminalChannel;
|
|
18
|
-
readonly events: WorkbenchEventService;
|
|
19
17
|
readonly metrics: NodeMetrics;
|
|
20
18
|
readonly heartbeatMs: number;
|
|
21
19
|
readonly refreshMs: number;
|
|
@@ -48,7 +48,6 @@ export class NodeConnectionLifecycleService {
|
|
|
48
48
|
this.options.terminal.connect();
|
|
49
49
|
this.options.control.send('heartbeat', {});
|
|
50
50
|
this.options.metrics.heartbeatSent();
|
|
51
|
-
this.options.events.replay();
|
|
52
51
|
this.heartbeat = setInterval(() => {
|
|
53
52
|
this.options.metrics.heartbeatSent();
|
|
54
53
|
this.options.control.send('heartbeat', {});
|
|
@@ -5,7 +5,7 @@ import { nodeConnectEndpoint } from '../runner/codex/conversation-parser.js';
|
|
|
5
5
|
export class NodeControlChannel {
|
|
6
6
|
options;
|
|
7
7
|
socket;
|
|
8
|
-
outbound = new ControlOutboundScheduler(() => this.socket);
|
|
8
|
+
outbound = new ControlOutboundScheduler(() => this.socket, () => this.socket?.close(1013, 'NODE_CONTROL_BACKPRESSURE_TIMEOUT'));
|
|
9
9
|
reconnectTimer;
|
|
10
10
|
attempts = 0;
|
|
11
11
|
constructor(options) {
|
|
@@ -8,7 +8,6 @@ interface NodeControlMessageServiceOptions {
|
|
|
8
8
|
readonly close: (code: number, reason: string) => void;
|
|
9
9
|
readonly inspectWorkspace: (envelope: NodeEnvelope) => Promise<void>;
|
|
10
10
|
readonly handleNodeRequest: (envelope: NodeEnvelope) => Promise<void>;
|
|
11
|
-
readonly acknowledgeWorkbenchEvent: (replyTo: string | undefined) => void;
|
|
12
11
|
readonly reconcileRuns: (envelope: NodeEnvelope) => void;
|
|
13
12
|
readonly runners: RunnerRegistry;
|
|
14
13
|
readonly deliverSessionChannelMessage: (envelope: NodeEnvelope) => void;
|
|
@@ -33,12 +33,8 @@ export class NodeControlMessageService {
|
|
|
33
33
|
void this.options.handleNodeRequest(envelope);
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
-
if (envelope.type === 'ack')
|
|
37
|
-
const payload = envelope.payload;
|
|
38
|
-
if (payload.type === 'workbench.event')
|
|
39
|
-
this.options.acknowledgeWorkbenchEvent(envelope.replyTo);
|
|
36
|
+
if (envelope.type === 'ack')
|
|
40
37
|
return;
|
|
41
|
-
}
|
|
42
38
|
if (envelope.type === 'runs.reconcile') {
|
|
43
39
|
this.options.reconcileRuns(envelope);
|
|
44
40
|
return;
|
|
@@ -49,7 +45,14 @@ export class NodeControlMessageService {
|
|
|
49
45
|
}
|
|
50
46
|
if (envelope.type === 'run.start') {
|
|
51
47
|
this.options.send('ack', { type: envelope.type }, envelope.id);
|
|
52
|
-
|
|
48
|
+
try {
|
|
49
|
+
this.options.runners.startManagedRun(envelope);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// A delayed internal start may arrive after the terminal projection was removed.
|
|
53
|
+
if (!(error instanceof Error) || error.message !== 'RUN_TERMINAL')
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
53
56
|
return;
|
|
54
57
|
}
|
|
55
58
|
if (envelope.type === 'session.channel.message') {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { NodeEnvelope } from '@myagentroam/protocol';
|
|
2
2
|
import type { NodeConfig } from '../config.js';
|
|
3
3
|
import type { NodeAgentSession, NodeDatabase, NodeWorkspace } from '../database.js';
|
|
4
|
-
import type { NodeRuntimeState } from '../runtime-state.js';
|
|
5
4
|
import type { TerminalSummary } from '../terminal.js';
|
|
6
5
|
import type { WorkspaceChangeService } from './workspace-change-service.js';
|
|
7
6
|
import type { WorkspaceSessionService } from './workspace-session-service.js';
|
|
@@ -9,14 +8,13 @@ import type { WorkspaceWatchService } from './workspace-watch-service.js';
|
|
|
9
8
|
interface NodeWorkspaceCoordinatorOptions {
|
|
10
9
|
readonly config: () => NodeConfig | undefined;
|
|
11
10
|
readonly database: () => NodeDatabase | undefined;
|
|
12
|
-
readonly runtime: NodeRuntimeState;
|
|
13
11
|
readonly watches: WorkspaceWatchService;
|
|
14
12
|
readonly sessions: WorkspaceSessionService;
|
|
15
13
|
readonly changes: WorkspaceChangeService;
|
|
16
14
|
readonly emit: (workspaceId: string, topic: 'files' | 'changes', revision: number) => void;
|
|
17
15
|
readonly send: (type: string, payload: unknown, replyTo?: string) => void;
|
|
18
16
|
}
|
|
19
|
-
/** Coordinates Workspace invalidation, inspection
|
|
17
|
+
/** Coordinates Workspace invalidation, inspection and persisted Terminal summaries. */
|
|
20
18
|
export declare class NodeWorkspaceCoordinator {
|
|
21
19
|
private readonly options;
|
|
22
20
|
constructor(options: NodeWorkspaceCoordinatorOptions);
|
|
@@ -27,6 +25,5 @@ export declare class NodeWorkspaceCoordinator {
|
|
|
27
25
|
invalidateChanges(workspaceId: string): void;
|
|
28
26
|
persistTerminal(summary: TerminalSummary): void;
|
|
29
27
|
inspect(envelope: NodeEnvelope): Promise<void>;
|
|
30
|
-
captureRunSnapshot(runId: string, cwd: string): Promise<void>;
|
|
31
28
|
}
|
|
32
29
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { inspectWorkspace
|
|
2
|
-
/** Coordinates Workspace invalidation, inspection
|
|
1
|
+
import { inspectWorkspace } from '../workspace.js';
|
|
2
|
+
/** Coordinates Workspace invalidation, inspection and persisted Terminal summaries. */
|
|
3
3
|
export class NodeWorkspaceCoordinator {
|
|
4
4
|
options;
|
|
5
5
|
constructor(options) {
|
|
@@ -67,14 +67,4 @@ export class NodeWorkspaceCoordinator {
|
|
|
67
67
|
this.options.send('workspace.inspect.result', { error: 'WORKSPACE_INVALID' }, envelope.id);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
async captureRunSnapshot(runId, cwd) {
|
|
71
|
-
try {
|
|
72
|
-
const snapshot = await readGitSummary(cwd);
|
|
73
|
-
this.options.runtime.saveSnapshot(runId, snapshot);
|
|
74
|
-
this.options.send('workspace.snapshot', { runId, snapshot });
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// Ordinary directories have no Git snapshot and keep the existing Run result.
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
70
|
}
|
|
@@ -4,6 +4,7 @@ import { lstat, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:f
|
|
|
4
4
|
import { AttachmentDomainState } from './attachment-domain-state.js';
|
|
5
5
|
const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
6
6
|
const IMAGE_CACHE_TTL_MS = 60 * 60_000;
|
|
7
|
+
const NATIVE_IMAGE_INDEX_TTL_MS = 60 * 60_000;
|
|
7
8
|
export class RunAttachmentService {
|
|
8
9
|
nativeImageRoots;
|
|
9
10
|
state = new AttachmentDomainState();
|
|
@@ -43,21 +44,28 @@ export class RunAttachmentService {
|
|
|
43
44
|
throw new Error('MESSAGE_ATTACHMENT_DIRECTORY_INVALID');
|
|
44
45
|
const runDirectory = join(filesDirectory, runId);
|
|
45
46
|
await mkdir(runDirectory, { mode: 0o700 });
|
|
47
|
+
this.state.runFileDirectories.set(runId, runDirectory);
|
|
46
48
|
try {
|
|
47
49
|
await Promise.all(attachments.map((attachment) => writeFile(join(runDirectory, attachment.name), Buffer.from(attachment.dataBase64, 'base64'), { flag: 'wx', mode: 0o600 })));
|
|
48
50
|
return attachments.map((attachment) => `.mar/temp-files/${runId}/${attachment.name}`);
|
|
49
51
|
}
|
|
50
52
|
catch (error) {
|
|
53
|
+
this.state.runFileDirectories.delete(runId);
|
|
51
54
|
await rm(runDirectory, { recursive: true, force: true });
|
|
52
55
|
throw error;
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
cleanup(runId) {
|
|
56
59
|
const directory = this.state.runDirectories.get(runId);
|
|
57
|
-
if (directory
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
if (directory !== undefined) {
|
|
61
|
+
this.state.runDirectories.delete(runId);
|
|
62
|
+
void rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
63
|
+
}
|
|
64
|
+
const fileDirectory = this.state.runFileDirectories.get(runId);
|
|
65
|
+
if (fileDirectory !== undefined) {
|
|
66
|
+
this.state.runFileDirectories.delete(runId);
|
|
67
|
+
void rm(fileDirectory, { recursive: true, force: true }).catch(() => undefined);
|
|
68
|
+
}
|
|
61
69
|
}
|
|
62
70
|
cache(runId, images) {
|
|
63
71
|
if (images.length === 0)
|
|
@@ -122,8 +130,18 @@ export class RunAttachmentService {
|
|
|
122
130
|
}
|
|
123
131
|
attachments.push({ name: image.name, mime: image.mime, runId, imageIndex });
|
|
124
132
|
}
|
|
125
|
-
if (sources.length > 0)
|
|
133
|
+
if (sources.length > 0) {
|
|
126
134
|
this.state.nativeHistoryImages.set(runId, sources);
|
|
135
|
+
const previousTimer = this.state.nativeHistoryImageTimers.get(runId);
|
|
136
|
+
if (previousTimer !== undefined)
|
|
137
|
+
clearTimeout(previousTimer);
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
this.state.nativeHistoryImageTimers.delete(runId);
|
|
140
|
+
this.state.nativeHistoryImages.delete(runId);
|
|
141
|
+
}, NATIVE_IMAGE_INDEX_TTL_MS);
|
|
142
|
+
timer.unref();
|
|
143
|
+
this.state.nativeHistoryImageTimers.set(runId, timer);
|
|
144
|
+
}
|
|
127
145
|
return attachments;
|
|
128
146
|
}
|
|
129
147
|
async readNative(runId, imageIndex) {
|
|
@@ -161,10 +179,16 @@ export class RunAttachmentService {
|
|
|
161
179
|
this.state.runImages.delete(runId);
|
|
162
180
|
}
|
|
163
181
|
dispose() {
|
|
164
|
-
for (const runId of
|
|
182
|
+
for (const runId of new Set([
|
|
183
|
+
...this.state.runDirectories.keys(),
|
|
184
|
+
...this.state.runFileDirectories.keys()
|
|
185
|
+
]))
|
|
165
186
|
this.cleanup(runId);
|
|
166
187
|
for (const runId of this.state.runImages.keys())
|
|
167
188
|
this.clear(runId);
|
|
189
|
+
for (const timer of this.state.nativeHistoryImageTimers.values())
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
this.state.nativeHistoryImageTimers.clear();
|
|
168
192
|
this.state.nativeHistoryImages.clear();
|
|
169
193
|
}
|
|
170
194
|
}
|
|
@@ -50,6 +50,9 @@ export class RunEventService {
|
|
|
50
50
|
this.emitNow(runId, 'text.delta', { text: pending.text });
|
|
51
51
|
}
|
|
52
52
|
emitNow(runId, eventType, payload, status) {
|
|
53
|
+
if (this.options.runtime.getRun(runId) === undefined)
|
|
54
|
+
return;
|
|
55
|
+
const notificationPayload = this.notificationPayload(runId, eventType, payload, status);
|
|
53
56
|
const event = {
|
|
54
57
|
runId,
|
|
55
58
|
sequence: (this.sequences.get(runId) ?? 0) + 1,
|
|
@@ -76,6 +79,7 @@ export class RunEventService {
|
|
|
76
79
|
/* A stale relayed run may race the storage boundary. */
|
|
77
80
|
}
|
|
78
81
|
if (status !== undefined && isTerminalRunStatus(status)) {
|
|
82
|
+
this.options.state.active.delete(runId);
|
|
79
83
|
this.options.state.interruptRequested.delete(runId);
|
|
80
84
|
const terminalRun = this.options.runtime.getRun(runId);
|
|
81
85
|
if (terminalRun !== undefined && this.options.isBootstrapSession(terminalRun.sessionId))
|
|
@@ -91,6 +95,11 @@ export class RunEventService {
|
|
|
91
95
|
this.options.emitWorkbench('conversation', {
|
|
92
96
|
event: { ...event, createdAt: persisted?.createdAt ?? Date.now() }
|
|
93
97
|
});
|
|
98
|
+
if (notificationPayload !== undefined)
|
|
99
|
+
this.emitNow(runId, 'notification', {
|
|
100
|
+
...notificationPayload,
|
|
101
|
+
occurredAt: persisted?.createdAt ?? Date.now()
|
|
102
|
+
});
|
|
94
103
|
if (status === undefined || !isTerminalRunStatus(status))
|
|
95
104
|
return;
|
|
96
105
|
const turn = this.options.runtime.conversationTurnForRun(runId);
|
|
@@ -102,7 +111,67 @@ export class RunEventService {
|
|
|
102
111
|
const run = this.options.runtime.getRun(runId);
|
|
103
112
|
if (run !== undefined) {
|
|
104
113
|
this.options.scheduleContextRefresh(run.sessionId);
|
|
114
|
+
this.options.runtime.removeTerminalRun(runId);
|
|
115
|
+
this.sequences.delete(runId);
|
|
105
116
|
void this.options.dispatchQueue(run.sessionId);
|
|
106
117
|
}
|
|
107
118
|
}
|
|
119
|
+
notificationPayload(runId, eventType, payload, status) {
|
|
120
|
+
if (eventType === 'notification')
|
|
121
|
+
return undefined;
|
|
122
|
+
const run = this.options.runtime.getRun(runId);
|
|
123
|
+
if (run === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
const session = this.options.runtime.getAgentSession(run.sessionId);
|
|
126
|
+
if (session === undefined)
|
|
127
|
+
return undefined;
|
|
128
|
+
const base = {
|
|
129
|
+
workspaceId: run.workspaceId,
|
|
130
|
+
sessionId: run.sessionId,
|
|
131
|
+
sessionTitle: session.customTitle ?? session.runnerTitle ?? session.id,
|
|
132
|
+
runId
|
|
133
|
+
};
|
|
134
|
+
if (status !== undefined && isTerminalRunStatus(status)) {
|
|
135
|
+
if (this.options.state.interruptRequested.has(runId))
|
|
136
|
+
return undefined;
|
|
137
|
+
if (status === 'SUCCEEDED' &&
|
|
138
|
+
this.options.runtime.sessionQueue(run.sessionId).runIds.length > 0)
|
|
139
|
+
return undefined;
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
notificationId: `run:${run.workspaceId}:${runId}`,
|
|
143
|
+
kind: 'RUN_TERMINAL',
|
|
144
|
+
outcome: status
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (eventType === 'approval.request' && isPlainRecord(payload)) {
|
|
148
|
+
const approvalId = payload.approvalId;
|
|
149
|
+
if (typeof approvalId !== 'string')
|
|
150
|
+
return undefined;
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
notificationId: `approval:${run.workspaceId}:${runId}:${approvalId}`,
|
|
154
|
+
kind: 'APPROVAL_REQUIRED'
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (eventType !== 'conversation.item' || !isPlainRecord(payload))
|
|
158
|
+
return undefined;
|
|
159
|
+
if (payload.kind === 'plan' && typeof payload.itemId === 'string')
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
notificationId: `plan:${run.workspaceId}:${run.sessionId}`,
|
|
163
|
+
kind: 'PLAN_READY'
|
|
164
|
+
};
|
|
165
|
+
if (payload.kind === 'user_input_request' &&
|
|
166
|
+
typeof payload.itemId === 'string' &&
|
|
167
|
+
isPlainRecord(payload.payload) &&
|
|
168
|
+
Array.isArray(payload.payload.questions) &&
|
|
169
|
+
payload.payload.questions.length > 0)
|
|
170
|
+
return {
|
|
171
|
+
...base,
|
|
172
|
+
notificationId: `user-input:${run.workspaceId}:${runId}:${payload.itemId}`,
|
|
173
|
+
kind: 'USER_INPUT_REQUIRED'
|
|
174
|
+
};
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
108
177
|
}
|
|
@@ -6,6 +6,7 @@ export interface RunWorkbenchServiceOptions {
|
|
|
6
6
|
readonly respondUserInput: (runId: string, requestId: string, answers: Record<string, unknown>) => void;
|
|
7
7
|
readonly deleteQueuedStart: (runId: string) => void;
|
|
8
8
|
readonly clearImages: (runId: string) => void;
|
|
9
|
+
readonly cleanupAttachments: (runId: string) => void;
|
|
9
10
|
readonly emitQueue: (workspaceId: string, sessionId: string) => void;
|
|
10
11
|
readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'CANCELLED') => void;
|
|
11
12
|
readonly control: (operation: string, payload: Record<string, unknown>) => void;
|
|
@@ -16,7 +17,6 @@ export declare class RunWorkbenchService {
|
|
|
16
17
|
constructor(options: RunWorkbenchServiceOptions);
|
|
17
18
|
operations(): Readonly<Record<string, NodeOperationHandler>>;
|
|
18
19
|
private get;
|
|
19
|
-
private snapshot;
|
|
20
20
|
private image;
|
|
21
21
|
private respond;
|
|
22
22
|
private interruptSession;
|
|
@@ -8,7 +8,6 @@ export class RunWorkbenchService {
|
|
|
8
8
|
operations() {
|
|
9
9
|
return {
|
|
10
10
|
'run.get': (data) => this.get(record(data)),
|
|
11
|
-
'run.snapshot': (data) => this.snapshot(record(data)),
|
|
12
11
|
'run.image.read': (data) => this.image(record(data)),
|
|
13
12
|
'run.user-input.respond': (data) => this.respond(record(data)),
|
|
14
13
|
'session.interrupt': (data) => this.interruptSession(record(data)),
|
|
@@ -21,10 +20,6 @@ export class RunWorkbenchService {
|
|
|
21
20
|
get(input) {
|
|
22
21
|
return { run: this.requireRun(input.runId) };
|
|
23
22
|
}
|
|
24
|
-
snapshot(input) {
|
|
25
|
-
const run = this.requireRun(input.runId);
|
|
26
|
-
return { snapshot: this.options.runtime.getSnapshot(run.id) ?? null };
|
|
27
|
-
}
|
|
28
23
|
async image(input) {
|
|
29
24
|
if (typeof input.runId !== 'string' ||
|
|
30
25
|
typeof input.imageIndex !== 'number' ||
|
|
@@ -63,6 +58,7 @@ export class RunWorkbenchService {
|
|
|
63
58
|
this.options.runtime.deleteQueuedRun(run.id);
|
|
64
59
|
this.options.deleteQueuedStart(run.id);
|
|
65
60
|
this.options.clearImages(run.id);
|
|
61
|
+
this.options.cleanupAttachments(run.id);
|
|
66
62
|
this.options.emitQueue(run.workspaceId, run.sessionId);
|
|
67
63
|
return { cancelled: true };
|
|
68
64
|
}
|
|
@@ -13,7 +13,6 @@ interface RunnerInteractionServiceOptions {
|
|
|
13
13
|
readonly runners: RunnerRegistry;
|
|
14
14
|
readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'FAILED' | 'CANCELLED' | 'INTERRUPTED') => void;
|
|
15
15
|
readonly emitItem: (runId: string, item: PublicConversationItem) => void;
|
|
16
|
-
readonly captureSnapshot: (runId: string, cwd: string) => Promise<void>;
|
|
17
16
|
readonly approvalTimeoutMs: number;
|
|
18
17
|
readonly publishApproval: (input: {
|
|
19
18
|
readonly runId: string;
|
|
@@ -33,7 +32,7 @@ export declare class RunnerInteractionService {
|
|
|
33
32
|
cancelClaudeRun(runId: string, intent: 'CANCEL' | 'INTERRUPT'): boolean;
|
|
34
33
|
cancelCodexTurn(runId: string, threadId: string, turnId: string, cwd: string, terminalStatus: 'CANCELLED' | 'INTERRUPTED'): Promise<void>;
|
|
35
34
|
respondUserInput(runId: string, requestId: string, rawAnswers: Record<string, unknown>): void;
|
|
36
|
-
completeCancelledRun(runId: string
|
|
35
|
+
completeCancelledRun(runId: string): boolean;
|
|
37
36
|
private respondCodexUserInput;
|
|
38
37
|
private answers;
|
|
39
38
|
}
|