@myagentroam/node 0.9.0 → 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.d.ts +2 -0
- package/dist/connector.js +50 -25
- 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 +105 -26
- package/dist/runner/abstract-runner.d.ts +1 -1
- package/dist/runner/abstract-runner.js +2 -2
- package/dist/runner/claude/managed-run-controller.d.ts +0 -1
- package/dist/runner/claude/managed-run-controller.js +32 -17
- package/dist/runner/claude-code-runner.js +3 -4
- package/dist/runner/codex/conversation-parser.d.ts +3 -3
- package/dist/runner/codex/conversation-parser.js +33 -12
- package/dist/runner/codex/managed-run-controller.d.ts +2 -2
- package/dist/runner/codex/managed-run-controller.js +26 -10
- package/dist/runner/codex-runner.d.ts +3 -1
- package/dist/runner/codex-runner.js +40 -10
- package/dist/runner/opencode/conversation-parser.d.ts +11 -7
- package/dist/runner/opencode/conversation-parser.js +16 -16
- package/dist/runner/opencode/managed-run-controller.d.ts +8 -2
- package/dist/runner/opencode/managed-run-controller.js +94 -22
- package/dist/runner/opencode-runner.d.ts +3 -0
- package/dist/runner/opencode-runner.js +25 -0
- package/dist/runner-command-engine.js +1 -1
- package/dist/runner-profiles.js +29 -17
- package/dist/runner-usage.js +5 -5
- 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-history-service.js +55 -24
- package/dist/service/conversation-segment-service.d.ts +20 -0
- package/dist/service/conversation-segment-service.js +156 -0
- 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/mcp-installation-verifier.js +1 -1
- package/dist/service/native-session-projection-service.js +5 -0
- package/dist/service/native-session-watch-service.d.ts +1 -0
- package/dist/service/native-session-watch-service.js +21 -4
- 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-request-service.js +2 -1
- 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.d.ts +3 -2
- package/dist/service/run-attachment-service.js +51 -17
- package/dist/service/run-event-service.d.ts +2 -0
- package/dist/service/run-event-service.js +77 -2
- 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-catalog-service.js +4 -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/session-presentation-service.d.ts +1 -0
- package/dist/service/session-presentation-service.js +8 -1
- package/dist/service/session-query-service.d.ts +4 -0
- package/dist/service/session-query-service.js +6 -1
- package/dist/service/skill-directory-service.d.ts +3 -1
- package/dist/service/skill-directory-service.js +31 -30
- package/dist/service/skill-install-service.js +4 -74
- package/dist/service/skill-node-operation-service.d.ts +1 -1
- package/dist/service/skill-node-operation-service.js +6 -3
- 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-git-exclude-service.d.ts +4 -0
- package/dist/service/workspace-git-exclude-service.js +119 -0
- package/dist/service/workspace-queue-workbench-service.d.ts +1 -0
- package/dist/service/workspace-queue-workbench-service.js +4 -2
- package/dist/service/workspace-service.js +2 -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/supervisor.js +2 -1
- package/dist/terminal.js +1 -1
- package/dist/util/node-operation-parsers.js +2 -2
- package/dist/util/runner-error.d.ts +6 -0
- package/dist/util/runner-error.js +52 -0
- package/dist/util/runner-native-session-parsers.d.ts +2 -3
- package/dist/util/runner-native-session-parsers.js +48 -20
- package/dist/util/safe-error.d.ts +2 -0
- package/dist/util/safe-error.js +5 -0
- package/package.json +2 -2
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const TURN_PAGE_SIZE = 30;
|
|
2
|
+
export function conversationSegments(turn) {
|
|
3
|
+
const groups = [];
|
|
4
|
+
let current = [];
|
|
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
|
+
}
|
|
10
|
+
current.push(item);
|
|
11
|
+
}
|
|
12
|
+
if (current.length > 0 || groups.length === 0)
|
|
13
|
+
groups.push(current);
|
|
14
|
+
return groups.map((items, index) => {
|
|
15
|
+
const id = `${turn.id}:segment:${index}`;
|
|
16
|
+
const latest = index === groups.length - 1;
|
|
17
|
+
const projectedItems = items.map((item) => ({
|
|
18
|
+
...item,
|
|
19
|
+
segmentId: id,
|
|
20
|
+
segmentIndex: index
|
|
21
|
+
}));
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
sessionId: turn.sessionId,
|
|
25
|
+
turnId: turn.id,
|
|
26
|
+
runId: turn.runId,
|
|
27
|
+
index,
|
|
28
|
+
status: (latest ? turn.status : 'SUCCEEDED'),
|
|
29
|
+
...(latest && turn.rewind !== undefined ? { rewind: turn.rewind } : {}),
|
|
30
|
+
items: projectedItems,
|
|
31
|
+
startedAt: projectedItems[0]?.startedAt ?? turn.startedAt,
|
|
32
|
+
completedAt: projectedItems.at(-1)?.completedAt ?? null
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function conversationSegmentIdentityForItem(turn, itemId, itemKind) {
|
|
37
|
+
let index = 0;
|
|
38
|
+
const runtimeItemId = turn.runId === null ? itemId : `${turn.runId}:${itemId}`;
|
|
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;
|
|
43
|
+
if (item.id === itemId || item.id === runtimeItemId)
|
|
44
|
+
return { id: `${turn.id}:segment:${index}`, index };
|
|
45
|
+
}
|
|
46
|
+
if (turn.items.at(-1)?.kind === 'assistant_message' && itemKind !== 'assistant_message')
|
|
47
|
+
index += 1;
|
|
48
|
+
return { id: `${turn.id}:segment:${index}`, index };
|
|
49
|
+
}
|
|
50
|
+
export class ConversationSegmentService {
|
|
51
|
+
readTurns;
|
|
52
|
+
constructor(readTurns) {
|
|
53
|
+
this.readTurns = readTurns;
|
|
54
|
+
}
|
|
55
|
+
projectInitial(session, page, limit) {
|
|
56
|
+
const all = page.turns.flatMap(conversationSegments);
|
|
57
|
+
const segments = all.slice(-limit);
|
|
58
|
+
return {
|
|
59
|
+
snapshotSequence: page.snapshotSequence,
|
|
60
|
+
source: page.source,
|
|
61
|
+
readAt: page.readAt,
|
|
62
|
+
latestVisibleAt: page.latestVisibleAt,
|
|
63
|
+
unit: 'SEGMENT',
|
|
64
|
+
segments,
|
|
65
|
+
nextCursor: segments.length === 0 || (segments.length === all.length && page.nextCursor === null)
|
|
66
|
+
? null
|
|
67
|
+
: encodeCursor({
|
|
68
|
+
version: 1,
|
|
69
|
+
sessionId: session.id,
|
|
70
|
+
beforeSegmentId: segments[0].id
|
|
71
|
+
})
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async read(session, input) {
|
|
75
|
+
const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
|
|
76
|
+
const boundary = input.cursor === undefined ? undefined : decodeCursor(input.cursor, session.id);
|
|
77
|
+
const collected = [];
|
|
78
|
+
const visited = new Set();
|
|
79
|
+
let turnCursor;
|
|
80
|
+
let locatingBoundary = boundary !== undefined;
|
|
81
|
+
let hasOlder = false;
|
|
82
|
+
let metadata;
|
|
83
|
+
while (collected.length < limit) {
|
|
84
|
+
const page = await this.readTurns(session, {
|
|
85
|
+
...(turnCursor === undefined ? {} : { cursor: turnCursor }),
|
|
86
|
+
limit: TURN_PAGE_SIZE
|
|
87
|
+
});
|
|
88
|
+
metadata ??= {
|
|
89
|
+
snapshotSequence: page.snapshotSequence,
|
|
90
|
+
source: page.source,
|
|
91
|
+
readAt: page.readAt,
|
|
92
|
+
latestVisibleAt: page.latestVisibleAt
|
|
93
|
+
};
|
|
94
|
+
const pageSegments = page.turns.flatMap(conversationSegments);
|
|
95
|
+
let eligible = pageSegments;
|
|
96
|
+
if (locatingBoundary) {
|
|
97
|
+
const boundaryIndex = pageSegments.findIndex((segment) => segment.id === boundary?.beforeSegmentId);
|
|
98
|
+
if (boundaryIndex >= 0) {
|
|
99
|
+
eligible = pageSegments.slice(0, boundaryIndex);
|
|
100
|
+
locatingBoundary = false;
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
eligible = [];
|
|
104
|
+
}
|
|
105
|
+
if (!locatingBoundary && eligible.length > 0) {
|
|
106
|
+
const remaining = Math.max(0, limit - collected.length);
|
|
107
|
+
const take = eligible.slice(-remaining);
|
|
108
|
+
hasOlder = eligible.length > take.length || page.nextCursor !== null;
|
|
109
|
+
collected.unshift(...take);
|
|
110
|
+
}
|
|
111
|
+
if (collected.length >= limit || page.nextCursor === null)
|
|
112
|
+
break;
|
|
113
|
+
if (visited.has(page.nextCursor))
|
|
114
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
115
|
+
visited.add(page.nextCursor);
|
|
116
|
+
turnCursor = page.nextCursor;
|
|
117
|
+
}
|
|
118
|
+
if (locatingBoundary)
|
|
119
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
120
|
+
const nextCursor = collected.length === 0 || !hasOlder
|
|
121
|
+
? null
|
|
122
|
+
: encodeCursor({
|
|
123
|
+
version: 1,
|
|
124
|
+
sessionId: session.id,
|
|
125
|
+
beforeSegmentId: collected[0].id
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
...(metadata ?? {
|
|
129
|
+
snapshotSequence: 0,
|
|
130
|
+
source: 'runtime',
|
|
131
|
+
readAt: Date.now(),
|
|
132
|
+
latestVisibleAt: 0
|
|
133
|
+
}),
|
|
134
|
+
unit: 'SEGMENT',
|
|
135
|
+
segments: collected,
|
|
136
|
+
nextCursor
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function encodeCursor(cursor) {
|
|
141
|
+
return Buffer.from(JSON.stringify(cursor)).toString('base64url');
|
|
142
|
+
}
|
|
143
|
+
function decodeCursor(value, sessionId) {
|
|
144
|
+
try {
|
|
145
|
+
const decoded = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
146
|
+
if (decoded.version !== 1 ||
|
|
147
|
+
decoded.sessionId !== sessionId ||
|
|
148
|
+
typeof decoded.beforeSegmentId !== 'string' ||
|
|
149
|
+
decoded.beforeSegmentId.length === 0)
|
|
150
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
151
|
+
return decoded;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -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) => {
|
|
@@ -4,7 +4,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
|
4
4
|
const INSTALL_TIMEOUT_MS = 90_000;
|
|
5
5
|
export class McpInstallationVerifier {
|
|
6
6
|
async verify(input) {
|
|
7
|
-
const client = new Client({ name: 'myagentroam-node-installer', version: '0.9.
|
|
7
|
+
const client = new Client({ name: 'myagentroam-node-installer', version: '0.9.1' });
|
|
8
8
|
try {
|
|
9
9
|
await withTimeout((async () => {
|
|
10
10
|
if (input.entry.runtime.transport === 'STDIO') {
|
|
@@ -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)
|
|
@@ -8,6 +8,7 @@ export interface NativeSessionWatchOptions<TPage extends {
|
|
|
8
8
|
readonly refreshActivity: (session: NodeAgentSession) => Promise<void>;
|
|
9
9
|
readonly present: (session: NodeAgentSession) => unknown;
|
|
10
10
|
readonly readPage: (session: NodeAgentSession, limit: number) => Promise<TPage>;
|
|
11
|
+
readonly projectInitialPage?: (session: NodeAgentSession, page: TPage, unit: 'SEGMENT', limit: number) => unknown;
|
|
11
12
|
readonly emitSession: (session: unknown) => void;
|
|
12
13
|
readonly emitTurn: (turn: unknown) => void;
|
|
13
14
|
}
|
|
@@ -15,7 +15,12 @@ export class NativeSessionWatchService {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
async executeWatch(input) {
|
|
18
|
-
|
|
18
|
+
const projectInitialPage = this.options.projectInitialPage;
|
|
19
|
+
if (input.unit === 'SEGMENT' && projectInitialPage === undefined)
|
|
20
|
+
throw new Error('SESSION_INVALID');
|
|
21
|
+
if (typeof input.sessionId !== 'string' ||
|
|
22
|
+
(input.mode !== 'initial' && input.mode !== 'renew') ||
|
|
23
|
+
(input.unit !== undefined && input.unit !== 'SEGMENT'))
|
|
19
24
|
throw new Error('SESSION_INVALID');
|
|
20
25
|
const session = await this.options.resolve(input.sessionId);
|
|
21
26
|
if (session === undefined || session.externalSessionId === null)
|
|
@@ -27,9 +32,21 @@ export class NativeSessionWatchService {
|
|
|
27
32
|
return { renewed: true };
|
|
28
33
|
}
|
|
29
34
|
const watch = this.watch(session);
|
|
30
|
-
const
|
|
31
|
-
if (watch !== undefined &&
|
|
35
|
+
const turnSnapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT);
|
|
36
|
+
if (watch !== undefined && turnSnapshot === undefined) {
|
|
37
|
+
this.stop(session.id);
|
|
32
38
|
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
39
|
+
}
|
|
40
|
+
let snapshot = turnSnapshot;
|
|
41
|
+
if (turnSnapshot !== undefined && input.unit === 'SEGMENT') {
|
|
42
|
+
try {
|
|
43
|
+
snapshot = projectInitialPage(session, turnSnapshot, 'SEGMENT', 10);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.stop(session.id);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
33
50
|
return {
|
|
34
51
|
session: this.options.present(session),
|
|
35
52
|
watch: watch === undefined ? null : { expiresAt: watch.expiresAt },
|
|
@@ -131,7 +148,7 @@ export class NativeSessionWatchService {
|
|
|
131
148
|
}
|
|
132
149
|
const page = await this.options.readPage(session, limit);
|
|
133
150
|
watch.failures = 0;
|
|
134
|
-
const turns = page.turns.length <= 10 ? page.turns : page.turns.slice(
|
|
151
|
+
const turns = page.turns.length <= 10 ? page.turns : page.turns.slice(-10);
|
|
135
152
|
const signature = JSON.stringify(turns);
|
|
136
153
|
if (signature === watch.signature)
|
|
137
154
|
return page;
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import { nodeLog } from '../operational.js';
|
|
2
2
|
import { validUserAuthorization } from '../runner/codex/conversation-parser.js';
|
|
3
|
+
import { safeErrorCode } from '../util/safe-error.js';
|
|
3
4
|
/** Validates and executes the closed Admin-facing Node operation envelope. */
|
|
4
5
|
export class NodeRequestService {
|
|
5
6
|
options;
|
|
@@ -44,7 +45,7 @@ export class NodeRequestService {
|
|
|
44
45
|
durationMs: Math.round(performance.now() - startedAt),
|
|
45
46
|
error: error instanceof Error ? error.message : 'INTERNAL_ERROR'
|
|
46
47
|
});
|
|
47
|
-
this.options.send('node.response', { error: error
|
|
48
|
+
this.options.send('node.response', { error: safeErrorCode(error, 'INTERNAL_ERROR') }, envelope.id);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -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
|
}
|
|
@@ -11,14 +11,15 @@ export interface NativeImageSource {
|
|
|
11
11
|
readonly name: string;
|
|
12
12
|
readonly mime: string;
|
|
13
13
|
readonly path: string;
|
|
14
|
+
readonly root: string;
|
|
14
15
|
}
|
|
15
16
|
export type RunnerImageAttachment = NodeMessageAttachmentInput & {
|
|
16
17
|
readonly mime: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
|
|
17
18
|
};
|
|
18
19
|
export declare class RunAttachmentService {
|
|
19
|
-
private readonly
|
|
20
|
+
private readonly nativeImageRoots;
|
|
20
21
|
readonly state: AttachmentDomainState<CachedRunImages, NativeImageSource>;
|
|
21
|
-
constructor(
|
|
22
|
+
constructor(nativeImageRoots: (session: NodeAgentSession) => readonly string[]);
|
|
22
23
|
materializeCodex(runId: string, attachments: readonly RunnerImageAttachment[]): Promise<readonly Extract<CodexComposerInput, {
|
|
23
24
|
readonly type: 'localImage';
|
|
24
25
|
}>[]>;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { tmpdir } from 'node:os';
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
-
import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
|
|
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();
|
|
10
|
-
constructor(
|
|
11
|
-
this.
|
|
11
|
+
constructor(nativeImageRoots) {
|
|
12
|
+
this.nativeImageRoots = nativeImageRoots;
|
|
12
13
|
}
|
|
13
14
|
async materializeCodex(runId, attachments) {
|
|
14
15
|
if (attachments.length === 0)
|
|
@@ -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)
|
|
@@ -95,8 +103,8 @@ export class RunAttachmentService {
|
|
|
95
103
|
return cached.images[imageIndex];
|
|
96
104
|
}
|
|
97
105
|
registerNative(session, images) {
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
106
|
+
const roots = this.nativeImageRoots(session).map((root) => resolve(root));
|
|
107
|
+
if (roots.length === 0)
|
|
100
108
|
return [];
|
|
101
109
|
const runId = `native-history:${session.id}`;
|
|
102
110
|
const sources = this.state.nativeHistoryImages.get(runId) ?? [];
|
|
@@ -107,20 +115,33 @@ export class RunAttachmentService {
|
|
|
107
115
|
continue;
|
|
108
116
|
}
|
|
109
117
|
const path = resolve(image.localPath);
|
|
110
|
-
const
|
|
111
|
-
|
|
118
|
+
const root = roots.find((candidate) => {
|
|
119
|
+
const relation = relative(candidate, path);
|
|
120
|
+
return relation.length > 0 && !relation.startsWith('..') && !isAbsolute(relation);
|
|
121
|
+
});
|
|
122
|
+
if (root === undefined) {
|
|
112
123
|
attachments.push({ name: image.name, mime: image.mime });
|
|
113
124
|
continue;
|
|
114
125
|
}
|
|
115
126
|
let imageIndex = sources.findIndex((source) => source.path === path);
|
|
116
127
|
if (imageIndex < 0) {
|
|
117
128
|
imageIndex = sources.length;
|
|
118
|
-
sources.push({ name: image.name, mime: image.mime, path });
|
|
129
|
+
sources.push({ name: image.name, mime: image.mime, path, root });
|
|
119
130
|
}
|
|
120
131
|
attachments.push({ name: image.name, mime: image.mime, runId, imageIndex });
|
|
121
132
|
}
|
|
122
|
-
if (sources.length > 0)
|
|
133
|
+
if (sources.length > 0) {
|
|
123
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
|
+
}
|
|
124
145
|
return attachments;
|
|
125
146
|
}
|
|
126
147
|
async readNative(runId, imageIndex) {
|
|
@@ -128,10 +149,17 @@ export class RunAttachmentService {
|
|
|
128
149
|
if (source === undefined)
|
|
129
150
|
return undefined;
|
|
130
151
|
try {
|
|
131
|
-
const
|
|
152
|
+
const [actualRoot, actualPath] = await Promise.all([
|
|
153
|
+
realpath(source.root),
|
|
154
|
+
realpath(source.path)
|
|
155
|
+
]);
|
|
156
|
+
const relation = relative(actualRoot, actualPath);
|
|
157
|
+
if (relation.length === 0 || relation.startsWith('..') || isAbsolute(relation))
|
|
158
|
+
return undefined;
|
|
159
|
+
const info = await lstat(actualPath);
|
|
132
160
|
if (!info.isFile() || info.size <= 0 || info.size > MAX_ATTACHMENT_BYTES)
|
|
133
161
|
return undefined;
|
|
134
|
-
return { ...source, dataBase64: (await readFile(
|
|
162
|
+
return { ...source, dataBase64: (await readFile(actualPath)).toString('base64') };
|
|
135
163
|
}
|
|
136
164
|
catch {
|
|
137
165
|
return undefined;
|
|
@@ -151,10 +179,16 @@ export class RunAttachmentService {
|
|
|
151
179
|
this.state.runImages.delete(runId);
|
|
152
180
|
}
|
|
153
181
|
dispose() {
|
|
154
|
-
for (const runId of
|
|
182
|
+
for (const runId of new Set([
|
|
183
|
+
...this.state.runDirectories.keys(),
|
|
184
|
+
...this.state.runFileDirectories.keys()
|
|
185
|
+
]))
|
|
155
186
|
this.cleanup(runId);
|
|
156
187
|
for (const runId of this.state.runImages.keys())
|
|
157
188
|
this.clear(runId);
|
|
189
|
+
for (const timer of this.state.nativeHistoryImageTimers.values())
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
this.state.nativeHistoryImageTimers.clear();
|
|
158
192
|
this.state.nativeHistoryImages.clear();
|
|
159
193
|
}
|
|
160
194
|
}
|
|
@@ -25,7 +25,9 @@ export declare class RunEventService {
|
|
|
25
25
|
constructor(options: RunEventServiceOptions);
|
|
26
26
|
emit(runId: string, eventType: string, payload: unknown, status?: FakeRunnerStatus): void;
|
|
27
27
|
clear(): void;
|
|
28
|
+
flushPendingText(runId: string): void;
|
|
28
29
|
private enqueueText;
|
|
29
30
|
private flushText;
|
|
30
31
|
private emitNow;
|
|
32
|
+
private notificationPayload;
|
|
31
33
|
}
|