@myagentroam/node 0.9.2 → 0.9.4
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/codex-app-server.d.ts +24 -0
- package/dist/codex-app-server.js +100 -5
- package/dist/connector.js +18 -15
- package/dist/control-outbound-scheduler.d.ts +4 -1
- package/dist/control-outbound-scheduler.js +39 -6
- package/dist/database.d.ts +2 -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 +11 -2
- package/dist/runner/codex/managed-run-controller.js +79 -24
- package/dist/runner/codex-runner.d.ts +3 -0
- package/dist/runner/codex-runner.js +42 -3
- 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 +7 -12
- package/dist/runtime-state.js +43 -53
- 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.d.ts +8 -0
- package/dist/service/conversation-history-service.js +181 -9
- package/dist/service/conversation-segment-service.js +17 -10
- 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/native-session-watch-service.d.ts +8 -2
- package/dist/service/native-session-watch-service.js +28 -10
- 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.js +30 -6
- package/dist/service/run-event-service.d.ts +1 -0
- package/dist/service/run-event-service.js +80 -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-lifecycle-service.js +4 -12
- package/dist/service/session-message-service.d.ts +1 -0
- package/dist/service/session-message-service.js +109 -85
- 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-change-service.js +1 -1
- package/dist/service/workspace-domain-state.d.ts +1 -0
- package/dist/service/workspace-queue-workbench-service.d.ts +13 -2
- package/dist/service/workspace-queue-workbench-service.js +8 -2
- 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.d.ts +2 -1
- package/dist/util/runner-native-session-parsers.js +63 -15
- package/dist/workspace.js +33 -8
- package/package.json +2 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { isImageAttachment, isRunnerImageAttachment, parseComposerAttachmentIds, parseMessageAttachments, sessionTitleFromFirstMessage, withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
|
|
2
3
|
import { parseSecretEnvironment } from '../util/secret-environment.js';
|
|
3
4
|
export class SessionMessageService {
|
|
4
5
|
options;
|
|
6
|
+
pendingMessages = new Map();
|
|
5
7
|
constructor(options) {
|
|
6
8
|
this.options = options;
|
|
7
9
|
}
|
|
@@ -19,20 +21,21 @@ export class SessionMessageService {
|
|
|
19
21
|
typeof input.content !== 'string' ||
|
|
20
22
|
input.content.length === 0)
|
|
21
23
|
throw new Error('MESSAGE_INVALID');
|
|
24
|
+
const clientMessageId = input.clientMessageId;
|
|
25
|
+
const content = input.content;
|
|
22
26
|
const inlineAttachments = parseMessageAttachments(input.attachments);
|
|
23
27
|
const secretEnvironment = parseSecretEnvironment(input.secretEnvironment);
|
|
24
28
|
const attachmentIds = parseComposerAttachmentIds(input.attachmentIds);
|
|
25
29
|
if (inlineAttachments.length > 0 && attachmentIds.length > 0)
|
|
26
30
|
throw new Error('MESSAGE_ATTACHMENTS_INVALID');
|
|
27
|
-
const session =
|
|
28
|
-
this.options.emitSession(session);
|
|
31
|
+
const session = await this.options.resolve(input.sessionId);
|
|
29
32
|
const intent = input.deliveryIntent;
|
|
30
33
|
if (intent !== undefined &&
|
|
31
34
|
intent !== 'SEND' &&
|
|
32
35
|
intent !== 'QUEUE' &&
|
|
33
36
|
intent !== 'REPLACE_CURRENT')
|
|
34
37
|
throw new Error('MESSAGE_INVALID');
|
|
35
|
-
const existing = this.options.runtime.findMessageRun(session.id,
|
|
38
|
+
const existing = this.options.runtime.findMessageRun(session.id, clientMessageId);
|
|
36
39
|
if (existing !== undefined)
|
|
37
40
|
return {
|
|
38
41
|
session: this.options.present(session),
|
|
@@ -40,92 +43,113 @@ export class SessionMessageService {
|
|
|
40
43
|
delivery: this.options.runtime.isQueuedRun(existing.id) ? 'QUEUED' : 'STARTED',
|
|
41
44
|
idempotent: true
|
|
42
45
|
};
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
46
|
+
const pendingKey = `${session.id}\u0000${clientMessageId}`;
|
|
47
|
+
const pending = this.pendingMessages.get(pendingKey);
|
|
48
|
+
if (pending !== undefined)
|
|
49
|
+
return pending;
|
|
50
|
+
const operation = (async () => {
|
|
51
|
+
const runId = randomUUID();
|
|
52
|
+
let enqueued = false;
|
|
53
|
+
try {
|
|
54
|
+
const uploaded = await this.options.uploads.resolve(session.workspaceId, attachmentIds);
|
|
55
|
+
const attachments = attachmentIds.length === 0
|
|
56
|
+
? inlineAttachments
|
|
57
|
+
: uploaded.map((attachment) => attachment.attachment);
|
|
58
|
+
const imageAttachments = attachments.filter(isImageAttachment);
|
|
59
|
+
const runnerImageAttachments = attachments.filter(isRunnerImageAttachment);
|
|
60
|
+
const attachmentPaths = await this.persistAttachments(session, runId, attachments);
|
|
61
|
+
const runnerInput = withNonImageAttachmentPrompt(content, attachmentPaths);
|
|
62
|
+
const planActive = this.options.commands
|
|
63
|
+
.list(session.id)
|
|
64
|
+
.some((state) => state.commandId === 'plan');
|
|
65
|
+
const runner = this.options.runners.require(session.runner);
|
|
66
|
+
const active = this.options.runtime
|
|
67
|
+
.listRunsForSession(session.id)
|
|
68
|
+
.find((run) => run.status === 'STARTING' || run.status === 'RUNNING');
|
|
69
|
+
const replaceCurrentRun = intent === 'REPLACE_CURRENT' ? active : undefined;
|
|
70
|
+
const queueWasPaused = this.options.runtime.sessionQueue(session.id).paused;
|
|
71
|
+
const queuedBeforeCreate = this.options.runtime.hasActiveSessionLease(session.id) || intent === 'QUEUE';
|
|
72
|
+
const created = this.options.queue.enqueue({
|
|
73
|
+
runId,
|
|
74
|
+
sessionId: session.id,
|
|
75
|
+
workspaceId: session.workspaceId,
|
|
76
|
+
runner: session.runner,
|
|
77
|
+
input: runner.prepareMessageInput(runnerInput, planActive ? 'plan' : 'default'),
|
|
78
|
+
attachments: runnerImageAttachments,
|
|
79
|
+
attachmentPaths,
|
|
80
|
+
cwd: session.cwd,
|
|
81
|
+
externalSessionId: session.externalSessionId,
|
|
82
|
+
model: session.model,
|
|
83
|
+
effort: session.effort,
|
|
84
|
+
access: session.access,
|
|
85
|
+
collaborationMode: planActive ? 'plan' : 'default',
|
|
86
|
+
secretEnvironment,
|
|
87
|
+
...(runner.serviceTier() === 'fast' ? { serviceTier: 'fast' } : {})
|
|
88
|
+
}, {
|
|
89
|
+
runId,
|
|
90
|
+
sessionId: session.id,
|
|
91
|
+
clientMessageId,
|
|
92
|
+
content: runnerInput,
|
|
93
|
+
...(Number.isSafeInteger(input.__requestUserId)
|
|
94
|
+
? { initiatedByUserId: input.__requestUserId }
|
|
95
|
+
: {}),
|
|
96
|
+
...(attachments.length === 0 ? {} : { attachments })
|
|
97
|
+
});
|
|
98
|
+
enqueued = created.created;
|
|
99
|
+
if (!created.created) {
|
|
100
|
+
this.options.attachments.cleanup(runId);
|
|
101
|
+
return {
|
|
102
|
+
session: this.options.present(session),
|
|
103
|
+
run: created.run,
|
|
104
|
+
delivery: this.options.runtime.isQueuedRun(created.run.id) ? 'QUEUED' : 'STARTED',
|
|
105
|
+
idempotent: true
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const acceptedSession = this.options.runtime.setTemporaryTitleIfMissing(session.id, sessionTitleFromFirstMessage(content));
|
|
109
|
+
this.options.emitSession(acceptedSession);
|
|
110
|
+
this.options.attachments.cache(created.run.id, imageAttachments);
|
|
111
|
+
void this.options.uploads.consume(attachmentIds).catch(() => undefined);
|
|
112
|
+
if (queueWasPaused && active === undefined && intent !== 'QUEUE')
|
|
113
|
+
this.options.runtime.prioritizeQueuedRun(created.run.id);
|
|
114
|
+
if (!queueWasPaused || intent !== 'QUEUE')
|
|
115
|
+
this.options.runtime.resumeSessionQueue(session.id);
|
|
116
|
+
if (replaceCurrentRun !== undefined)
|
|
117
|
+
this.options.queue.replace(created.run.id, replaceCurrentRun.id);
|
|
118
|
+
else if (intent !== 'QUEUE' ||
|
|
119
|
+
!queuedBeforeCreate ||
|
|
120
|
+
!this.options.runtime.hasActiveSessionLease(session.id))
|
|
121
|
+
this.options.queue.schedule(session.id);
|
|
122
|
+
this.options.emitQueue(session.workspaceId, session.id);
|
|
123
|
+
return {
|
|
124
|
+
session: this.options.present(acceptedSession),
|
|
125
|
+
run: created.run,
|
|
126
|
+
delivery: replaceCurrentRun !== undefined
|
|
127
|
+
? 'RESTARTING'
|
|
128
|
+
: queuedBeforeCreate
|
|
129
|
+
? 'QUEUED'
|
|
130
|
+
: 'STARTED',
|
|
131
|
+
idempotent: false
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (!enqueued) {
|
|
136
|
+
this.options.attachments.clear(runId);
|
|
137
|
+
this.options.attachments.cleanup(runId);
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
this.pendingMessages.set(pendingKey, operation);
|
|
70
143
|
try {
|
|
71
|
-
|
|
144
|
+
return await operation;
|
|
72
145
|
}
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
throw error;
|
|
146
|
+
finally {
|
|
147
|
+
if (this.pendingMessages.get(pendingKey) === operation)
|
|
148
|
+
this.pendingMessages.delete(pendingKey);
|
|
77
149
|
}
|
|
78
|
-
const runnerInput = withNonImageAttachmentPrompt(input.content, attachmentPaths);
|
|
79
|
-
this.options.runtime.updateQueuedMessage(created.run.id, runnerInput);
|
|
80
|
-
const planActive = this.options.commands
|
|
81
|
-
.list(session.id)
|
|
82
|
-
.some((state) => state.commandId === 'plan');
|
|
83
|
-
this.options.queue.remember({
|
|
84
|
-
runId: created.run.id,
|
|
85
|
-
sessionId: created.run.sessionId,
|
|
86
|
-
workspaceId: created.run.workspaceId,
|
|
87
|
-
runner: created.run.runner,
|
|
88
|
-
input: this.options.runners
|
|
89
|
-
.require(created.run.runner)
|
|
90
|
-
.prepareMessageInput(runnerInput, planActive ? 'plan' : 'default'),
|
|
91
|
-
attachments: runnerImageAttachments,
|
|
92
|
-
attachmentPaths,
|
|
93
|
-
cwd: session.cwd,
|
|
94
|
-
externalSessionId: session.externalSessionId,
|
|
95
|
-
model: session.model,
|
|
96
|
-
effort: session.effort,
|
|
97
|
-
access: session.access,
|
|
98
|
-
collaborationMode: planActive ? 'plan' : 'default',
|
|
99
|
-
secretEnvironment,
|
|
100
|
-
...(this.options.runners.require(created.run.runner).serviceTier() === 'fast'
|
|
101
|
-
? { serviceTier: 'fast' }
|
|
102
|
-
: {})
|
|
103
|
-
});
|
|
104
|
-
if (queueWasPaused && active === undefined && intent !== 'QUEUE')
|
|
105
|
-
this.options.runtime.prioritizeQueuedRun(created.run.id);
|
|
106
|
-
if (!queueWasPaused || intent !== 'QUEUE')
|
|
107
|
-
this.options.runtime.resumeSessionQueue(session.id);
|
|
108
|
-
this.options.emitQueue(session.workspaceId, session.id);
|
|
109
|
-
if (replaceCurrentRun !== undefined)
|
|
110
|
-
this.options.queue.replace(created.run.id, replaceCurrentRun.id);
|
|
111
|
-
else if (intent !== 'QUEUE' || !queuedBeforeCreate)
|
|
112
|
-
this.options.queue.schedule(session.id);
|
|
113
|
-
return {
|
|
114
|
-
session: this.options.present(session),
|
|
115
|
-
run: created.run,
|
|
116
|
-
delivery: replaceCurrentRun !== undefined ? 'RESTARTING' : queuedBeforeCreate ? 'QUEUED' : 'STARTED',
|
|
117
|
-
idempotent: false
|
|
118
|
-
};
|
|
119
150
|
}
|
|
120
|
-
async persistAttachments(session, runId,
|
|
121
|
-
return
|
|
122
|
-
...uploaded
|
|
123
|
-
.filter(({ attachment }) => !isRunnerImageAttachment(attachment))
|
|
124
|
-
.map(({ path }) => path),
|
|
125
|
-
...(await this.options.attachments.persistFiles(session.cwd, runId, attachmentIds.length === 0
|
|
126
|
-
? attachments.filter((attachment) => !isRunnerImageAttachment(attachment))
|
|
127
|
-
: []))
|
|
128
|
-
];
|
|
151
|
+
async persistAttachments(session, runId, attachments) {
|
|
152
|
+
return this.options.attachments.persistFiles(session.cwd, runId, attachments.filter((attachment) => !isRunnerImageAttachment(attachment)));
|
|
129
153
|
}
|
|
130
154
|
channelMessage(input) {
|
|
131
155
|
if (typeof input.sessionId !== 'string' ||
|
|
@@ -12,6 +12,8 @@ export interface LocalSkillTargetInspection {
|
|
|
12
12
|
readonly skills: readonly LocalSkillInspection[];
|
|
13
13
|
}
|
|
14
14
|
export declare class SkillDirectoryService {
|
|
15
|
-
inspectNodeHome(home: string
|
|
15
|
+
inspectNodeHome(home: string, options?: {
|
|
16
|
+
readonly includeClaudeCompatibility?: boolean;
|
|
17
|
+
}): Promise<LocalSkillTargetInspection>;
|
|
16
18
|
inspectWorkspace(workspace: string): Promise<LocalSkillTargetInspection>;
|
|
17
19
|
}
|
|
@@ -6,14 +6,20 @@ const MAX_METADATA_BYTES = 64 * 1024;
|
|
|
6
6
|
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
7
7
|
const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
|
|
8
8
|
export class SkillDirectoryService {
|
|
9
|
-
async inspectNodeHome(home) {
|
|
9
|
+
async inspectNodeHome(home, options = {}) {
|
|
10
10
|
const agentsRoot = path.join(home, '.agents', 'skills');
|
|
11
11
|
const claudeRoot = path.join(home, '.claude', 'skills');
|
|
12
|
-
const
|
|
12
|
+
const compatibilityMode = await nodeCompatibilityMode(agentsRoot, claudeRoot);
|
|
13
|
+
const agents = await scanRoot(agentsRoot, 'AGENTS');
|
|
14
|
+
const claude = options.includeClaudeCompatibility === true && compatibilityMode !== 'UNAVAILABLE'
|
|
15
|
+
? await scanRoot(claudeRoot, 'CLAUDE')
|
|
16
|
+
: [];
|
|
13
17
|
return {
|
|
14
18
|
targetKind: 'NODE',
|
|
15
|
-
compatibilityMode
|
|
16
|
-
skills
|
|
19
|
+
compatibilityMode,
|
|
20
|
+
skills: options.includeClaudeCompatibility === true
|
|
21
|
+
? mergeNodeCompatibilityRoots(agents, claude)
|
|
22
|
+
: agents.map(withoutSource)
|
|
17
23
|
};
|
|
18
24
|
}
|
|
19
25
|
async inspectWorkspace(workspace) {
|
|
@@ -147,6 +153,24 @@ function mergeWorkspaceCopies(agents, claude) {
|
|
|
147
153
|
return withoutSource(copies.agents);
|
|
148
154
|
});
|
|
149
155
|
}
|
|
156
|
+
function mergeNodeCompatibilityRoots(agents, claude) {
|
|
157
|
+
const byName = new Map();
|
|
158
|
+
for (const item of agents)
|
|
159
|
+
byName.set(item.name, { agents: item });
|
|
160
|
+
for (const item of claude)
|
|
161
|
+
byName.set(item.name, { ...byName.get(item.name), claude: item });
|
|
162
|
+
return [...byName.entries()]
|
|
163
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
164
|
+
.map(([name, copies]) => {
|
|
165
|
+
if (copies.agents === undefined || copies.claude === undefined)
|
|
166
|
+
return withoutSource((copies.agents ?? copies.claude));
|
|
167
|
+
if (copies.agents.description !== copies.claude.description ||
|
|
168
|
+
copies.agents.localStatus !== copies.claude.localStatus ||
|
|
169
|
+
JSON.stringify(copies.agents.install) !== JSON.stringify(copies.claude.install))
|
|
170
|
+
return { name, description: copies.agents.description, localStatus: 'PARTIAL' };
|
|
171
|
+
return withoutSource(copies.agents);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
150
174
|
function withoutSource(entry) {
|
|
151
175
|
return {
|
|
152
176
|
name: entry.name,
|
|
@@ -9,7 +9,7 @@ export declare class SkillNodeOperationService {
|
|
|
9
9
|
private readonly jobs;
|
|
10
10
|
constructor(database: () => NodeDatabase, config: () => NodeConfig);
|
|
11
11
|
operations(): {
|
|
12
|
-
'node.skills.inspect': () => Promise<import("./skill-directory-service.js").LocalSkillTargetInspection>;
|
|
12
|
+
'node.skills.inspect': (data: unknown) => Promise<import("./skill-directory-service.js").LocalSkillTargetInspection>;
|
|
13
13
|
'node.skills.install': (data: unknown) => Promise<{
|
|
14
14
|
accepted: boolean;
|
|
15
15
|
installJobId: `${string}-${string}-${string}-${string}-${string}`;
|
|
@@ -17,9 +17,11 @@ export class SkillNodeOperationService {
|
|
|
17
17
|
}
|
|
18
18
|
operations() {
|
|
19
19
|
return {
|
|
20
|
-
'node.skills.inspect': async () => {
|
|
20
|
+
'node.skills.inspect': async (data) => {
|
|
21
21
|
await this.installer.recoverNodeHome(homedir());
|
|
22
|
-
return this.directories.inspectNodeHome(homedir()
|
|
22
|
+
return this.directories.inspectNodeHome(homedir(), {
|
|
23
|
+
includeClaudeCompatibility: record(data)['includeClaudeCompatibility'] === true
|
|
24
|
+
});
|
|
23
25
|
},
|
|
24
26
|
'node.skills.install': (data) => this.install('NODE', data),
|
|
25
27
|
'node.skills.remove': (data) => this.remove('NODE', data),
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { NodeOperationHandler } from '../connector/node-operation-router.js';
|
|
1
|
+
import { type NodeEnvelope, type WorkbenchEventCategory } from '@myagentroam/protocol';
|
|
3
2
|
export declare class WorkbenchEventService {
|
|
4
3
|
private readonly send;
|
|
5
4
|
private readonly accepts;
|
|
6
|
-
private readonly buffer;
|
|
7
5
|
private sequence;
|
|
8
|
-
private readonly history;
|
|
9
6
|
constructor(send: (envelope: NodeEnvelope) => void, accepts: (category: WorkbenchEventCategory, payload: unknown) => boolean);
|
|
10
7
|
emit(category: WorkbenchEventCategory, payload: unknown): void;
|
|
11
|
-
read(after: number): {
|
|
12
|
-
readonly events: readonly WorkbenchEvent[];
|
|
13
|
-
readonly latestSequence: number;
|
|
14
|
-
readonly resyncRequired: boolean;
|
|
15
|
-
};
|
|
16
|
-
operations(): Readonly<Record<string, NodeOperationHandler>>;
|
|
17
8
|
snapshotSequence(): number;
|
|
18
|
-
acknowledge(replyTo: string | undefined): boolean;
|
|
19
|
-
replay(): void;
|
|
20
|
-
get pendingCount(): number;
|
|
21
9
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createEnvelope } from '@myagentroam/protocol';
|
|
2
2
|
export class WorkbenchEventService {
|
|
3
3
|
send;
|
|
4
4
|
accepts;
|
|
5
|
-
buffer = new ReliableWorkbenchEventBuffer();
|
|
6
5
|
sequence = 0;
|
|
7
|
-
history = [];
|
|
8
6
|
constructor(send, accepts) {
|
|
9
7
|
this.send = send;
|
|
10
8
|
this.accepts = accepts;
|
|
@@ -13,41 +11,9 @@ export class WorkbenchEventService {
|
|
|
13
11
|
if (!this.accepts(category, payload))
|
|
14
12
|
return;
|
|
15
13
|
const event = { sequence: ++this.sequence, category, payload };
|
|
16
|
-
this.
|
|
17
|
-
if (this.history.length > 1_000)
|
|
18
|
-
this.history.shift();
|
|
19
|
-
this.send(this.buffer.queue(event));
|
|
20
|
-
}
|
|
21
|
-
read(after) {
|
|
22
|
-
const first = this.history[0]?.sequence;
|
|
23
|
-
const reset = after > this.sequence || (first !== undefined && after < first - 1);
|
|
24
|
-
return {
|
|
25
|
-
events: reset ? [] : this.history.filter((event) => event.sequence > after),
|
|
26
|
-
latestSequence: this.sequence,
|
|
27
|
-
resyncRequired: reset
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
operations() {
|
|
31
|
-
return {
|
|
32
|
-
'workbench.events.read': (data) => {
|
|
33
|
-
const afterValue = data?.after;
|
|
34
|
-
const after = typeof afterValue === 'number' && Number.isInteger(afterValue) && afterValue >= 0
|
|
35
|
-
? afterValue
|
|
36
|
-
: 0;
|
|
37
|
-
return this.read(after);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
14
|
+
this.send(createEnvelope('workbench.event', event));
|
|
40
15
|
}
|
|
41
16
|
snapshotSequence() {
|
|
42
17
|
return this.sequence;
|
|
43
18
|
}
|
|
44
|
-
acknowledge(replyTo) {
|
|
45
|
-
return this.buffer.acknowledge(replyTo);
|
|
46
|
-
}
|
|
47
|
-
replay() {
|
|
48
|
-
this.buffer.replay(this.send);
|
|
49
|
-
}
|
|
50
|
-
get pendingCount() {
|
|
51
|
-
return this.buffer.size;
|
|
52
|
-
}
|
|
53
19
|
}
|
|
@@ -21,7 +21,6 @@ export declare class WorkbenchManifestService {
|
|
|
21
21
|
reasonCode: null;
|
|
22
22
|
itemVersions: number[];
|
|
23
23
|
supportsSessionCursor: boolean;
|
|
24
|
-
supportsWorkspaceActivityCursor: boolean;
|
|
25
24
|
supportsHistoryPaging: boolean;
|
|
26
25
|
supportsItemFinalSnapshots: boolean;
|
|
27
26
|
maxPageSize: number;
|
|
@@ -42,6 +42,7 @@ export interface CompletedComposerAttachment {
|
|
|
42
42
|
readonly name: string;
|
|
43
43
|
readonly mime: string;
|
|
44
44
|
readonly size: number;
|
|
45
|
+
readonly timer: NodeJS.Timeout;
|
|
45
46
|
}
|
|
46
47
|
export declare class WorkspaceDomainState {
|
|
47
48
|
readonly uploads: Map<string, ActiveWorkspaceUpload>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NodeOperationHandler } from '../connector/node-operation-router.js';
|
|
2
2
|
import type { NodeRuntimeState } from '../runtime-state.js';
|
|
3
3
|
import type { RunnerName } from '@myagentroam/protocol';
|
|
4
|
-
import type { NodeMcpInstallation } from '../database.js';
|
|
4
|
+
import type { NodeAgentRun, NodeMcpInstallation, NodeMessageAttachmentInput } from '../database.js';
|
|
5
5
|
export interface QueuedRunStart<TAttachment = unknown> {
|
|
6
6
|
readonly runId: string;
|
|
7
7
|
readonly sessionId: string;
|
|
@@ -23,6 +23,7 @@ export interface WorkspaceQueueWorkbenchServiceOptions {
|
|
|
23
23
|
readonly runtime: NodeRuntimeState;
|
|
24
24
|
readonly emit: (workspaceId: string, sessionId: string) => void;
|
|
25
25
|
readonly clearImages: (runId: string) => void;
|
|
26
|
+
readonly cleanupAttachments: (runId: string) => void;
|
|
26
27
|
readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'FAILED' | 'CANCELLED') => void;
|
|
27
28
|
readonly emitConversation: (turn: unknown) => void;
|
|
28
29
|
readonly control: (operation: string, payload: Record<string, unknown>) => void;
|
|
@@ -35,7 +36,17 @@ export declare class WorkspaceQueueWorkbenchService<TAttachment> {
|
|
|
35
36
|
private readonly options;
|
|
36
37
|
private readonly starts;
|
|
37
38
|
constructor(options: WorkspaceQueueWorkbenchServiceOptions);
|
|
38
|
-
|
|
39
|
+
enqueue(start: QueuedRunStart<TAttachment>, message: {
|
|
40
|
+
readonly runId: string;
|
|
41
|
+
readonly sessionId: string;
|
|
42
|
+
readonly clientMessageId: string;
|
|
43
|
+
readonly content: string;
|
|
44
|
+
readonly initiatedByUserId?: number;
|
|
45
|
+
readonly attachments?: readonly NodeMessageAttachmentInput[];
|
|
46
|
+
}): {
|
|
47
|
+
readonly run: NodeAgentRun;
|
|
48
|
+
readonly created: boolean;
|
|
49
|
+
};
|
|
39
50
|
pending(runId: string): QueuedRunStart<TAttachment> | undefined;
|
|
40
51
|
remove(runId: string): void;
|
|
41
52
|
clear(): void;
|
|
@@ -8,8 +8,13 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
8
8
|
constructor(options) {
|
|
9
9
|
this.options = options;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
enqueue(start, message) {
|
|
12
|
+
if (message.runId !== start.runId || message.sessionId !== start.sessionId)
|
|
13
|
+
throw new Error('QUEUE_RUN_MISMATCH');
|
|
14
|
+
const created = this.options.runtime.createMessageRun(message);
|
|
15
|
+
if (created.created)
|
|
16
|
+
this.starts.set(start.runId, start);
|
|
17
|
+
return created;
|
|
13
18
|
}
|
|
14
19
|
pending(runId) {
|
|
15
20
|
return this.starts.get(runId);
|
|
@@ -80,6 +85,7 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
80
85
|
this.options.runtime.deleteQueuedRun(run.id);
|
|
81
86
|
this.remove(run.id);
|
|
82
87
|
this.options.clearImages(run.id);
|
|
88
|
+
this.options.cleanupAttachments(run.id);
|
|
83
89
|
this.options.emit(run.workspaceId, run.sessionId);
|
|
84
90
|
return { queue: this.present(run.workspaceId, run.sessionId) };
|
|
85
91
|
}
|
|
@@ -23,9 +23,11 @@ export declare class WorkspaceUploadService {
|
|
|
23
23
|
complete(uploadId: unknown): Promise<UploadStatus>;
|
|
24
24
|
cancel(uploadId: unknown): Promise<void>;
|
|
25
25
|
resolve(workspaceId: string, ids: readonly string[]): Promise<readonly ResolvedComposerAttachment[]>;
|
|
26
|
+
consume(ids: readonly string[]): Promise<void>;
|
|
26
27
|
shutdown(): Promise<void>;
|
|
27
28
|
private require;
|
|
28
29
|
private remove;
|
|
30
|
+
private removeCompleted;
|
|
29
31
|
private attachmentTarget;
|
|
30
32
|
}
|
|
31
33
|
interface UploadStatus {
|
|
@@ -133,16 +133,25 @@ export class WorkspaceUploadService {
|
|
|
133
133
|
clearTimeout(upload.timer);
|
|
134
134
|
this.state.uploads.delete(upload.id);
|
|
135
135
|
this.onCompleted(upload.workspaceId);
|
|
136
|
-
if (upload.composerAttachment !== undefined)
|
|
137
|
-
|
|
136
|
+
if (upload.composerAttachment !== undefined) {
|
|
137
|
+
const attachmentId = upload.composerAttachment.id;
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
const attachment = this.state.composerAttachments.get(attachmentId);
|
|
140
|
+
if (attachment !== undefined)
|
|
141
|
+
void this.removeCompleted(attachment);
|
|
142
|
+
}, TTL_MS);
|
|
143
|
+
timer.unref();
|
|
144
|
+
this.state.composerAttachments.set(attachmentId, {
|
|
138
145
|
id: upload.composerAttachment.id,
|
|
139
146
|
workspaceId: upload.workspaceId,
|
|
140
147
|
path: upload.path,
|
|
141
148
|
targetPath: upload.targetPath,
|
|
142
149
|
name: upload.composerAttachment.name,
|
|
143
150
|
mime: upload.composerAttachment.mime,
|
|
144
|
-
size: upload.size
|
|
151
|
+
size: upload.size,
|
|
152
|
+
timer
|
|
145
153
|
});
|
|
154
|
+
}
|
|
146
155
|
return {
|
|
147
156
|
uploadId: upload.id,
|
|
148
157
|
path: upload.path,
|
|
@@ -159,8 +168,7 @@ export class WorkspaceUploadService {
|
|
|
159
168
|
const attachment = this.state.composerAttachments.get(uploadId);
|
|
160
169
|
if (attachment === undefined)
|
|
161
170
|
throw new Error('UPLOAD_NOT_FOUND');
|
|
162
|
-
this.
|
|
163
|
-
await rm(dirname(attachment.targetPath), { recursive: true, force: true });
|
|
171
|
+
await this.removeCompleted(attachment);
|
|
164
172
|
}
|
|
165
173
|
async resolve(workspaceId, ids) {
|
|
166
174
|
return Promise.all(ids.map(async (id) => {
|
|
@@ -184,10 +192,17 @@ export class WorkspaceUploadService {
|
|
|
184
192
|
};
|
|
185
193
|
}));
|
|
186
194
|
}
|
|
195
|
+
async consume(ids) {
|
|
196
|
+
await Promise.all(ids.map(async (id) => {
|
|
197
|
+
const attachment = this.state.composerAttachments.get(id);
|
|
198
|
+
if (attachment !== undefined)
|
|
199
|
+
await this.removeCompleted(attachment);
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
187
202
|
async shutdown() {
|
|
188
203
|
await Promise.allSettled([...this.state.uploads.values()].map((upload) => this.remove(upload)));
|
|
189
204
|
for (const attachment of this.state.composerAttachments.values())
|
|
190
|
-
await
|
|
205
|
+
await this.removeCompleted(attachment);
|
|
191
206
|
this.state.composerAttachments.clear();
|
|
192
207
|
}
|
|
193
208
|
require(id) {
|
|
@@ -206,6 +221,13 @@ export class WorkspaceUploadService {
|
|
|
206
221
|
if (upload.composerAttachment !== undefined)
|
|
207
222
|
await rm(dirname(upload.targetPath), { recursive: true, force: true }).catch(() => undefined);
|
|
208
223
|
}
|
|
224
|
+
async removeCompleted(attachment) {
|
|
225
|
+
if (this.state.composerAttachments.get(attachment.id) !== attachment)
|
|
226
|
+
return;
|
|
227
|
+
this.state.composerAttachments.delete(attachment.id);
|
|
228
|
+
clearTimeout(attachment.timer);
|
|
229
|
+
await rm(dirname(attachment.targetPath), { recursive: true, force: true }).catch(() => undefined);
|
|
230
|
+
}
|
|
209
231
|
async attachmentTarget(workspacePath, id, name) {
|
|
210
232
|
const mar = resolve(workspacePath, '.mar');
|
|
211
233
|
await mkdir(mar, { recursive: true, mode: 0o700 });
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { NodeOperationHandler } from '../connector/node-operation-router.js';
|
|
2
2
|
import type { NodeWorkspace } from '../database.js';
|
|
3
|
-
import { NodeRuntimeState } from '../runtime-state.js';
|
|
4
3
|
import { WorkspaceChangeService } from './workspace-change-service.js';
|
|
5
4
|
import { WorkspaceFileService } from './workspace-file-service.js';
|
|
6
5
|
import { WorkspaceUploadService } from './workspace-upload-service.js';
|
|
7
6
|
import { WorkspaceWatchService } from './workspace-watch-service.js';
|
|
8
7
|
export interface WorkspaceWorkbenchServiceOptions {
|
|
9
8
|
readonly requireWorkspace: (workspaceId: unknown) => NodeWorkspace;
|
|
10
|
-
readonly runtime: NodeRuntimeState;
|
|
11
9
|
readonly watch: WorkspaceWatchService;
|
|
12
10
|
readonly files: WorkspaceFileService;
|
|
13
11
|
readonly uploads: WorkspaceUploadService;
|
|
@@ -20,7 +18,6 @@ export declare class WorkspaceWorkbenchService {
|
|
|
20
18
|
private readonly contentSearch;
|
|
21
19
|
constructor(options: WorkspaceWorkbenchServiceOptions);
|
|
22
20
|
operations(): Readonly<Record<string, NodeOperationHandler>>;
|
|
23
|
-
private activity;
|
|
24
21
|
private watch;
|
|
25
22
|
private unwatch;
|
|
26
23
|
private listFiles;
|
|
@@ -9,7 +9,6 @@ export class WorkspaceWorkbenchService {
|
|
|
9
9
|
}
|
|
10
10
|
operations() {
|
|
11
11
|
return {
|
|
12
|
-
'workspace.activity.read': (data) => this.activity(record(data)),
|
|
13
12
|
'workspace.watch': (data) => this.watch(record(data)),
|
|
14
13
|
'workspace.unwatch': (data) => this.unwatch(record(data)),
|
|
15
14
|
'workspace.files.list': (data) => this.listFiles(record(data)),
|
|
@@ -35,20 +34,6 @@ export class WorkspaceWorkbenchService {
|
|
|
35
34
|
'workspace.diff': (data) => this.diff(record(data))
|
|
36
35
|
};
|
|
37
36
|
}
|
|
38
|
-
activity(input) {
|
|
39
|
-
if (typeof input.workspaceId !== 'string' ||
|
|
40
|
-
(input.cursor !== undefined && typeof input.cursor !== 'string') ||
|
|
41
|
-
(input.limit !== undefined &&
|
|
42
|
-
(!Number.isInteger(input.limit) ||
|
|
43
|
-
input.limit < 1 ||
|
|
44
|
-
input.limit > 500)))
|
|
45
|
-
throw new Error('EVENT_CURSOR_INVALID');
|
|
46
|
-
return this.options.runtime.listWorkspaceActivity({
|
|
47
|
-
workspaceId: input.workspaceId,
|
|
48
|
-
...(typeof input.cursor === 'string' ? { cursor: input.cursor } : {}),
|
|
49
|
-
...(typeof input.limit === 'number' ? { limit: input.limit } : {})
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
37
|
async watch(input) {
|
|
53
38
|
if (typeof input.workspaceId !== 'string' ||
|
|
54
39
|
typeof input.watchId !== 'string' ||
|
|
@@ -143,8 +128,6 @@ export class WorkspaceWorkbenchService {
|
|
|
143
128
|
(input.state !== 'STAGED' && input.state !== 'UNSTAGED' && input.state !== 'UNTRACKED'))
|
|
144
129
|
throw new Error('GIT_CHANGE_RESTORE_UNAVAILABLE');
|
|
145
130
|
const workspace = this.gitWorkspace(input.workspaceId);
|
|
146
|
-
if (this.options.runtime.hasActiveWorkspaceLease(workspace.id))
|
|
147
|
-
throw new Error('WORKSPACE_BUSY');
|
|
148
131
|
const repository = await resolveGitRepository(workspace.path, input.repositoryPath ?? '');
|
|
149
132
|
await restoreCurrentChange(repository, input.path, input.state);
|
|
150
133
|
this.options.invalidate(workspace.id);
|