@myagentroam/node 0.9.176 → 0.9.177
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 +26 -6
- package/dist/runner/codex/conversation-parser.js +2 -2
- package/dist/runner/opencode/conversation-parser.js +4 -4
- package/dist/runtime-state.js +7 -3
- package/dist/service/attachment-domain-state.js +2 -0
- package/dist/service/composer-message-input.js +50 -0
- package/dist/service/continuous-run-coordinator.js +8 -3
- package/dist/service/conversation-history-service.js +9 -6
- package/dist/service/goal-runtime.js +14 -35
- package/dist/service/run-attachment-service.js +158 -6
- package/dist/service/run-event-service.js +3 -0
- package/dist/service/session-identity-service.js +2 -0
- package/dist/service/session-lifecycle-service.js +2 -1
- package/dist/service/session-message-service.js +39 -9
- package/dist/service/workspace-queue-workbench-service.js +1 -1
- package/dist/util/node-operation-parsers.js +18 -7
- package/dist/util/runner-native-session-parsers.js +3 -3
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname, resolve } from 'node:path';
|
|
2
|
-
import { createEnvelope, directNodeCommandSchema } from '@myagentroam/protocol';
|
|
2
|
+
import { createEnvelope, composerVisibleText, directNodeCommandSchema } from '@myagentroam/protocol';
|
|
3
3
|
import { hostedConversationStorePath, loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
|
|
4
4
|
import { NodeMetrics, nodeLog } from './operational.js';
|
|
5
5
|
import { assertLocalSqlitePath } from './storage.js';
|
|
@@ -61,6 +61,7 @@ import { RunnerInteractionService } from './service/runner-interaction-service.j
|
|
|
61
61
|
import { SessionLifecycleService } from './service/session-lifecycle-service.js';
|
|
62
62
|
import { ExternalSessionResumeService } from './service/external-session-resume-service.js';
|
|
63
63
|
import { SessionMessageService } from './service/session-message-service.js';
|
|
64
|
+
import { composerResourceInputTags } from './service/composer-message-input.js';
|
|
64
65
|
import { SessionScheduleService } from './service/session-schedule-service.js';
|
|
65
66
|
import { ScheduledSessionCoordinator } from './service/scheduled-session-coordinator.js';
|
|
66
67
|
import { ScheduledSessionStore } from './service/scheduled-session-store.js';
|
|
@@ -343,6 +344,7 @@ export class NodeConnector {
|
|
|
343
344
|
this.publishWorkbenchEvent(category, payload);
|
|
344
345
|
},
|
|
345
346
|
cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
|
|
347
|
+
refreshAttachments: (sessionId) => this.runAttachmentService.refreshSession(sessionId),
|
|
346
348
|
expireImages: (runId) => this.runAttachmentService.expire(runId),
|
|
347
349
|
scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
|
|
348
350
|
refreshWorkspaceSessions: (workspaceId) => this.workspaceWorkbenchService.ensureTopicSnapshot(workspaceId, 'sessions'),
|
|
@@ -515,7 +517,8 @@ export class NodeConnector {
|
|
|
515
517
|
this.fakeRunner =
|
|
516
518
|
options.fakeRunner === undefined ? undefined : new FakeTestRunner(options.fakeRunner);
|
|
517
519
|
this.fakeScript = options.fakeScript;
|
|
518
|
-
this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoots(session))
|
|
520
|
+
this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoots(session), (sessionId) => this.runtime.activeSessionRun(sessionId) !== undefined ||
|
|
521
|
+
this.runtime.sessionQueue(sessionId).itemIds.length > 0);
|
|
519
522
|
this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory, options.codexRunClientFactory);
|
|
520
523
|
this.codexRunController = new CodexManagedRunController(this.codexClient, {
|
|
521
524
|
available: () => this.capabilities.codex.available,
|
|
@@ -747,9 +750,15 @@ export class NodeConnector {
|
|
|
747
750
|
});
|
|
748
751
|
this.continuousRunCoordinator = new ContinuousRunCoordinator({
|
|
749
752
|
runtime: this.runtime,
|
|
750
|
-
emitQueue: (workspaceId, sessionId) =>
|
|
753
|
+
emitQueue: (workspaceId, sessionId) => {
|
|
754
|
+
this.runEventBridge.emitQueue(workspaceId, sessionId);
|
|
755
|
+
this.runAttachmentService.refreshSession(sessionId);
|
|
756
|
+
},
|
|
751
757
|
clearImages: (runId) => this.runAttachmentService.clear(runId),
|
|
752
|
-
cleanupAttachments: (runId) =>
|
|
758
|
+
cleanupAttachments: (runId) => {
|
|
759
|
+
this.runAttachmentService.cleanup(runId);
|
|
760
|
+
void this.runAttachmentService.discardFiles(runId);
|
|
761
|
+
},
|
|
753
762
|
migrateAssets: (previousId, nextId) => this.runAttachmentService.move(previousId, nextId),
|
|
754
763
|
emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
|
|
755
764
|
emitConversation: (turn) => this.emitConversationTurn(turn),
|
|
@@ -894,6 +903,9 @@ export class NodeConnector {
|
|
|
894
903
|
});
|
|
895
904
|
this.sessionIdentityService = new SessionIdentityService({
|
|
896
905
|
runtime: this.runtime,
|
|
906
|
+
releaseAttachments: (sessionId) => {
|
|
907
|
+
void this.runAttachmentService.releaseSession(sessionId);
|
|
908
|
+
},
|
|
897
909
|
persist: (session, nativeSessionId) => {
|
|
898
910
|
if (session.model === null || session.effort === null || session.access === null)
|
|
899
911
|
return;
|
|
@@ -908,6 +920,7 @@ export class NodeConnector {
|
|
|
908
920
|
},
|
|
909
921
|
migrateCommands: (previousId, nextId) => this.commandStates.promoteSessionIdentity(previousId, nextId),
|
|
910
922
|
migrateGoal: (previousId, nextId) => {
|
|
923
|
+
this.runAttachmentService.migrateSession(previousId, nextId);
|
|
911
924
|
this.goalRuntime.migrateSession(previousId, nextId);
|
|
912
925
|
this.goalMcpServer.migrateSession(previousId, nextId);
|
|
913
926
|
const retry = this.goalRetryTimers.get(previousId);
|
|
@@ -1076,6 +1089,7 @@ export class NodeConnector {
|
|
|
1076
1089
|
coordinator: this.continuousRunCoordinator,
|
|
1077
1090
|
runners: this.runners,
|
|
1078
1091
|
resolve: (sessionId) => this.externalSessionResumeService.acceptForMessage(sessionId),
|
|
1092
|
+
workspacePath: (workspaceId) => this.database?.getWorkspace(workspaceId)?.path,
|
|
1079
1093
|
present: (session) => this.sessionPresentationService.present(session),
|
|
1080
1094
|
emitSession: (session) => this.emitSessionWorkbenchEvent({
|
|
1081
1095
|
session: this.sessionPresentationService.present(session)
|
|
@@ -1106,8 +1120,11 @@ export class NodeConnector {
|
|
|
1106
1120
|
activated: false
|
|
1107
1121
|
};
|
|
1108
1122
|
}
|
|
1123
|
+
const objective = composerVisibleText(content);
|
|
1124
|
+
if (objective.length === 0)
|
|
1125
|
+
return undefined;
|
|
1109
1126
|
this.invalidateGoalContinuation(session.id);
|
|
1110
|
-
const goal = this.goalRuntime.start(session.id, content);
|
|
1127
|
+
const goal = this.goalRuntime.start(session.id, objective, composerResourceInputTags(content));
|
|
1111
1128
|
this.publishGoalState(session, goal);
|
|
1112
1129
|
return {
|
|
1113
1130
|
runnerContent: this.goalRuntime.prompt(session.id, 'start'),
|
|
@@ -1144,7 +1161,10 @@ export class NodeConnector {
|
|
|
1144
1161
|
present: (session) => this.sessionPresentationService.present(session),
|
|
1145
1162
|
projectionForNative: (session) => this.nativeSessionProjectionService.projectionForNative(session),
|
|
1146
1163
|
nativeIds: (session) => this.nativeSessionProjectionService.nativeIds(session),
|
|
1147
|
-
forget: (session, nativeIds) =>
|
|
1164
|
+
forget: (session, nativeIds) => {
|
|
1165
|
+
this.nativeSessionProjectionService.forget(session, nativeIds);
|
|
1166
|
+
void this.runAttachmentService.releaseSession(session.id);
|
|
1167
|
+
},
|
|
1148
1168
|
refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
|
|
1149
1169
|
externalActivity: (sessionId) => this.sessionState.externalActivity.get(sessionId),
|
|
1150
1170
|
isWatching: (sessionId) => this.nativeSessionWatchService.isWatching(sessionId),
|
|
@@ -3,7 +3,7 @@ import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
|
3
3
|
import { conversationImageAttachmentSchema } from '@myagentroam/protocol';
|
|
4
4
|
import { parseCodexComposerImageMarkers } from './composer-image-markers.js';
|
|
5
5
|
import { normalizeCodexTool } from './tool-normalizer.js';
|
|
6
|
-
import {
|
|
6
|
+
import { publicGoalHistoryText } from '../../service/goal-runtime.js';
|
|
7
7
|
const MAX_COMPOSER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
8
8
|
const MAX_COMPOSER_ATTACHMENTS_BYTES = 10 * 1024 * 1024;
|
|
9
9
|
const MAX_ASSISTANT_MESSAGE_CHARACTERS = 64 * 1024;
|
|
@@ -529,7 +529,7 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
|
|
|
529
529
|
completedAt
|
|
530
530
|
};
|
|
531
531
|
if (value.type === 'userMessage') {
|
|
532
|
-
const text =
|
|
532
|
+
const text = publicGoalHistoryText(codexContentText(value.content));
|
|
533
533
|
const hasImage = Array.isArray(value.content) &&
|
|
534
534
|
value.content.some((part) => isPlainRecord(part) &&
|
|
535
535
|
(part.type === 'image' ||
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { boundedConversationItemId, compactRunnerText } from '../codex/conversation-parser.js';
|
|
2
|
-
import {
|
|
2
|
+
import { publicGoalHistoryText } from '../../service/goal-runtime.js';
|
|
3
3
|
import { normalizeOpenCodeTool } from './tool-normalizer.js';
|
|
4
4
|
export function openCodeUserMessagePreview(messages) {
|
|
5
5
|
for (const message of messages) {
|
|
@@ -21,7 +21,7 @@ export function openCodeUserMessagePreview(messages) {
|
|
|
21
21
|
: [];
|
|
22
22
|
})
|
|
23
23
|
.join('\n');
|
|
24
|
-
const preview =
|
|
24
|
+
const preview = publicGoalHistoryText(text).trim().replace(/\s+/gu, ' ').slice(0, 160);
|
|
25
25
|
if (preview)
|
|
26
26
|
return preview;
|
|
27
27
|
}
|
|
@@ -75,9 +75,9 @@ function openCodeTurn(session, user, userParts, assistants, sequence, managed) {
|
|
|
75
75
|
const localUser = managed?.items.find((item) => item.kind === 'user_message');
|
|
76
76
|
if (localUser !== undefined)
|
|
77
77
|
items.push({ ...localUser, turnId: managed.id });
|
|
78
|
-
else if (
|
|
78
|
+
else if (publicGoalHistoryText(userText).length > 0)
|
|
79
79
|
items.push(item(session, nativeTurnId, `user-${nativeTurnId}`, 'user_message', 'COMPLETED', {
|
|
80
|
-
text: compactRunnerText(
|
|
80
|
+
text: compactRunnerText(publicGoalHistoryText(userText), 20_000),
|
|
81
81
|
attachments: []
|
|
82
82
|
}, startedAt, startedAt, sequence));
|
|
83
83
|
for (const part of compactionParts) {
|
package/dist/runtime-state.js
CHANGED
|
@@ -525,10 +525,12 @@ export class NodeRuntimeState {
|
|
|
525
525
|
this.bumpQueueVersion(item.sessionId);
|
|
526
526
|
return next;
|
|
527
527
|
}
|
|
528
|
-
updateQueueItemAttachments(itemId, attachments) {
|
|
528
|
+
updateQueueItemAttachments(itemId, attachments, content) {
|
|
529
529
|
const item = this.requireQueueItem(itemId);
|
|
530
|
-
const next = { ...item, attachments };
|
|
530
|
+
const next = { ...item, attachments, ...(content === undefined ? {} : { content }) };
|
|
531
531
|
this.queueItems.set(itemId, next);
|
|
532
|
+
if (content !== undefined && content !== item.content)
|
|
533
|
+
this.bumpQueueVersion(item.sessionId);
|
|
532
534
|
return next;
|
|
533
535
|
}
|
|
534
536
|
deleteQueueItem(itemId) {
|
|
@@ -1224,7 +1226,9 @@ export class NodeRuntimeState {
|
|
|
1224
1226
|
}
|
|
1225
1227
|
projectMessageAttachments(runId, attachments) {
|
|
1226
1228
|
let imageIndex = 0;
|
|
1227
|
-
return attachments
|
|
1229
|
+
return attachments
|
|
1230
|
+
.filter(({ mime }) => mime.startsWith('image/'))
|
|
1231
|
+
.map(({ name, mime, dataBase64 }) => {
|
|
1228
1232
|
const attachment = {
|
|
1229
1233
|
name,
|
|
1230
1234
|
mime,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { parseComposerTags, serializeComposerTag } from '@myagentroam/protocol';
|
|
2
|
+
import { relative, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { SkillDirectoryService } from './skill-directory-service.js';
|
|
5
|
+
export function composerResourceInputTags(input) {
|
|
6
|
+
return parseComposerTags(input)
|
|
7
|
+
.filter((part) => part.kind === 'mar_skills' ||
|
|
8
|
+
part.kind === 'mar_references' ||
|
|
9
|
+
part.kind === 'mar_attachments')
|
|
10
|
+
.map((part) => part.raw)
|
|
11
|
+
.join('\n');
|
|
12
|
+
}
|
|
13
|
+
/** Product references keep their Workspace identity while tools use the Session's cwd. */
|
|
14
|
+
export function prepareComposerResourceInput(input, cwd, workspacePath) {
|
|
15
|
+
return parseComposerTags(input)
|
|
16
|
+
.map((part) => {
|
|
17
|
+
if (part.kind === 'text')
|
|
18
|
+
return part.text;
|
|
19
|
+
if (part.kind !== 'mar_references')
|
|
20
|
+
return part.raw;
|
|
21
|
+
return serializeComposerTag('mar_references', {
|
|
22
|
+
references: part.payload.references.map((reference) => ({
|
|
23
|
+
...reference,
|
|
24
|
+
runnerPath: relative(cwd, resolve(workspacePath, reference.path)).replaceAll('\\', '/')
|
|
25
|
+
}))
|
|
26
|
+
});
|
|
27
|
+
})
|
|
28
|
+
.join('');
|
|
29
|
+
}
|
|
30
|
+
/** Recheck selection identity, without loading Skill content into the message or granting access. */
|
|
31
|
+
export async function validateComposerSkills(input, workspacePath, home = homedir()) {
|
|
32
|
+
const selected = parseComposerTags(input).flatMap((part) => part.kind === 'mar_skills' ? part.payload.skills : []);
|
|
33
|
+
if (selected.length === 0)
|
|
34
|
+
return;
|
|
35
|
+
const directories = new SkillDirectoryService();
|
|
36
|
+
const [node, workspace] = await Promise.all([
|
|
37
|
+
directories.inspectNodeHome(home),
|
|
38
|
+
directories.inspectWorkspace(workspacePath)
|
|
39
|
+
]);
|
|
40
|
+
const effective = new Map(node.skills.map((skill) => [skill.name, { ...skill, source: 'NODE_HOME' }]));
|
|
41
|
+
for (const skill of workspace.skills)
|
|
42
|
+
effective.set(skill.name, { ...skill, source: 'WORKSPACE' });
|
|
43
|
+
for (const skill of selected) {
|
|
44
|
+
const current = effective.get(skill.name);
|
|
45
|
+
if (current === undefined ||
|
|
46
|
+
current.source !== skill.source ||
|
|
47
|
+
resolve(workspacePath, skill.path) !== resolve(current.skillFilePath))
|
|
48
|
+
throw new Error('MESSAGE_INVALID');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
|
|
2
2
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
3
|
+
import { prepareComposerResourceInput } from './composer-message-input.js';
|
|
3
4
|
export class ContinuousRunCoordinator {
|
|
4
5
|
options;
|
|
5
6
|
preparedInputs = new Map();
|
|
@@ -181,7 +182,11 @@ export class ContinuousRunCoordinator {
|
|
|
181
182
|
const pending = this.preparedInputs.get(itemId);
|
|
182
183
|
if (pending === undefined)
|
|
183
184
|
return;
|
|
184
|
-
|
|
185
|
+
const resources = prepareComposerResourceInput(input, pending.cwd, pending.workspacePath ?? pending.cwd);
|
|
186
|
+
this.preparedInputs.set(itemId, {
|
|
187
|
+
...pending,
|
|
188
|
+
input: withNonImageAttachmentPrompt(resources, pending.attachmentPaths, pending.attachmentFiles)
|
|
189
|
+
});
|
|
185
190
|
}
|
|
186
191
|
deleteQueueItem(itemId) {
|
|
187
192
|
const item = this.options.runtime.getQueueItem(itemId);
|
|
@@ -275,7 +280,7 @@ export class ContinuousRunCoordinator {
|
|
|
275
280
|
return;
|
|
276
281
|
}
|
|
277
282
|
this.prioritizeNextQueueItem(sessionId, itemId);
|
|
278
|
-
if (this.options.runtime.claimNextQueueItem(sessionId, withNonImageAttachmentPrompt(item.content, pending.attachmentPaths)) === undefined)
|
|
283
|
+
if (this.options.runtime.claimNextQueueItem(sessionId, withNonImageAttachmentPrompt(item.content, pending.attachmentPaths, pending.attachmentFiles)) === undefined)
|
|
279
284
|
return;
|
|
280
285
|
const execution = this.options.runtime.createExecution(active.id);
|
|
281
286
|
executionId = execution.id;
|
|
@@ -466,7 +471,7 @@ export class ContinuousRunCoordinator {
|
|
|
466
471
|
return;
|
|
467
472
|
}
|
|
468
473
|
this.prioritizeNextQueueItem(sessionId, itemId);
|
|
469
|
-
const { run, turn } = this.options.runtime.startRunFromQueueItem(itemId, withNonImageAttachmentPrompt(item.content, pending.attachmentPaths));
|
|
474
|
+
const { run, turn } = this.options.runtime.startRunFromQueueItem(itemId, withNonImageAttachmentPrompt(item.content, pending.attachmentPaths, pending.attachmentFiles));
|
|
470
475
|
this.immediateQueueItems.delete(itemId);
|
|
471
476
|
this.preparedInputs.delete(itemId);
|
|
472
477
|
this.preparedInputs.set(run.id, { ...pending, runId: run.id });
|
|
@@ -739,16 +739,19 @@ function sameConversationTurn(left, right) {
|
|
|
739
739
|
if (left.id === right.id)
|
|
740
740
|
return true;
|
|
741
741
|
const leftMessageId = conversationClientMessageId(left);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
742
|
+
const rightMessageId = conversationClientMessageId(right);
|
|
743
|
+
if (leftMessageId !== null && rightMessageId !== null)
|
|
744
|
+
return leftMessageId === rightMessageId;
|
|
745
|
+
if (left.userItemId !== null && right.userItemId !== null)
|
|
745
746
|
return false;
|
|
746
|
-
//
|
|
747
|
-
//
|
|
747
|
+
// Runtime namespaces stable Runner Items by Run. A failure before input
|
|
748
|
+
// persistence can leave an error-only native Turn and a local user Turn.
|
|
748
749
|
const [native, local] = left.runId === null ? [left, right] : [right, left];
|
|
749
750
|
if (native.runId !== null || local.runId === null)
|
|
750
751
|
return false;
|
|
751
|
-
return native.items.some((item) => local.items.some((candidate) => candidate.kind === item.kind &&
|
|
752
|
+
return native.items.some((item) => local.items.some((candidate) => candidate.kind === item.kind &&
|
|
753
|
+
candidate.id === `${local.runId}:${item.id}` &&
|
|
754
|
+
((native.userItemId === null && local.userItemId === null) || item.kind === 'error')));
|
|
752
755
|
}
|
|
753
756
|
function terminalConversationTurn(status) {
|
|
754
757
|
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { parseComposerTags, serializeComposerTag } from '@myagentroam/protocol';
|
|
2
3
|
export function goalUnrestrictedAccess(runner) {
|
|
3
4
|
if (runner === 'codex' || runner === 'mar-agent')
|
|
4
5
|
return 'full-access';
|
|
@@ -10,34 +11,15 @@ const START_OPEN = '<mar_goal_start>';
|
|
|
10
11
|
const START_CLOSE = '</mar_goal_start>';
|
|
11
12
|
const CONTINUE_OPEN = '<mar_goal_continue>';
|
|
12
13
|
const CONTINUE_CLOSE = '</mar_goal_continue>';
|
|
13
|
-
const LEGACY_START_OPEN = '<mar_mission_start>';
|
|
14
|
-
const LEGACY_START_CLOSE = '</mar_mission_start>';
|
|
15
|
-
const LEGACY_CONTINUE_OPEN = '<mar_mission_continue>';
|
|
16
|
-
const LEGACY_CONTINUE_CLOSE = '</mar_mission_continue>';
|
|
17
14
|
function goalPromptTags(text) {
|
|
18
15
|
if (text.startsWith(START_OPEN))
|
|
19
|
-
return { open: START_OPEN, close: START_CLOSE,
|
|
16
|
+
return { open: START_OPEN, close: START_CLOSE, continuation: false };
|
|
20
17
|
if (text.startsWith(CONTINUE_OPEN))
|
|
21
|
-
return { open: CONTINUE_OPEN, close: CONTINUE_CLOSE,
|
|
22
|
-
if (text.startsWith(LEGACY_START_OPEN))
|
|
23
|
-
return {
|
|
24
|
-
open: LEGACY_START_OPEN,
|
|
25
|
-
close: LEGACY_START_CLOSE,
|
|
26
|
-
legacy: true,
|
|
27
|
-
continuation: false
|
|
28
|
-
};
|
|
29
|
-
if (text.startsWith(LEGACY_CONTINUE_OPEN))
|
|
30
|
-
return {
|
|
31
|
-
open: LEGACY_CONTINUE_OPEN,
|
|
32
|
-
close: LEGACY_CONTINUE_CLOSE,
|
|
33
|
-
legacy: true,
|
|
34
|
-
continuation: true
|
|
35
|
-
};
|
|
18
|
+
return { open: CONTINUE_OPEN, close: CONTINUE_CLOSE, continuation: true };
|
|
36
19
|
return undefined;
|
|
37
20
|
}
|
|
38
21
|
export function publicGoalStartText(objective) {
|
|
39
|
-
|
|
40
|
-
return normalized.length === 0 ? 'Goal Start' : `Goal Start\n${normalized}`;
|
|
22
|
+
return serializeComposerTag('mar_goal_start', { objective: objective.trim() });
|
|
41
23
|
}
|
|
42
24
|
export function stripGoalStartPrompt(text) {
|
|
43
25
|
const tags = goalPromptTags(text);
|
|
@@ -50,25 +32,20 @@ export function publicGoalHistoryText(text) {
|
|
|
50
32
|
const tags = goalPromptTags(text);
|
|
51
33
|
if (tags === undefined)
|
|
52
34
|
return text;
|
|
35
|
+
const safe = parseComposerTags(text)[0];
|
|
36
|
+
if (safe?.kind === 'mar_goal_start' || safe?.kind === 'mar_goal_continue')
|
|
37
|
+
return text;
|
|
53
38
|
const stripped = stripGoalStartPrompt(text);
|
|
54
|
-
if (stripped.length > 0)
|
|
55
|
-
return stripped;
|
|
56
39
|
if (tags.continuation)
|
|
57
|
-
return
|
|
40
|
+
return [serializeComposerTag('mar_goal_continue', {}), stripped].filter(Boolean).join('\n');
|
|
58
41
|
const close = text.lastIndexOf(tags.close);
|
|
59
42
|
const prompt = close === -1 ? text : text.slice(0, close);
|
|
60
43
|
const objectiveStart = prompt.indexOf('\nOriginal objective:\n');
|
|
61
|
-
const objectiveEnd = prompt.lastIndexOf(
|
|
62
|
-
? '\n\nContinue executing this Mission across turns:'
|
|
63
|
-
: '\n\nContinue executing this Goal across turns:');
|
|
44
|
+
const objectiveEnd = prompt.lastIndexOf('\n\nContinue executing this Goal across turns:');
|
|
64
45
|
const objective = objectiveStart === -1 || objectiveEnd === -1
|
|
65
46
|
? ''
|
|
66
47
|
: prompt.slice(objectiveStart + '\nOriginal objective:\n'.length, objectiveEnd);
|
|
67
|
-
return
|
|
68
|
-
? objective.trim().length === 0
|
|
69
|
-
? 'Mission Start'
|
|
70
|
-
: `Mission Start\n${objective.trim()}`
|
|
71
|
-
: publicGoalStartText(objective);
|
|
48
|
+
return [publicGoalStartText(objective), stripped].filter(Boolean).join('\n');
|
|
72
49
|
}
|
|
73
50
|
export class GoalRuntime {
|
|
74
51
|
changed;
|
|
@@ -96,17 +73,19 @@ export class GoalRuntime {
|
|
|
96
73
|
this.executionFailures.set(nextId, failures);
|
|
97
74
|
}
|
|
98
75
|
}
|
|
99
|
-
start(sessionId, objective) {
|
|
76
|
+
start(sessionId, objective, inputTags) {
|
|
100
77
|
const normalized = objective.trim();
|
|
101
78
|
if (normalized.length === 0 || normalized.length > 20_000)
|
|
102
79
|
throw new Error('GOAL_OBJECTIVE_INVALID');
|
|
103
80
|
const current = this.goals.get(sessionId);
|
|
104
81
|
const continuing = current?.status === 'active';
|
|
82
|
+
const selectedInputTags = inputTags ?? (continuing ? current?.inputTags : undefined);
|
|
105
83
|
const now = this.now();
|
|
106
84
|
const goal = {
|
|
107
85
|
id: continuing ? current.id : randomUUID(),
|
|
108
86
|
sessionId,
|
|
109
87
|
objective: normalized,
|
|
88
|
+
...(selectedInputTags ? { inputTags: selectedInputTags } : {}),
|
|
110
89
|
startedAt: continuing ? current.startedAt : now,
|
|
111
90
|
revisionStartedAt: now,
|
|
112
91
|
revision: continuing ? current.revision + 1 : 1,
|
|
@@ -245,7 +224,7 @@ export class GoalRuntime {
|
|
|
245
224
|
const heading = kind === 'start' ? 'A Goal has started or changed.' : 'Continue the active Goal.';
|
|
246
225
|
const open = kind === 'start' ? START_OPEN : CONTINUE_OPEN;
|
|
247
226
|
const close = kind === 'start' ? START_CLOSE : CONTINUE_CLOSE;
|
|
248
|
-
return `${open}\n${heading}\n\nOriginal objective:\n${goal.objective}\n\nContinue executing this Goal across turns:\n- This Goal is owned by the Node's myagentroam_goal MCP server. Prefer myagentroam_goal.mar_update_goal to complete it. In Codex, a runner-native Goal created during this Goal may also complete it when Codex reports that native Goal complete; native Goal state from another task or turn does not count. Do not claim completion based only on a final message or an unrelated Goal.\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call mar_update_goal with status=completed when available.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use mar_get_goal only when you need to reread the authoritative objective or status.\n- After mar_update_goal confirms completed or finally blocked, summarize the completed work, verification evidence, remaining issues, and the tool-reported elapsedWallMs converted from milliseconds into a concise readable duration in your final user-facing response. If Codex instead confirms completion with its native Goal, summarize the work without claiming a MAR elapsedWallMs value. Name whether the Goal completed or is finally blocked. MAR elapsedWallMs includes waiting and retries through the status update, not tool execution time or the final response. If blocked returns active for the required retry, continue work instead of claiming a final summary.\n${close}`;
|
|
227
|
+
return `${open}\n${heading}\n\nOriginal objective:\n${goal.objective}\n\nContinue executing this Goal across turns:\n- This Goal is owned by the Node's myagentroam_goal MCP server. Prefer myagentroam_goal.mar_update_goal to complete it. In Codex, a runner-native Goal created during this Goal may also complete it when Codex reports that native Goal complete; native Goal state from another task or turn does not count. Do not claim completion based only on a final message or an unrelated Goal.\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call mar_update_goal with status=completed when available.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use mar_get_goal only when you need to reread the authoritative objective or status.\n- After mar_update_goal confirms completed or finally blocked, summarize the completed work, verification evidence, remaining issues, and the tool-reported elapsedWallMs converted from milliseconds into a concise readable duration in your final user-facing response. If Codex instead confirms completion with its native Goal, summarize the work without claiming a MAR elapsedWallMs value. Name whether the Goal completed or is finally blocked. MAR elapsedWallMs includes waiting and retries through the status update, not tool execution time or the final response. If blocked returns active for the required retry, continue work instead of claiming a final summary.\n${close}${goal.inputTags ? `\n${goal.inputTags}` : ''}`;
|
|
249
228
|
}
|
|
250
229
|
requireCurrent(sessionId, goalId, revision) {
|
|
251
230
|
const current = this.goals.get(sessionId);
|
|
@@ -7,24 +7,162 @@ import { AttachmentDomainState } from './attachment-domain-state.js';
|
|
|
7
7
|
const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
8
8
|
const IMAGE_CACHE_TTL_MS = 60 * 60_000;
|
|
9
9
|
const NATIVE_IMAGE_SOURCE_TTL_MS = 60 * 60_000;
|
|
10
|
+
const SESSION_ATTACHMENT_IDLE_TTL_MS = 30 * 60_000;
|
|
10
11
|
export class RunAttachmentService {
|
|
11
12
|
nativeImageRoots;
|
|
13
|
+
sessionIsBusy;
|
|
12
14
|
state = new AttachmentDomainState();
|
|
13
|
-
|
|
15
|
+
disposed = false;
|
|
16
|
+
constructor(nativeImageRoots, sessionIsBusy = () => false) {
|
|
14
17
|
this.nativeImageRoots = nativeImageRoots;
|
|
18
|
+
this.sessionIsBusy = sessionIsBusy;
|
|
19
|
+
}
|
|
20
|
+
protectSession(sessionId) {
|
|
21
|
+
this.assertOpen();
|
|
22
|
+
const retained = this.sessionRetention(sessionId);
|
|
23
|
+
retained.protections += 1;
|
|
24
|
+
this.stopSessionTimer(retained);
|
|
25
|
+
let released = false;
|
|
26
|
+
return () => {
|
|
27
|
+
if (released)
|
|
28
|
+
return;
|
|
29
|
+
released = true;
|
|
30
|
+
const current = this.state.sessionRetentions.get(retained.sessionId);
|
|
31
|
+
if (current === undefined)
|
|
32
|
+
return;
|
|
33
|
+
current.protections -= 1;
|
|
34
|
+
this.refreshSession(current.sessionId);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
acceptSessionInput(sessionId) {
|
|
38
|
+
const retained = this.state.sessionRetentions.get(sessionId);
|
|
39
|
+
if (retained === undefined)
|
|
40
|
+
return;
|
|
41
|
+
this.stopSessionTimer(retained);
|
|
42
|
+
retained.deadline = undefined;
|
|
43
|
+
this.refreshSession(sessionId);
|
|
44
|
+
}
|
|
45
|
+
retainSessionFiles(sessionId, runId) {
|
|
46
|
+
this.assertOpen();
|
|
47
|
+
if (!this.state.runFileDirectories.has(runId))
|
|
48
|
+
return;
|
|
49
|
+
const retained = this.sessionRetention(sessionId);
|
|
50
|
+
retained.runs.add(runId);
|
|
51
|
+
this.state.runFileSessions.set(runId, sessionId);
|
|
52
|
+
this.refreshSession(sessionId);
|
|
53
|
+
}
|
|
54
|
+
refreshSession(sessionId) {
|
|
55
|
+
if (this.disposed)
|
|
56
|
+
return;
|
|
57
|
+
const retained = this.state.sessionRetentions.get(sessionId);
|
|
58
|
+
if (retained === undefined)
|
|
59
|
+
return;
|
|
60
|
+
this.stopSessionTimer(retained);
|
|
61
|
+
if (retained.protections > 0)
|
|
62
|
+
return;
|
|
63
|
+
if (this.sessionIsBusy(sessionId)) {
|
|
64
|
+
retained.deadline = undefined;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (retained.runs.size === 0) {
|
|
68
|
+
this.state.sessionRetentions.delete(sessionId);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
retained.deadline ??= Date.now() + SESSION_ATTACHMENT_IDLE_TTL_MS;
|
|
72
|
+
const timer = setTimeout(async () => {
|
|
73
|
+
if (this.state.sessionRetentions.get(sessionId) !== retained || retained.timer !== timer)
|
|
74
|
+
return;
|
|
75
|
+
retained.timer = undefined;
|
|
76
|
+
if (retained.protections > 0 || this.sessionIsBusy(sessionId)) {
|
|
77
|
+
this.refreshSession(sessionId);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await this.releaseSession(sessionId);
|
|
81
|
+
}, Math.max(0, retained.deadline - Date.now()));
|
|
82
|
+
timer.unref();
|
|
83
|
+
retained.timer = timer;
|
|
84
|
+
}
|
|
85
|
+
migrateSession(previousId, nextId) {
|
|
86
|
+
if (previousId === nextId)
|
|
87
|
+
return;
|
|
88
|
+
const previous = this.state.sessionRetentions.get(previousId);
|
|
89
|
+
if (previous === undefined)
|
|
90
|
+
return;
|
|
91
|
+
this.stopSessionTimer(previous);
|
|
92
|
+
const existing = this.state.sessionRetentions.get(nextId);
|
|
93
|
+
if (existing !== undefined) {
|
|
94
|
+
this.stopSessionTimer(existing);
|
|
95
|
+
for (const runId of previous.runs)
|
|
96
|
+
existing.runs.add(runId);
|
|
97
|
+
existing.protections += previous.protections;
|
|
98
|
+
existing.deadline = Math.max(existing.deadline ?? 0, previous.deadline ?? 0) || undefined;
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
this.state.sessionRetentions.set(nextId, previous);
|
|
102
|
+
previous.sessionId = nextId;
|
|
103
|
+
this.state.sessionRetentions.delete(previousId);
|
|
104
|
+
for (const runId of previous.runs)
|
|
105
|
+
this.state.runFileSessions.set(runId, nextId);
|
|
106
|
+
this.refreshSession(nextId);
|
|
107
|
+
}
|
|
108
|
+
async discardFiles(runId) {
|
|
109
|
+
const directory = this.state.runFileDirectories.get(runId);
|
|
110
|
+
this.state.runFileDirectories.delete(runId);
|
|
111
|
+
const sessionId = this.state.runFileSessions.get(runId);
|
|
112
|
+
this.state.runFileSessions.delete(runId);
|
|
113
|
+
if (sessionId !== undefined) {
|
|
114
|
+
this.state.sessionRetentions.get(sessionId)?.runs.delete(runId);
|
|
115
|
+
this.refreshSession(sessionId);
|
|
116
|
+
}
|
|
117
|
+
if (directory !== undefined)
|
|
118
|
+
await rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
119
|
+
}
|
|
120
|
+
async releaseSession(sessionId) {
|
|
121
|
+
const retained = this.state.sessionRetentions.get(sessionId);
|
|
122
|
+
if (retained === undefined)
|
|
123
|
+
return;
|
|
124
|
+
this.stopSessionTimer(retained);
|
|
125
|
+
this.state.sessionRetentions.delete(sessionId);
|
|
126
|
+
await Promise.all([...retained.runs].map((runId) => this.discardFiles(runId)));
|
|
127
|
+
}
|
|
128
|
+
sessionRetention(sessionId) {
|
|
129
|
+
const existing = this.state.sessionRetentions.get(sessionId);
|
|
130
|
+
if (existing !== undefined)
|
|
131
|
+
return existing;
|
|
132
|
+
const retained = {
|
|
133
|
+
sessionId,
|
|
134
|
+
runs: new Set(),
|
|
135
|
+
protections: 0,
|
|
136
|
+
deadline: undefined,
|
|
137
|
+
timer: undefined
|
|
138
|
+
};
|
|
139
|
+
this.state.sessionRetentions.set(sessionId, retained);
|
|
140
|
+
return retained;
|
|
141
|
+
}
|
|
142
|
+
assertOpen() {
|
|
143
|
+
if (this.disposed)
|
|
144
|
+
throw new Error('NODE_STOPPED');
|
|
145
|
+
}
|
|
146
|
+
stopSessionTimer(retained) {
|
|
147
|
+
if (retained.timer !== undefined)
|
|
148
|
+
clearTimeout(retained.timer);
|
|
149
|
+
retained.timer = undefined;
|
|
15
150
|
}
|
|
16
151
|
async materializeCodex(runId, attachments) {
|
|
17
152
|
if (attachments.length === 0)
|
|
18
153
|
return [];
|
|
154
|
+
this.assertOpen();
|
|
19
155
|
const directory = await mkdtemp(join(tmpdir(), 'myagentroam-attachment-'));
|
|
20
156
|
this.state.runDirectories.set(runId, directory);
|
|
21
157
|
try {
|
|
158
|
+
this.assertOpen();
|
|
22
159
|
const inputs = await Promise.all(attachments.map(async (attachment, index) => {
|
|
23
160
|
const extension = attachment.mime.slice('image/'.length).replace('jpeg', 'jpg');
|
|
24
161
|
const path = join(directory, `${index + 1}.${extension}`);
|
|
25
162
|
await writeFile(path, Buffer.from(attachment.dataBase64, 'base64'), { mode: 0o600 });
|
|
26
163
|
return { type: 'localImage', path };
|
|
27
164
|
}));
|
|
165
|
+
this.assertOpen();
|
|
28
166
|
const cached = this.state.runImages.get(runId);
|
|
29
167
|
if (cached !== undefined)
|
|
30
168
|
cached.localPaths = inputs.map((input) => input.path);
|
|
@@ -38,6 +176,7 @@ export class RunAttachmentService {
|
|
|
38
176
|
async persistFiles(workspacePath, runId, attachments) {
|
|
39
177
|
if (attachments.length === 0)
|
|
40
178
|
return [];
|
|
179
|
+
this.assertOpen();
|
|
41
180
|
const marDirectory = resolve(workspacePath, '.mar');
|
|
42
181
|
await mkdir(marDirectory, { recursive: true, mode: 0o700 });
|
|
43
182
|
const marMetadata = await lstat(marDirectory);
|
|
@@ -52,7 +191,9 @@ export class RunAttachmentService {
|
|
|
52
191
|
await mkdir(runDirectory, { mode: 0o700 });
|
|
53
192
|
this.state.runFileDirectories.set(runId, runDirectory);
|
|
54
193
|
try {
|
|
194
|
+
this.assertOpen();
|
|
55
195
|
await Promise.all(attachments.map((attachment) => writeFile(join(runDirectory, attachment.name), Buffer.from(attachment.dataBase64, 'base64'), { flag: 'wx', mode: 0o600 })));
|
|
196
|
+
this.assertOpen();
|
|
56
197
|
return attachments.map((attachment) => `.mar/temp-files/${runId}/${attachment.name}`);
|
|
57
198
|
}
|
|
58
199
|
catch (error) {
|
|
@@ -67,15 +208,13 @@ export class RunAttachmentService {
|
|
|
67
208
|
this.state.runDirectories.delete(runId);
|
|
68
209
|
void rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
69
210
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.state.runFileDirectories.delete(runId);
|
|
73
|
-
void rm(fileDirectory, { recursive: true, force: true }).catch(() => undefined);
|
|
74
|
-
}
|
|
211
|
+
if (!this.state.runFileSessions.has(runId))
|
|
212
|
+
void this.discardFiles(runId);
|
|
75
213
|
}
|
|
76
214
|
cache(runId, images) {
|
|
77
215
|
if (images.length === 0)
|
|
78
216
|
return;
|
|
217
|
+
this.assertOpen();
|
|
79
218
|
this.clear(runId);
|
|
80
219
|
this.state.runImages.set(runId, {
|
|
81
220
|
images,
|
|
@@ -100,6 +239,14 @@ export class RunAttachmentService {
|
|
|
100
239
|
this.state.runFileDirectories.delete(previousId);
|
|
101
240
|
this.state.runFileDirectories.set(nextId, fileDirectory);
|
|
102
241
|
}
|
|
242
|
+
const sessionId = this.state.runFileSessions.get(previousId);
|
|
243
|
+
if (sessionId !== undefined) {
|
|
244
|
+
this.state.runFileSessions.delete(previousId);
|
|
245
|
+
this.state.runFileSessions.set(nextId, sessionId);
|
|
246
|
+
const retained = this.state.sessionRetentions.get(sessionId);
|
|
247
|
+
retained?.runs.delete(previousId);
|
|
248
|
+
retained?.runs.add(nextId);
|
|
249
|
+
}
|
|
103
250
|
}
|
|
104
251
|
append(runId, images) {
|
|
105
252
|
if (images.length === 0)
|
|
@@ -306,6 +453,11 @@ export class RunAttachmentService {
|
|
|
306
453
|
this.state.runImages.delete(runId);
|
|
307
454
|
}
|
|
308
455
|
dispose() {
|
|
456
|
+
this.disposed = true;
|
|
457
|
+
for (const retained of this.state.sessionRetentions.values())
|
|
458
|
+
this.stopSessionTimer(retained);
|
|
459
|
+
this.state.sessionRetentions.clear();
|
|
460
|
+
this.state.runFileSessions.clear();
|
|
309
461
|
for (const runId of new Set([
|
|
310
462
|
...this.state.runDirectories.keys(),
|
|
311
463
|
...this.state.runFileDirectories.keys()
|
|
@@ -117,6 +117,8 @@ export class RunEventService {
|
|
|
117
117
|
}
|
|
118
118
|
if (eventType === 'run.started' || eventType === 'run.running')
|
|
119
119
|
this.options.executionStarted?.(executionId);
|
|
120
|
+
if (!terminalExecution)
|
|
121
|
+
this.options.refreshAttachments?.(run.sessionId);
|
|
120
122
|
let finalStatus;
|
|
121
123
|
let manuallyInterrupted = false;
|
|
122
124
|
if (terminalCurrentExecution) {
|
|
@@ -199,6 +201,7 @@ export class RunEventService {
|
|
|
199
201
|
this.options.onTerminalRun?.(run, finalStatus);
|
|
200
202
|
const idleSnapshot = this.options.refreshWorkspaceSessions(run.workspaceId);
|
|
201
203
|
this.options.runtime.removeTerminalRun(runId);
|
|
204
|
+
this.options.refreshAttachments?.(run.sessionId);
|
|
202
205
|
if (terminalNotification)
|
|
203
206
|
void idleSnapshot
|
|
204
207
|
.catch(() => undefined)
|
|
@@ -59,6 +59,8 @@ export class SessionIdentityService {
|
|
|
59
59
|
return promise;
|
|
60
60
|
}
|
|
61
61
|
reject(sessionId, code) {
|
|
62
|
+
if (this.bootstrapIds.has(sessionId))
|
|
63
|
+
this.options.releaseAttachments?.(sessionId);
|
|
62
64
|
const waiter = this.waiters.get(sessionId);
|
|
63
65
|
if (waiter === undefined)
|
|
64
66
|
return;
|
|
@@ -153,7 +153,8 @@ export class SessionLifecycleService {
|
|
|
153
153
|
if (typeof input.clientMessageId !== 'string' ||
|
|
154
154
|
input.clientMessageId.length === 0 ||
|
|
155
155
|
typeof input.content !== 'string' ||
|
|
156
|
-
input.content.length === 0
|
|
156
|
+
(input.content.length === 0 &&
|
|
157
|
+
!(Array.isArray(input.attachmentIds) && input.attachmentIds.length > 0)))
|
|
157
158
|
throw new Error('MESSAGE_INVALID');
|
|
158
159
|
if (input.deferredCommands !== undefined &&
|
|
159
160
|
(!Array.isArray(input.deferredCommands) ||
|
|
@@ -4,6 +4,9 @@ import { parseSecretEnvironment } from '../util/secret-environment.js';
|
|
|
4
4
|
import { personalInstructionsForRunner } from '../util/personal-instructions.js';
|
|
5
5
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
6
6
|
import { publicGoalStartText } from './goal-runtime.js';
|
|
7
|
+
import { serializeComposerTag, composerVisibleText } from '@myagentroam/protocol';
|
|
8
|
+
import { relative, resolve } from 'node:path';
|
|
9
|
+
import { prepareComposerResourceInput, composerResourceInputTags, validateComposerSkills } from './composer-message-input.js';
|
|
7
10
|
const INTERNAL_GOAL = Symbol('internalGoal');
|
|
8
11
|
const INTERNAL_SCHEDULE = Symbol('internalSchedule');
|
|
9
12
|
export class SessionMessageService {
|
|
@@ -24,7 +27,9 @@ export class SessionMessageService {
|
|
|
24
27
|
[INTERNAL_GOAL]: true,
|
|
25
28
|
sessionId: input.sessionId,
|
|
26
29
|
clientMessageId: `goal:${randomUUID()}`,
|
|
27
|
-
content: input.kind === 'start'
|
|
30
|
+
content: input.kind === 'start'
|
|
31
|
+
? publicGoalStartText(input.objective ?? '')
|
|
32
|
+
: serializeComposerTag('mar_goal_continue', {}),
|
|
28
33
|
__runnerContent: input.content,
|
|
29
34
|
__goalRef: input.goal,
|
|
30
35
|
...(input.initiatedByUserId === undefined
|
|
@@ -45,7 +50,7 @@ export class SessionMessageService {
|
|
|
45
50
|
sessionId: input.sessionId,
|
|
46
51
|
clientMessageId: input.occurrenceId,
|
|
47
52
|
content: input.objective,
|
|
48
|
-
__runnerContent:
|
|
53
|
+
__runnerContent: serializeComposerTag('mar_schedule', { objective: input.objective }),
|
|
49
54
|
__requestUserId: input.initiatedByUserId,
|
|
50
55
|
deliveryIntent: 'SEND',
|
|
51
56
|
secretEnvironment: input.secretEnvironment,
|
|
@@ -61,7 +66,9 @@ export class SessionMessageService {
|
|
|
61
66
|
typeof input.clientMessageId !== 'string' ||
|
|
62
67
|
input.clientMessageId.length === 0 ||
|
|
63
68
|
typeof input.content !== 'string' ||
|
|
64
|
-
input.content.length === 0
|
|
69
|
+
(input.content.length === 0 &&
|
|
70
|
+
!(Array.isArray(input.attachmentIds) && input.attachmentIds.length > 0) &&
|
|
71
|
+
!(Array.isArray(input.attachments) && input.attachments.length > 0)))
|
|
65
72
|
throw new Error('MESSAGE_INVALID');
|
|
66
73
|
const clientMessageId = input.clientMessageId;
|
|
67
74
|
const content = input.content;
|
|
@@ -106,8 +113,13 @@ export class SessionMessageService {
|
|
|
106
113
|
? undefined
|
|
107
114
|
: this.options.goalMessage(session, content, requestedIntent);
|
|
108
115
|
const runnerContent = goalMessage?.runnerContent ?? internalRunnerContent;
|
|
109
|
-
const publicContent = goalMessage?.activated === true
|
|
116
|
+
const publicContent = goalMessage?.activated === true
|
|
117
|
+
? [publicGoalStartText(composerVisibleText(content)), composerResourceInputTags(content)]
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join('\n')
|
|
120
|
+
: content;
|
|
110
121
|
const intent = goalMessage?.activated === true ? 'REPLACE_CURRENT' : requestedIntent;
|
|
122
|
+
const releasePreparation = this.options.attachments.protectSession(session.id);
|
|
111
123
|
const operation = (async () => {
|
|
112
124
|
const acceptedSession = accepted.session;
|
|
113
125
|
const session = this.options.runtime.getAgentSession(acceptedSession.id) ?? acceptedSession;
|
|
@@ -144,9 +156,21 @@ export class SessionMessageService {
|
|
|
144
156
|
: uploaded.map((attachment) => attachment.attachment);
|
|
145
157
|
const imageAttachments = attachments.filter(isImageAttachment);
|
|
146
158
|
const runnerImageAttachments = attachments.filter(isRunnerImageAttachment);
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
const
|
|
159
|
+
const workspacePath = this.options.workspacePath?.(currentSession.workspaceId) ?? currentSession.cwd;
|
|
160
|
+
await validateComposerSkills(runnerContent, workspacePath);
|
|
161
|
+
const attachmentPaths = await this.persistAttachments(currentSession, submissionId, attachments);
|
|
162
|
+
const attachmentFiles = attachments.map((attachment, index) => ({
|
|
163
|
+
name: attachment.name,
|
|
164
|
+
mime: attachment.mime,
|
|
165
|
+
path: attachmentPaths[index]
|
|
166
|
+
}));
|
|
167
|
+
const runnerFiles = attachmentFiles.map((file) => {
|
|
168
|
+
const path = relative(currentSession.cwd, resolve(workspacePath, file.path)).replaceAll('\\', '/');
|
|
169
|
+
return path === file.path ? file : { ...file, path, workspacePath: file.path };
|
|
170
|
+
});
|
|
171
|
+
const displayInput = withNonImageAttachmentPrompt(publicContent, attachmentPaths, attachmentFiles);
|
|
172
|
+
const resourceInput = prepareComposerResourceInput(runnerContent, currentSession.cwd, workspacePath);
|
|
173
|
+
const runnerInput = withNonImageAttachmentPrompt(resourceInput, attachmentPaths, runnerFiles);
|
|
150
174
|
const planActive = this.options.commands
|
|
151
175
|
.list(session.id)
|
|
152
176
|
.some((state) => state.commandId === 'plan');
|
|
@@ -157,7 +181,7 @@ export class SessionMessageService {
|
|
|
157
181
|
this.options.coordinator.publishCurrentTurn(created.run.id);
|
|
158
182
|
}
|
|
159
183
|
else
|
|
160
|
-
this.options.runtime.updateQueueItemAttachments(created.queueItem.id, attachments);
|
|
184
|
+
this.options.runtime.updateQueueItemAttachments(created.queueItem.id, attachments, displayInput);
|
|
161
185
|
this.options.coordinator.prepare(submissionId, {
|
|
162
186
|
runId,
|
|
163
187
|
sessionId: currentSession.id,
|
|
@@ -167,7 +191,9 @@ export class SessionMessageService {
|
|
|
167
191
|
clientMessageId,
|
|
168
192
|
attachments: runnerImageAttachments,
|
|
169
193
|
attachmentPaths,
|
|
194
|
+
attachmentFiles: runnerFiles,
|
|
170
195
|
cwd: currentSession.cwd,
|
|
196
|
+
workspacePath,
|
|
171
197
|
externalSessionId: currentSession.externalSessionId,
|
|
172
198
|
model: currentSession.model,
|
|
173
199
|
effort: currentSession.effort,
|
|
@@ -195,12 +221,14 @@ export class SessionMessageService {
|
|
|
195
221
|
if (goalMessage !== undefined &&
|
|
196
222
|
!this.options.prepareGoalInsertion(session.id, submissionId, goalMessage.goal))
|
|
197
223
|
throw new Error('GOAL_REVISION_STALE');
|
|
224
|
+
this.options.attachments.retainSessionFiles(currentSession.id, submissionId);
|
|
198
225
|
const delivery = this.options.coordinator.settle({
|
|
199
226
|
submissionId,
|
|
200
227
|
intent,
|
|
201
228
|
queueBefore: queueBeforeCreate,
|
|
202
229
|
...(replaceCurrentRun === undefined ? {} : { activeRunId: replaceCurrentRun.id })
|
|
203
230
|
});
|
|
231
|
+
this.options.attachments.acceptSessionInput(currentSession.id);
|
|
204
232
|
void this.options.uploads.consume(attachmentIds).catch(() => undefined);
|
|
205
233
|
return delivery;
|
|
206
234
|
}
|
|
@@ -210,6 +238,7 @@ export class SessionMessageService {
|
|
|
210
238
|
this.options.coordinator.failPreparation(submissionId, safeErrorCode(error, 'MESSAGE_PREPARATION_FAILED'));
|
|
211
239
|
this.options.attachments.clear(submissionId);
|
|
212
240
|
this.options.attachments.cleanup(submissionId);
|
|
241
|
+
await this.options.attachments.discardFiles(submissionId);
|
|
213
242
|
throw error;
|
|
214
243
|
}
|
|
215
244
|
};
|
|
@@ -228,12 +257,13 @@ export class SessionMessageService {
|
|
|
228
257
|
return await operation;
|
|
229
258
|
}
|
|
230
259
|
finally {
|
|
260
|
+
releasePreparation();
|
|
231
261
|
if (this.pendingMessages.get(pendingKey) === operation)
|
|
232
262
|
this.pendingMessages.delete(pendingKey);
|
|
233
263
|
}
|
|
234
264
|
}
|
|
235
265
|
async persistAttachments(session, runId, attachments) {
|
|
236
|
-
return this.options.attachments.persistFiles(session.cwd, runId, attachments
|
|
266
|
+
return this.options.attachments.persistFiles(this.options.workspacePath?.(session.workspaceId) ?? session.cwd, runId, attachments);
|
|
237
267
|
}
|
|
238
268
|
async channelMessage(input) {
|
|
239
269
|
if (typeof input.sessionId !== 'string' ||
|
|
@@ -107,7 +107,7 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
107
107
|
this.options.runtime.updateQueueItem(item.id, content);
|
|
108
108
|
const prepared = this.options.coordinator.pending(item.id);
|
|
109
109
|
if (prepared !== undefined) {
|
|
110
|
-
const runnerInput = withNonImageAttachmentPrompt(content, prepared.attachmentPaths);
|
|
110
|
+
const runnerInput = withNonImageAttachmentPrompt(content, prepared.attachmentPaths, prepared.attachmentFiles);
|
|
111
111
|
this.options.coordinator.updatePreparedInput(item.id, this.options.prepareInput(prepared.runner, runnerInput, prepared.collaborationMode));
|
|
112
112
|
}
|
|
113
113
|
this.options.emit(item.workspaceId, item.sessionId);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { nodeCapabilitiesSchema } from '@myagentroam/protocol';
|
|
1
|
+
import { nodeCapabilitiesSchema, serializeComposerTag, parseComposerTags, composerVisibleText } from '@myagentroam/protocol';
|
|
2
2
|
const MAX_COMPOSER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
3
3
|
const MAX_COMPOSER_ATTACHMENTS_BYTES = 10 * 1024 * 1024;
|
|
4
4
|
const USER_AUTHORIZATION_CLOCK_SKEW_MS = 60 * 60 * 1_000;
|
|
5
|
-
const NON_IMAGE_ATTACHMENT_PROMPT_PREFIX = '\n\nAttachments available in the Workspace:\n';
|
|
6
5
|
const COMPOSER_IMAGE_MIME_TYPES = new Set([
|
|
7
6
|
'image/png',
|
|
8
7
|
'image/jpeg',
|
|
@@ -243,7 +242,7 @@ export function validSessionTitle(value) {
|
|
|
243
242
|
(typeof value === 'string' && value.trim().length > 0 && value.length <= 160));
|
|
244
243
|
}
|
|
245
244
|
export function sessionTitleFromFirstMessage(content) {
|
|
246
|
-
return content
|
|
245
|
+
return composerVisibleText(content).replace(/\s+/g, ' ').slice(0, 160);
|
|
247
246
|
}
|
|
248
247
|
export function parseComposerAttachmentUpload(input) {
|
|
249
248
|
if (input.composerAttachmentId === undefined && input.mime === undefined)
|
|
@@ -323,12 +322,24 @@ export function isRunnerImageAttachment(attachment) {
|
|
|
323
322
|
export function isImageAttachment(attachment) {
|
|
324
323
|
return attachment.mime.startsWith('image/');
|
|
325
324
|
}
|
|
326
|
-
export function withNonImageAttachmentPrompt(input, paths) {
|
|
325
|
+
export function withNonImageAttachmentPrompt(input, paths, files) {
|
|
327
326
|
if (paths.length === 0)
|
|
328
327
|
return input;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
328
|
+
const tag = serializeComposerTag('mar_attachments', {
|
|
329
|
+
files: files
|
|
330
|
+
? [...files]
|
|
331
|
+
: paths.map((path) => ({
|
|
332
|
+
name: path.split(/[\\/]/u).at(-1),
|
|
333
|
+
mime: 'application/octet-stream',
|
|
334
|
+
path
|
|
335
|
+
}))
|
|
336
|
+
});
|
|
337
|
+
const body = parseComposerTags(input)
|
|
338
|
+
.filter((part) => part.kind !== 'mar_attachments')
|
|
339
|
+
.map((part) => (part.kind === 'text' ? part.text : part.raw))
|
|
340
|
+
.join('')
|
|
341
|
+
.trimEnd();
|
|
342
|
+
return `${body}\n${tag}`;
|
|
332
343
|
}
|
|
333
344
|
export function isCanonicalBase64(value) {
|
|
334
345
|
return value.length > 0 && value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { stripClaudePlanTag } from '../runner-command-engine.js';
|
|
4
|
-
import {
|
|
4
|
+
import { publicGoalHistoryText } from '../service/goal-runtime.js';
|
|
5
5
|
import { parseCodexComposerImageMarkers } from '../runner/codex/composer-image-markers.js';
|
|
6
6
|
import { normalizeCodexTool } from '../runner/codex/tool-normalizer.js';
|
|
7
7
|
import { normalizeClaudeTool } from '../runner/claude/tool-normalizer.js';
|
|
@@ -452,8 +452,8 @@ function runtimeUserMessagePayload(clientMessageId, runtimeTurns) {
|
|
|
452
452
|
return undefined;
|
|
453
453
|
}
|
|
454
454
|
function nativeUserMessageText(runner, text) {
|
|
455
|
-
const
|
|
456
|
-
const normalized =
|
|
455
|
+
const withoutPlan = runner === 'claude-code' ? stripClaudePlanTag(text) : text;
|
|
456
|
+
const normalized = publicGoalHistoryText(withoutPlan);
|
|
457
457
|
if (runner !== 'codex')
|
|
458
458
|
return normalized;
|
|
459
459
|
return parseCodexComposerImageMarkers(normalized).text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.177",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"playwright": "1.62.1",
|
|
37
37
|
"ws": "^8.21.3",
|
|
38
38
|
"zod": "4.4.3",
|
|
39
|
-
"@myagentroam/agent": "0.9.
|
|
40
|
-
"@myagentroam/
|
|
39
|
+
"@myagentroam/agent": "0.9.177",
|
|
40
|
+
"@myagentroam/marctl": "0.9.123",
|
|
41
41
|
"@myagentroam/code-navigation": "0.9.129",
|
|
42
|
-
"@myagentroam/
|
|
42
|
+
"@myagentroam/protocol": "0.9.177"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/ws": "^8.18.1"
|