@myagentroam/node 0.1.3 → 0.1.6

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.
Files changed (195) hide show
  1. package/README.md +9 -1
  2. package/dist/capabilities.d.ts +2 -0
  3. package/dist/capabilities.js +15 -4
  4. package/dist/claude-agent-sdk.d.ts +4 -16
  5. package/dist/claude-agent-sdk.js +18 -79
  6. package/dist/claude-channel.js +0 -1
  7. package/dist/codex-app-server.d.ts +5 -0
  8. package/dist/codex-app-server.js +26 -5
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +18 -2
  11. package/dist/connector/node-connector-options.d.ts +26 -0
  12. package/dist/connector/node-connector-options.js +1 -0
  13. package/dist/connector/node-operation-router.d.ts +9 -0
  14. package/dist/connector/node-operation-router.js +22 -0
  15. package/dist/connector.d.ts +62 -356
  16. package/dist/connector.js +714 -6176
  17. package/dist/control-outbound-scheduler.d.ts +26 -0
  18. package/dist/control-outbound-scheduler.js +121 -0
  19. package/dist/database.d.ts +4 -0
  20. package/dist/database.js +15 -10
  21. package/dist/event-buffer.js +0 -1
  22. package/dist/fake-runner.js +0 -1
  23. package/dist/health.js +0 -1
  24. package/dist/main.js +6 -1
  25. package/dist/migrations/v001.js +0 -1
  26. package/dist/migrations/v002.js +0 -1
  27. package/dist/migrations/v003.d.ts +4 -0
  28. package/dist/migrations/v003.js +44 -0
  29. package/dist/native-jsonl-reader.d.ts +14 -0
  30. package/dist/native-jsonl-reader.js +97 -0
  31. package/dist/native-session-history.d.ts +41 -4
  32. package/dist/native-session-history.js +340 -159
  33. package/dist/opencode-runtime.d.ts +33 -0
  34. package/dist/opencode-runtime.js +80 -0
  35. package/dist/opencode-server.d.ts +47 -0
  36. package/dist/opencode-server.js +250 -0
  37. package/dist/operational.js +10 -3
  38. package/dist/process-tree.js +0 -1
  39. package/dist/runner/abstract-runner.d.ts +153 -0
  40. package/dist/runner/abstract-runner.js +154 -0
  41. package/dist/runner/claude/managed-run-controller.d.ts +52 -0
  42. package/dist/runner/claude/managed-run-controller.js +326 -0
  43. package/dist/runner/claude-code-runner.d.ts +70 -0
  44. package/dist/runner/claude-code-runner.js +286 -0
  45. package/dist/runner/codex/conversation-parser.d.ts +59 -0
  46. package/dist/runner/codex/conversation-parser.js +816 -0
  47. package/dist/runner/codex/managed-run-controller.d.ts +80 -0
  48. package/dist/runner/codex/managed-run-controller.js +388 -0
  49. package/dist/runner/codex-runner.d.ts +85 -0
  50. package/dist/runner/codex-runner.js +365 -0
  51. package/dist/runner/fake-test-runner.d.ts +26 -0
  52. package/dist/runner/fake-test-runner.js +47 -0
  53. package/dist/runner/opencode/conversation-parser.d.ts +27 -0
  54. package/dist/runner/opencode/conversation-parser.js +217 -0
  55. package/dist/runner/opencode/managed-run-controller.d.ts +76 -0
  56. package/dist/runner/opencode/managed-run-controller.js +451 -0
  57. package/dist/runner/opencode-runner.d.ts +60 -0
  58. package/dist/runner/opencode-runner.js +330 -0
  59. package/dist/runner/runner-registry.d.ts +19 -0
  60. package/dist/runner/runner-registry.js +75 -0
  61. package/dist/runner-command-engine.js +0 -1
  62. package/dist/runner-profiles.d.ts +4 -0
  63. package/dist/runner-profiles.js +76 -3
  64. package/dist/runner-usage.d.ts +2 -2
  65. package/dist/runner-usage.js +3 -6
  66. package/dist/runtime-state.js +26 -14
  67. package/dist/service/attachment-domain-state.d.ts +5 -0
  68. package/dist/service/attachment-domain-state.js +5 -0
  69. package/dist/service/conversation-history-service.d.ts +34 -0
  70. package/dist/service/conversation-history-service.js +158 -0
  71. package/dist/service/external-session-resume-service.d.ts +32 -0
  72. package/dist/service/external-session-resume-service.js +98 -0
  73. package/dist/service/fake-managed-run-service.d.ts +29 -0
  74. package/dist/service/fake-managed-run-service.js +62 -0
  75. package/dist/service/managed-runner-session-service.d.ts +12 -0
  76. package/dist/service/managed-runner-session-service.js +35 -0
  77. package/dist/service/native-session-projection-service.d.ts +51 -0
  78. package/dist/service/native-session-projection-service.js +271 -0
  79. package/dist/service/native-session-watch-service.d.ts +37 -0
  80. package/dist/service/native-session-watch-service.js +150 -0
  81. package/dist/service/node-connection-lifecycle-service.d.ts +37 -0
  82. package/dist/service/node-connection-lifecycle-service.js +85 -0
  83. package/dist/service/node-control-channel.d.ts +32 -0
  84. package/dist/service/node-control-channel.js +85 -0
  85. package/dist/service/node-control-message-service.d.ts +28 -0
  86. package/dist/service/node-control-message-service.js +104 -0
  87. package/dist/service/node-request-service.d.ts +14 -0
  88. package/dist/service/node-request-service.js +50 -0
  89. package/dist/service/node-run-event-bridge.d.ts +40 -0
  90. package/dist/service/node-run-event-bridge.js +83 -0
  91. package/dist/service/node-terminal-channel.d.ts +29 -0
  92. package/dist/service/node-terminal-channel.js +182 -0
  93. package/dist/service/node-upgrade-coordinator.d.ts +29 -0
  94. package/dist/service/node-upgrade-coordinator.js +137 -0
  95. package/dist/service/node-workspace-coordinator.d.ts +32 -0
  96. package/dist/service/node-workspace-coordinator.js +80 -0
  97. package/dist/service/run-attachment-service.d.ts +37 -0
  98. package/dist/service/run-attachment-service.js +160 -0
  99. package/dist/service/run-domain-state.d.ts +4 -0
  100. package/dist/service/run-domain-state.js +4 -0
  101. package/dist/service/run-event-service.d.ts +31 -0
  102. package/dist/service/run-event-service.js +102 -0
  103. package/dist/service/run-workbench-service.d.ts +27 -0
  104. package/dist/service/run-workbench-service.js +113 -0
  105. package/dist/service/runner-channel-message-service.d.ts +13 -0
  106. package/dist/service/runner-channel-message-service.js +20 -0
  107. package/dist/service/runner-interaction-service.d.ts +40 -0
  108. package/dist/service/runner-interaction-service.js +246 -0
  109. package/dist/service/runner-service.d.ts +44 -0
  110. package/dist/service/runner-service.js +68 -0
  111. package/dist/service/session-catalog-service.d.ts +45 -0
  112. package/dist/service/session-catalog-service.js +224 -0
  113. package/dist/service/session-command-service.d.ts +23 -0
  114. package/dist/service/session-command-service.js +69 -0
  115. package/dist/service/session-context-service.d.ts +34 -0
  116. package/dist/service/session-context-service.js +87 -0
  117. package/dist/service/session-domain-state.d.ts +8 -0
  118. package/dist/service/session-domain-state.js +8 -0
  119. package/dist/service/session-identity-service.d.ts +24 -0
  120. package/dist/service/session-identity-service.js +86 -0
  121. package/dist/service/session-lifecycle-service.d.ts +31 -0
  122. package/dist/service/session-lifecycle-service.js +242 -0
  123. package/dist/service/session-message-service.d.ts +33 -0
  124. package/dist/service/session-message-service.js +157 -0
  125. package/dist/service/session-presentation-service.d.ts +33 -0
  126. package/dist/service/session-presentation-service.js +36 -0
  127. package/dist/service/session-query-service.d.ts +31 -0
  128. package/dist/service/session-query-service.js +77 -0
  129. package/dist/service/terminal-relay-state.d.ts +6 -0
  130. package/dist/service/terminal-relay-state.js +3 -0
  131. package/dist/service/terminal-service.d.ts +12 -0
  132. package/dist/service/terminal-service.js +52 -0
  133. package/dist/service/workbench-event-service.d.ts +21 -0
  134. package/dist/service/workbench-event-service.js +53 -0
  135. package/dist/service/workbench-manifest-service.d.ts +89 -0
  136. package/dist/service/workbench-manifest-service.js +70 -0
  137. package/dist/service/workspace-change-service.d.ts +9 -0
  138. package/dist/service/workspace-change-service.js +86 -0
  139. package/dist/service/workspace-content-search-service.d.ts +14 -0
  140. package/dist/service/workspace-content-search-service.js +477 -0
  141. package/dist/service/workspace-domain-state.d.ts +75 -0
  142. package/dist/service/workspace-domain-state.js +40 -0
  143. package/dist/service/workspace-file-service.d.ts +17 -0
  144. package/dist/service/workspace-file-service.js +109 -0
  145. package/dist/service/workspace-queue-workbench-service.d.ts +55 -0
  146. package/dist/service/workspace-queue-workbench-service.js +226 -0
  147. package/dist/service/workspace-service.d.ts +15 -0
  148. package/dist/service/workspace-service.js +67 -0
  149. package/dist/service/workspace-session-service.d.ts +23 -0
  150. package/dist/service/workspace-session-service.js +139 -0
  151. package/dist/service/workspace-upload-service.d.ts +37 -0
  152. package/dist/service/workspace-upload-service.js +240 -0
  153. package/dist/service/workspace-watch-service.d.ts +18 -0
  154. package/dist/service/workspace-watch-service.js +120 -0
  155. package/dist/service/workspace-workbench-service.d.ts +47 -0
  156. package/dist/service/workspace-workbench-service.js +223 -0
  157. package/dist/service.js +2 -3
  158. package/dist/storage.js +0 -1
  159. package/dist/supervisor.d.ts +1 -0
  160. package/dist/supervisor.js +154 -0
  161. package/dist/terminal.d.ts +1 -0
  162. package/dist/terminal.js +1 -2
  163. package/dist/util/node-operation-parsers.d.ts +92 -0
  164. package/dist/util/node-operation-parsers.js +382 -0
  165. package/dist/util/runner-native-session-parsers.d.ts +72 -0
  166. package/dist/util/runner-native-session-parsers.js +381 -0
  167. package/dist/util/secret-environment.d.ts +1 -0
  168. package/dist/util/secret-environment.js +25 -0
  169. package/dist/workspace.d.ts +62 -2
  170. package/dist/workspace.js +539 -57
  171. package/package.json +3 -2
  172. package/dist/capabilities.js.map +0 -1
  173. package/dist/claude-agent-sdk.js.map +0 -1
  174. package/dist/claude-channel.js.map +0 -1
  175. package/dist/codex-app-server.js.map +0 -1
  176. package/dist/config.js.map +0 -1
  177. package/dist/connector.js.map +0 -1
  178. package/dist/database.js.map +0 -1
  179. package/dist/event-buffer.js.map +0 -1
  180. package/dist/fake-runner.js.map +0 -1
  181. package/dist/health.js.map +0 -1
  182. package/dist/main.js.map +0 -1
  183. package/dist/migrations/v001.js.map +0 -1
  184. package/dist/migrations/v002.js.map +0 -1
  185. package/dist/native-session-history.js.map +0 -1
  186. package/dist/operational.js.map +0 -1
  187. package/dist/process-tree.js.map +0 -1
  188. package/dist/runner-command-engine.js.map +0 -1
  189. package/dist/runner-profiles.js.map +0 -1
  190. package/dist/runner-usage.js.map +0 -1
  191. package/dist/runtime-state.js.map +0 -1
  192. package/dist/service.js.map +0 -1
  193. package/dist/storage.js.map +0 -1
  194. package/dist/terminal.js.map +0 -1
  195. package/dist/workspace.js.map +0 -1
