@myagentroam/node 0.9.175 → 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 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) => this.runEventBridge.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) => this.runAttachmentService.cleanup(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) => this.nativeSessionProjectionService.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 { stripGoalStartPrompt } from '../../service/goal-runtime.js';
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 = stripGoalStartPrompt(codexContentText(value.content));
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 { stripGoalStartPrompt } from '../../service/goal-runtime.js';
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 = stripGoalStartPrompt(text).trim().replace(/\s+/gu, ' ').slice(0, 160);
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 (stripGoalStartPrompt(userText).length > 0)
78
+ else if (publicGoalHistoryText(userText).length > 0)
79
79
  items.push(item(session, nativeTurnId, `user-${nativeTurnId}`, 'user_message', 'COMPLETED', {
80
- text: compactRunnerText(stripGoalStartPrompt(userText), 20_000),
80
+ text: compactRunnerText(publicGoalHistoryText(userText), 20_000),
81
81
  attachments: []
82
82
  }, startedAt, startedAt, sequence));
83
83
  for (const part of compactionParts) {
@@ -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.map(({ name, mime, dataBase64 }) => {
1229
+ return attachments
1230
+ .filter(({ mime }) => mime.startsWith('image/'))
1231
+ .map(({ name, mime, dataBase64 }) => {
1228
1232
  const attachment = {
1229
1233
  name,
1230
1234
  mime,
@@ -1,6 +1,8 @@
1
1
  export class AttachmentDomainState {
2
2
  runDirectories = new Map();
3
3
  runFileDirectories = new Map();
4
+ runFileSessions = new Map();
5
+ sessionRetentions = new Map();
4
6
  runImages = new Map();
5
7
  nativeHistoryImages = new Map();
6
8
  nativeHistoryImageTimers = new Map();
@@ -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
- this.preparedInputs.set(itemId, { ...pending, input });
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
- if (leftMessageId !== null)
743
- return leftMessageId === conversationClientMessageId(right);
744
- if (left.userItemId !== null || right.userItemId !== null)
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
- // Item-only live Turns have no input receipt. Runtime namespaces the stable
747
- // Runner Item identity by Run, so reconcile it with the unprefixed history Item.
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 && candidate.id === `${local.runId}:${item.id}`));
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, legacy: false, continuation: false };
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, legacy: false, continuation: true };
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
- const normalized = objective.trim();
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 tags.legacy ? 'Mission Continue' : 'Goal Continue';
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(tags.legacy
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 tags.legacy
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
- constructor(nativeImageRoots) {
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
- const fileDirectory = this.state.runFileDirectories.get(runId);
71
- if (fileDirectory !== undefined) {
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' ? publicGoalStartText(input.objective ?? '') : 'Goal Continue',
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: `Scheduled task execution:\n${input.objective}`,
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 ? publicGoalStartText(content) : content;
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 attachmentPaths = await this.persistAttachments(currentSession, runId, attachments);
148
- const displayInput = withNonImageAttachmentPrompt(publicContent, attachmentPaths);
149
- const runnerInput = withNonImageAttachmentPrompt(runnerContent, attachmentPaths);
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.filter((attachment) => !isRunnerImageAttachment(attachment)));
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.trim().replace(/\s+/g, ' ').slice(0, 160);
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
- return `${input}${NON_IMAGE_ATTACHMENT_PROMPT_PREFIX}${paths
330
- .map((path) => `- ${path}`)
331
- .join('\n')}`;
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 { stripGoalStartPrompt } from '../service/goal-runtime.js';
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 goalFiltered = stripGoalStartPrompt(text);
456
- const normalized = runner === 'claude-code' ? stripClaudePlanTag(goalFiltered) : goalFiltered;
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.175",
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.175",
39
+ "@myagentroam/agent": "0.9.177",
40
40
  "@myagentroam/marctl": "0.9.123",
41
- "@myagentroam/protocol": "0.9.175",
42
- "@myagentroam/code-navigation": "0.9.129"
41
+ "@myagentroam/code-navigation": "0.9.129",
42
+ "@myagentroam/protocol": "0.9.177"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/ws": "^8.18.1"