@@ -0,0 +1,137 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
4
+ export class NodeUpgradeCoordinator {
5
+ requestPath;
6
+ activeRunCount;
7
+ onStatus;
8
+ onReady;
9
+ now;
10
+ status = { state: 'IDLE', updatedAt: Date.now() };
11
+ timer;
12
+ constructor(requestPath, activeRunCount, onStatus, onReady, now = Date.now) {
13
+ this.requestPath = requestPath;
14
+ this.activeRunCount = activeRunCount;
15
+ this.onStatus = onStatus;
16
+ this.onReady = onReady;
17
+ this.now = now;
18
+ }
19
+ snapshot() {
20
+ return this.status;
21
+ }
22
+ async loadResult(resultPath) {
23
+ try {
24
+ const result = JSON.parse(await readFile(resultPath, 'utf8'));
25
+ if (result['state'] === 'FAILED' && typeof result['errorCode'] === 'string')
26
+ this.update({
27
+ state: 'FAILED',
28
+ errorCode: result['errorCode'],
29
+ updatedAt: typeof result['updatedAt'] === 'number' ? result['updatedAt'] : this.now()
30
+ });
31
+ }
32
+ catch {
33
+ // Missing or malformed historical result does not prevent Node startup.
34
+ }
35
+ }
36
+ request(data) {
37
+ const targetVersion = record(data).targetVersion;
38
+ if (typeof targetVersion !== 'string' || !STABLE_VERSION.test(targetVersion))
39
+ throw new Error('NODE_UPGRADE_VERSION_INVALID');
40
+ if (this.status.state !== 'IDLE' && this.status.state !== 'FAILED') {
41
+ if (this.status.targetVersion !== targetVersion)
42
+ throw new Error('NODE_UPGRADE_ALREADY_PENDING');
43
+ return { status: 'ALREADY_PENDING', targetVersion, activeRunCount: this.activeRunCount() };
44
+ }
45
+ this.update({
46
+ state: 'WAITING_FOR_RUNS',
47
+ targetVersion,
48
+ activeRunCount: this.activeRunCount(),
49
+ updatedAt: this.now()
50
+ });
51
+ this.timer = setInterval(() => void this.checkReadySafely(), 250);
52
+ void this.checkReadySafely();
53
+ return { status: 'ACCEPTED', targetVersion, activeRunCount: this.activeRunCount() };
54
+ }
55
+ cancel() {
56
+ if (this.status.state !== 'WAITING_FOR_RUNS')
57
+ throw new Error('NODE_UPGRADE_NOT_CANCELLABLE');
58
+ this.clearTimer();
59
+ this.update({ state: 'IDLE', updatedAt: this.now() });
60
+ return { cancelled: true };
61
+ }
62
+ blocks(operation) {
63
+ return (this.pending() &&
64
+ (operation === 'session.start' ||
65
+ operation === 'session.resume' ||
66
+ operation === 'session.message' ||
67
+ operation === 'session.command.execute' ||
68
+ operation === 'runner.command.execute' ||
69
+ operation === 'session.channel.message' ||
70
+ operation === 'workspace.queue.message.execute' ||
71
+ operation === 'terminal.open'));
72
+ }
73
+ pending() {
74
+ return this.status.state === 'WAITING_FOR_RUNS' || this.status.state === 'READY_TO_RESTART';
75
+ }
76
+ stop() {
77
+ this.clearTimer();
78
+ }
79
+ async checkReady() {
80
+ if (this.status.state !== 'WAITING_FOR_RUNS')
81
+ return;
82
+ const activeRunCount = this.activeRunCount();
83
+ if (activeRunCount > 0) {
84
+ if (this.status.activeRunCount !== activeRunCount)
85
+ this.update({ ...this.status, activeRunCount, updatedAt: this.now() });
86
+ return;
87
+ }
88
+ this.clearTimer();
89
+ const targetVersion = this.status.targetVersion;
90
+ this.update({
91
+ state: 'READY_TO_RESTART',
92
+ targetVersion,
93
+ activeRunCount: 0,
94
+ updatedAt: this.now()
95
+ });
96
+ const path = this.requestPath();
97
+ const temporary = `${path}.${process.pid}.tmp`;
98
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
99
+ await writeFile(temporary, `${JSON.stringify({ schemaVersion: 1, targetVersion, createdAt: this.now() })}\n`, { mode: 0o600 });
100
+ await rename(temporary, path).catch(async (error) => {
101
+ await rm(temporary, { force: true });
102
+ throw error;
103
+ });
104
+ this.onReady();
105
+ }
106
+ async checkReadySafely() {
107
+ try {
108
+ await this.checkReady();
109
+ }
110
+ catch {
111
+ this.clearTimer();
112
+ this.update({
113
+ state: 'FAILED',
114
+ targetVersion: this.status.targetVersion,
115
+ errorCode: 'NODE_UPGRADE_REQUEST_WRITE_FAILED',
116
+ updatedAt: this.now()
117
+ });
118
+ }
119
+ }
120
+ update(status) {
121
+ this.status = status;
122
+ this.onStatus(status);
123
+ }
124
+ clearTimer() {
125
+ if (this.timer !== undefined)
126
+ clearInterval(this.timer);
127
+ this.timer = undefined;
128
+ }
129
+ }
130
+ export function nodeUpgradeRequestPath(databasePath) {
131
+ return resolve(dirname(databasePath), 'upgrade-request.json');
132
+ }
133
+ function record(value) {
134
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
135
+ throw new Error('NODE_UPGRADE_REQUEST_INVALID');
136
+ return value;
137
+ }
@@ -0,0 +1,32 @@
1
+ import type { NodeEnvelope } from '@myagentroam/protocol';
2
+ import type { NodeConfig } from '../config.js';
3
+ import type { NodeAgentSession, NodeDatabase, NodeWorkspace } from '../database.js';
4
+ import type { NodeRuntimeState } from '../runtime-state.js';
5
+ import type { TerminalSummary } from '../terminal.js';
6
+ import type { WorkspaceChangeService } from './workspace-change-service.js';
7
+ import type { WorkspaceSessionService } from './workspace-session-service.js';
8
+ import type { WorkspaceWatchService } from './workspace-watch-service.js';
9
+ interface NodeWorkspaceCoordinatorOptions {
10
+ readonly config: () => NodeConfig | undefined;
11
+ readonly database: () => NodeDatabase | undefined;
12
+ readonly runtime: NodeRuntimeState;
13
+ readonly watches: WorkspaceWatchService;
14
+ readonly sessions: WorkspaceSessionService;
15
+ readonly changes: WorkspaceChangeService;
16
+ readonly emit: (workspaceId: string, topic: 'files' | 'changes', revision: number) => void;
17
+ readonly send: (type: string, payload: unknown, replyTo?: string) => void;
18
+ }
19
+ /** Coordinates Workspace invalidation, inspection, snapshots and persisted Terminal summaries. */
20
+ export declare class NodeWorkspaceCoordinator {
21
+ private readonly options;
22
+ constructor(options: NodeWorkspaceCoordinatorOptions);
23
+ require(workspaceId: unknown): NodeWorkspace;
24
+ clearWatches(workspaceId: string): void;
25
+ cachedSessions(workspaceId: string): Promise<readonly NodeAgentSession[]>;
26
+ invalidate(workspaceId: string): void;
27
+ invalidateChanges(workspaceId: string): void;
28
+ persistTerminal(summary: TerminalSummary): void;
29
+ inspect(envelope: NodeEnvelope): Promise<void>;
30
+ captureRunSnapshot(runId: string, cwd: string): Promise<void>;
31
+ }
32
+ export {};
@@ -0,0 +1,80 @@
1
+ import { inspectWorkspace, readGitSummary } from '../workspace.js';
2
+ /** Coordinates Workspace invalidation, inspection, snapshots and persisted Terminal summaries. */
3
+ export class NodeWorkspaceCoordinator {
4
+ options;
5
+ constructor(options) {
6
+ this.options = options;
7
+ }
8
+ require(workspaceId) {
9
+ if (typeof workspaceId !== 'string')
10
+ throw new Error('WORKSPACE_NOT_FOUND');
11
+ const workspace = this.options.database()?.getWorkspace(workspaceId);
12
+ if (workspace === undefined)
13
+ throw new Error('WORKSPACE_NOT_FOUND');
14
+ return workspace;
15
+ }
16
+ clearWatches(workspaceId) {
17
+ this.options.watches.clear(workspaceId);
18
+ }
19
+ cachedSessions(workspaceId) {
20
+ return this.options.sessions.cached(workspaceId);
21
+ }
22
+ invalidate(workspaceId) {
23
+ this.options.changes.invalidate(workspaceId);
24
+ for (const topic of ['files', 'changes']) {
25
+ if (!this.options.watches.topicActive(workspaceId, topic))
26
+ continue;
27
+ this.options.emit(workspaceId, topic, this.options.watches.nextRevision(workspaceId));
28
+ }
29
+ }
30
+ invalidateChanges(workspaceId) {
31
+ this.options.changes.invalidate(workspaceId);
32
+ }
33
+ persistTerminal(summary) {
34
+ const database = this.options.database();
35
+ if (database === undefined)
36
+ return;
37
+ const value = {
38
+ id: summary.id,
39
+ workspaceId: summary.workspaceId,
40
+ profileId: summary.profileId,
41
+ state: summary.state,
42
+ exitCode: summary.exitCode,
43
+ rows: summary.rows,
44
+ cols: summary.cols,
45
+ latestSequence: summary.latestSequence,
46
+ createdAt: summary.createdAt,
47
+ updatedAt: summary.updatedAt
48
+ };
49
+ database.saveTerminalSession(value);
50
+ }
51
+ async inspect(envelope) {
52
+ const requestedPath = typeof envelope.payload === 'object' &&
53
+ envelope.payload !== null &&
54
+ 'path' in envelope.payload
55
+ ? envelope.payload.path
56
+ : undefined;
57
+ const config = this.options.config();
58
+ if (typeof requestedPath !== 'string' || config === undefined) {
59
+ this.options.send('workspace.inspect.result', { error: 'WORKSPACE_INVALID' }, envelope.id);
60
+ return;
61
+ }
62
+ try {
63
+ const inspection = await inspectWorkspace(requestedPath, config.allowedRoots);
64
+ this.options.send('workspace.inspect.result', inspection, envelope.id);
65
+ }
66
+ catch {
67
+ this.options.send('workspace.inspect.result', { error: 'WORKSPACE_INVALID' }, envelope.id);
68
+ }
69
+ }
70
+ async captureRunSnapshot(runId, cwd) {
71
+ try {
72
+ const snapshot = await readGitSummary(cwd);
73
+ this.options.runtime.saveSnapshot(runId, snapshot);
74
+ this.options.send('workspace.snapshot', { runId, snapshot });
75
+ }
76
+ catch {
77
+ // Ordinary directories have no Git snapshot and keep the existing Run result.
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,37 @@
1
+ import type { CodexComposerInput } from '../codex-app-server.js';
2
+ import type { NodeAgentSession, NodeMessageAttachmentInput } from '../database.js';
3
+ import type { NativeTranscriptImageAttachment } from '../native-session-history.js';
4
+ import { AttachmentDomainState } from './attachment-domain-state.js';
5
+ export interface CachedRunImages {
6
+ images: readonly NodeMessageAttachmentInput[];
7
+ expiresAt: number | undefined;
8
+ timer: NodeJS.Timeout | undefined;
9
+ }
10
+ export interface NativeImageSource {
11
+ readonly name: string;
12
+ readonly mime: string;
13
+ readonly path: string;
14
+ }
15
+ export type RunnerImageAttachment = NodeMessageAttachmentInput & {
16
+ readonly mime: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
17
+ };
18
+ export declare class RunAttachmentService {
19
+ private readonly nativeImageRoot;
20
+ readonly state: AttachmentDomainState<CachedRunImages, NativeImageSource>;
21
+ constructor(nativeImageRoot: (session: NodeAgentSession) => string | undefined);
22
+ materializeCodex(runId: string, attachments: readonly RunnerImageAttachment[]): Promise<readonly Extract<CodexComposerInput, {
23
+ readonly type: 'localImage';
24
+ }>[]>;
25
+ persistFiles(workspacePath: string, runId: string, attachments: readonly NodeMessageAttachmentInput[]): Promise<readonly string[]>;
26
+ cleanup(runId: string): void;
27
+ cache(runId: string, images: readonly NodeMessageAttachmentInput[]): void;
28
+ append(runId: string, images: readonly NodeMessageAttachmentInput[]): readonly Record<string, unknown>[];
29
+ read(runId: string, imageIndex: number): NodeMessageAttachmentInput | undefined;
30
+ registerNative(session: NodeAgentSession, images: readonly NativeTranscriptImageAttachment[]): readonly Record<string, unknown>[];
31
+ readNative(runId: string, imageIndex: number): Promise<(NativeImageSource & {
32
+ readonly dataBase64: string;
33
+ }) | undefined>;
34
+ expire(runId: string): void;
35
+ clear(runId: string): void;
36
+ dispose(): void;
37
+ }
@@ -0,0 +1,160 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { AttachmentDomainState } from './attachment-domain-state.js';
5
+ const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
6
+ const IMAGE_CACHE_TTL_MS = 60 * 60_000;
7
+ export class RunAttachmentService {
8
+ nativeImageRoot;
9
+ state = new AttachmentDomainState();
10
+ constructor(nativeImageRoot) {
11
+ this.nativeImageRoot = nativeImageRoot;
12
+ }
13
+ async materializeCodex(runId, attachments) {
14
+ if (attachments.length === 0)
15
+ return [];
16
+ const directory = await mkdtemp(join(tmpdir(), 'myagentroam-attachment-'));
17
+ this.state.runDirectories.set(runId, directory);
18
+ try {
19
+ return await Promise.all(attachments.map(async (attachment, index) => {
20
+ const extension = attachment.mime.slice('image/'.length).replace('jpeg', 'jpg');
21
+ const path = join(directory, `${index + 1}.${extension}`);
22
+ await writeFile(path, Buffer.from(attachment.dataBase64, 'base64'), { mode: 0o600 });
23
+ return { type: 'localImage', path };
24
+ }));
25
+ }
26
+ catch (error) {
27
+ this.cleanup(runId);
28
+ throw error;
29
+ }
30
+ }
31
+ async persistFiles(workspacePath, runId, attachments) {
32
+ if (attachments.length === 0)
33
+ return [];
34
+ const marDirectory = resolve(workspacePath, '.mar');
35
+ await mkdir(marDirectory, { recursive: true, mode: 0o700 });
36
+ const marMetadata = await lstat(marDirectory);
37
+ if (!marMetadata.isDirectory() || marMetadata.isSymbolicLink())
38
+ throw new Error('MESSAGE_ATTACHMENT_DIRECTORY_INVALID');
39
+ const filesDirectory = join(marDirectory, 'temp-files');
40
+ await mkdir(filesDirectory, { recursive: true, mode: 0o700 });
41
+ const filesMetadata = await lstat(filesDirectory);
42
+ if (!filesMetadata.isDirectory() || filesMetadata.isSymbolicLink())
43
+ throw new Error('MESSAGE_ATTACHMENT_DIRECTORY_INVALID');
44
+ const runDirectory = join(filesDirectory, runId);
45
+ await mkdir(runDirectory, { mode: 0o700 });
46
+ try {
47
+ await Promise.all(attachments.map((attachment) => writeFile(join(runDirectory, attachment.name), Buffer.from(attachment.dataBase64, 'base64'), { flag: 'wx', mode: 0o600 })));
48
+ return attachments.map((attachment) => `.mar/temp-files/${runId}/${attachment.name}`);
49
+ }
50
+ catch (error) {
51
+ await rm(runDirectory, { recursive: true, force: true });
52
+ throw error;
53
+ }
54
+ }
55
+ cleanup(runId) {
56
+ const directory = this.state.runDirectories.get(runId);
57
+ if (directory === undefined)
58
+ return;
59
+ this.state.runDirectories.delete(runId);
60
+ void rm(directory, { recursive: true, force: true }).catch(() => undefined);
61
+ }
62
+ cache(runId, images) {
63
+ if (images.length === 0)
64
+ return;
65
+ this.clear(runId);
66
+ this.state.runImages.set(runId, { images, expiresAt: undefined, timer: undefined });
67
+ }
68
+ append(runId, images) {
69
+ if (images.length === 0)
70
+ return [];
71
+ const cached = this.state.runImages.get(runId);
72
+ const existing = cached?.images ?? [];
73
+ const imageIndex = existing.length;
74
+ const next = [...existing, ...images];
75
+ if (cached === undefined)
76
+ this.state.runImages.set(runId, { images: next, expiresAt: undefined, timer: undefined });
77
+ else
78
+ cached.images = next;
79
+ return images.map((image, index) => ({
80
+ name: image.name,
81
+ mime: image.mime,
82
+ byteSize: Buffer.byteLength(image.dataBase64, 'base64'),
83
+ runId,
84
+ imageIndex: imageIndex + index
85
+ }));
86
+ }
87
+ read(runId, imageIndex) {
88
+ const cached = this.state.runImages.get(runId);
89
+ if (cached === undefined)
90
+ return undefined;
91
+ if (cached.expiresAt !== undefined && cached.expiresAt <= Date.now()) {
92
+ this.clear(runId);
93
+ return undefined;
94
+ }
95
+ return cached.images[imageIndex];
96
+ }
97
+ registerNative(session, images) {
98
+ const root = this.nativeImageRoot(session);
99
+ if (root === undefined)
100
+ return [];
101
+ const runId = `native-history:${session.id}`;
102
+ const sources = this.state.nativeHistoryImages.get(runId) ?? [];
103
+ const attachments = [];
104
+ for (const image of images) {
105
+ if (image.localPath === undefined || !image.mime.startsWith('image/')) {
106
+ attachments.push({ name: image.name, mime: image.mime });
107
+ continue;
108
+ }
109
+ const path = resolve(image.localPath);
110
+ const relation = relative(root, path);
111
+ if (relation.length === 0 || relation.startsWith('..') || isAbsolute(relation)) {
112
+ attachments.push({ name: image.name, mime: image.mime });
113
+ continue;
114
+ }
115
+ let imageIndex = sources.findIndex((source) => source.path === path);
116
+ if (imageIndex < 0) {
117
+ imageIndex = sources.length;
118
+ sources.push({ name: image.name, mime: image.mime, path });
119
+ }
120
+ attachments.push({ name: image.name, mime: image.mime, runId, imageIndex });
121
+ }
122
+ if (sources.length > 0)
123
+ this.state.nativeHistoryImages.set(runId, sources);
124
+ return attachments;
125
+ }
126
+ async readNative(runId, imageIndex) {
127
+ const source = this.state.nativeHistoryImages.get(runId)?.[imageIndex];
128
+ if (source === undefined)
129
+ return undefined;
130
+ try {
131
+ const info = await lstat(source.path);
132
+ if (!info.isFile() || info.size <= 0 || info.size > MAX_ATTACHMENT_BYTES)
133
+ return undefined;
134
+ return { ...source, dataBase64: (await readFile(source.path)).toString('base64') };
135
+ }
136
+ catch {
137
+ return undefined;
138
+ }
139
+ }
140
+ expire(runId) {
141
+ const cached = this.state.runImages.get(runId);
142
+ if (cached === undefined || cached.expiresAt !== undefined)
143
+ return;
144
+ cached.expiresAt = Date.now() + IMAGE_CACHE_TTL_MS;
145
+ cached.timer = setTimeout(() => this.clear(runId), IMAGE_CACHE_TTL_MS);
146
+ }
147
+ clear(runId) {
148
+ const cached = this.state.runImages.get(runId);
149
+ if (cached?.timer !== undefined)
150
+ clearTimeout(cached.timer);
151
+ this.state.runImages.delete(runId);
152
+ }
153
+ dispose() {
154
+ for (const runId of this.state.runDirectories.keys())
155
+ this.cleanup(runId);
156
+ for (const runId of this.state.runImages.keys())
157
+ this.clear(runId);
158
+ this.state.nativeHistoryImages.clear();
159
+ }
160
+ }
@@ -0,0 +1,4 @@
1
+ export declare class RunDomainState {
2
+ readonly active: Set<string>;
3
+ readonly interruptRequested: Set<string>;
4
+ }
@@ -0,0 +1,4 @@
1
+ export class RunDomainState {
2
+ active = new Set();
3
+ interruptRequested = new Set();
4
+ }
@@ -0,0 +1,31 @@
1
+ import type { WorkbenchEventCategory } from '@myagentroam/protocol';
2
+ import type { NodeAgentSession } from '../database.js';
3
+ import type { FakeRunnerStatus } from '../fake-runner.js';
4
+ import { NodeMetrics } from '../operational.js';
5
+ import { NodeRuntimeState } from '../runtime-state.js';
6
+ import { RunDomainState } from './run-domain-state.js';
7
+ export interface RunEventServiceOptions {
8
+ readonly runtime: NodeRuntimeState;
9
+ readonly metrics: NodeMetrics;
10
+ readonly state: RunDomainState;
11
+ readonly isBootstrapSession: (sessionId: string) => boolean;
12
+ readonly presentSession: (session: NodeAgentSession) => unknown;
13
+ readonly rejectBootstrap: (sessionId: string, code: string) => void;
14
+ readonly emitWorkbench: (category: WorkbenchEventCategory, payload: unknown) => void;
15
+ readonly cleanupAttachments: (runId: string) => void;
16
+ readonly expireImages: (runId: string) => void;
17
+ readonly scheduleContextRefresh: (sessionId: string) => void;
18
+ readonly dispatchQueue: (sessionId: string) => Promise<void>;
19
+ readonly removeQueuedStart: (runId: string) => void;
20
+ }
21
+ export declare class RunEventService {
22
+ private readonly options;
23
+ private readonly sequences;
24
+ private readonly pendingText;
25
+ constructor(options: RunEventServiceOptions);
26
+ emit(runId: string, eventType: string, payload: unknown, status?: FakeRunnerStatus): void;
27
+ clear(): void;
28
+ private enqueueText;
29
+ private flushText;
30
+ private emitNow;
31
+ }
@@ -0,0 +1,102 @@
1
+ import { nodeLog } from '../operational.js';
2
+ import { isPlainRecord, isTerminalRunStatus } from '../util/node-operation-parsers.js';
3
+ const COALESCE_MS = 200;
4
+ const MAX_BATCH_CHARS = 16_384;
5
+ export class RunEventService {
6
+ options;
7
+ sequences = new Map();
8
+ pendingText = new Map();
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ emit(runId, eventType, payload, status) {
13
+ if (eventType === 'text.delta' &&
14
+ status === undefined &&
15
+ isPlainRecord(payload) &&
16
+ typeof payload.text === 'string') {
17
+ this.enqueueText(runId, payload.text);
18
+ return;
19
+ }
20
+ this.flushText(runId);
21
+ this.emitNow(runId, eventType, payload, status);
22
+ }
23
+ clear() {
24
+ for (const pending of this.pendingText.values())
25
+ clearTimeout(pending.timer);
26
+ this.pendingText.clear();
27
+ }
28
+ enqueueText(runId, text) {
29
+ if (text.length === 0)
30
+ return;
31
+ const pending = this.pendingText.get(runId);
32
+ if (pending !== undefined) {
33
+ pending.text += text;
34
+ if (pending.text.length >= MAX_BATCH_CHARS)
35
+ this.flushText(runId);
36
+ return;
37
+ }
38
+ const timer = setTimeout(() => this.flushText(runId), COALESCE_MS);
39
+ this.pendingText.set(runId, { text, timer });
40
+ }
41
+ flushText(runId) {
42
+ const pending = this.pendingText.get(runId);
43
+ if (pending === undefined)
44
+ return;
45
+ clearTimeout(pending.timer);
46
+ this.pendingText.delete(runId);
47
+ this.emitNow(runId, 'text.delta', { text: pending.text });
48
+ }
49
+ emitNow(runId, eventType, payload, status) {
50
+ const event = {
51
+ runId,
52
+ sequence: (this.sequences.get(runId) ?? 0) + 1,
53
+ eventType,
54
+ payload,
55
+ ...(status === undefined ? {} : { status })
56
+ };
57
+ this.sequences.set(runId, event.sequence);
58
+ if (eventType !== 'text.delta')
59
+ nodeLog('run.event', { runId, eventType, status });
60
+ if (status === 'FAILED')
61
+ this.options.metrics.runnerFailed();
62
+ try {
63
+ this.options.runtime.appendRunEvent({
64
+ runId,
65
+ sequence: event.sequence,
66
+ eventType,
67
+ payload,
68
+ ...(status === undefined ? {} : { status })
69
+ });
70
+ }
71
+ catch {
72
+ /* A stale relayed run may race the storage boundary. */
73
+ }
74
+ if (status !== undefined && isTerminalRunStatus(status)) {
75
+ this.options.state.interruptRequested.delete(runId);
76
+ const terminalRun = this.options.runtime.getRun(runId);
77
+ if (terminalRun !== undefined && this.options.isBootstrapSession(terminalRun.sessionId))
78
+ this.options.rejectBootstrap(terminalRun.sessionId, 'SESSION_START_FAILED');
79
+ }
80
+ if (status !== undefined) {
81
+ const run = this.options.runtime.getRun(runId);
82
+ this.options.emitWorkbench('run', { run });
83
+ const session = run === undefined ? undefined : this.options.runtime.getAgentSession(run.sessionId);
84
+ if (session !== undefined)
85
+ this.options.emitWorkbench('session', { session: this.options.presentSession(session) });
86
+ }
87
+ this.options.emitWorkbench('conversation', { event });
88
+ if (status === undefined || !isTerminalRunStatus(status))
89
+ return;
90
+ const turn = this.options.runtime.conversationTurnForRun(runId);
91
+ if (turn !== undefined)
92
+ this.options.emitWorkbench('conversation', { turn });
93
+ this.options.cleanupAttachments(runId);
94
+ this.options.expireImages(runId);
95
+ this.options.removeQueuedStart(runId);
96
+ const run = this.options.runtime.getRun(runId);
97
+ if (run !== undefined) {
98
+ this.options.scheduleContextRefresh(run.sessionId);
99
+ void this.options.dispatchQueue(run.sessionId);
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,27 @@
1
+ import type { NodeOperationHandler } from '../connector/node-operation-router.js';
2
+ import type { NodeRuntimeState } from '../runtime-state.js';
3
+ export interface RunWorkbenchServiceOptions {
4
+ readonly runtime: NodeRuntimeState;
5
+ readonly readImage: (runId: string, imageIndex: number) => Promise<unknown | undefined>;
6
+ readonly respondUserInput: (runId: string, requestId: string, answers: Record<string, unknown>) => void;
7
+ readonly deleteQueuedStart: (runId: string) => void;
8
+ readonly clearImages: (runId: string) => void;
9
+ readonly emitQueue: (workspaceId: string, sessionId: string) => void;
10
+ readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'CANCELLED') => void;
11
+ readonly control: (operation: string, payload: Record<string, unknown>) => void;
12
+ readonly markInterrupted: (runId: string) => void;
13
+ }
14
+ export declare class RunWorkbenchService {
15
+ private readonly options;
16
+ constructor(options: RunWorkbenchServiceOptions);
17
+ operations(): Readonly<Record<string, NodeOperationHandler>>;
18
+ private get;
19
+ private snapshot;
20
+ private image;
21
+ private respond;
22
+ private interruptSession;
23
+ private cancelOrInterrupt;
24
+ private approvals;
25
+ private decideApproval;
26
+ private requireRun;
27
+ }