@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,109 @@
1
+ import { isAbsolute } from 'node:path';
2
+ import { listWorkspaceFiles, mutateWorkspaceFile, readAllowedTextFile, readWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
3
+ const INDEX_CACHE_LIMIT = 8;
4
+ export class WorkspaceFileService {
5
+ state;
6
+ allowedRoots;
7
+ onInvalidated;
8
+ mutationTails = new Map();
9
+ constructor(state, allowedRoots, onInvalidated) {
10
+ this.state = state;
11
+ this.allowedRoots = allowedRoots;
12
+ this.onInvalidated = onInvalidated;
13
+ }
14
+ async list(workspace, input) {
15
+ if ((input.path !== undefined && typeof input.path !== 'string') ||
16
+ (input.cursor !== undefined && typeof input.cursor !== 'string') ||
17
+ (input.limit !== undefined &&
18
+ (!Number.isInteger(input.limit) ||
19
+ input.limit < 1 ||
20
+ input.limit > 200)))
21
+ throw new Error('DIRECTORY_CURSOR_INVALID');
22
+ return listWorkspaceFiles(workspace.path, typeof input.path === 'string' ? input.path : '.', typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined);
23
+ }
24
+ async read(workspace, input) {
25
+ if (typeof input.path !== 'string' ||
26
+ (input.offset !== undefined &&
27
+ (!Number.isInteger(input.offset) || input.offset < 0)) ||
28
+ (input.limit !== undefined &&
29
+ (!Number.isInteger(input.limit) ||
30
+ input.limit < 1 ||
31
+ input.limit > 512 * 1024)))
32
+ throw new Error('FILE_RANGE_INVALID');
33
+ const offset = typeof input.offset === 'number' ? input.offset : 0;
34
+ const limit = typeof input.limit === 'number' ? input.limit : 512 * 1024;
35
+ return isAbsolute(input.path)
36
+ ? readAllowedTextFile(input.path, this.allowedRoots(), offset, limit)
37
+ : readWorkspaceTextFile(workspace.path, input.path, offset, limit);
38
+ }
39
+ async search(workspace, input) {
40
+ if (typeof input.query !== 'string' ||
41
+ (input.cursor !== undefined && typeof input.cursor !== 'string') ||
42
+ (input.includeDirectories !== undefined && typeof input.includeDirectories !== 'boolean') ||
43
+ (input.limit !== undefined &&
44
+ (!Number.isInteger(input.limit) ||
45
+ input.limit < 1 ||
46
+ input.limit > 200)))
47
+ throw new Error('FILE_SEARCH_LIMIT_EXCEEDED');
48
+ return this.index(workspace).search(input.query, typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined, input.includeDirectories === true);
49
+ }
50
+ async mutate(workspace, input) {
51
+ const action = input.action;
52
+ if ((action !== 'CREATE_FILE' &&
53
+ action !== 'CREATE_DIRECTORY' &&
54
+ action !== 'MOVE' &&
55
+ action !== 'DELETE') ||
56
+ typeof input.path !== 'string' ||
57
+ (action === 'MOVE' && typeof input.destination !== 'string'))
58
+ throw new Error('FILE_MUTATION_INVALID');
59
+ const path = input.path;
60
+ const destination = typeof input.destination === 'string' ? input.destination : undefined;
61
+ return this.serializeMutation(workspace.id, async () => {
62
+ const result = await mutateWorkspaceFile(workspace.path, action === 'MOVE' ? { action, path, destination: destination } : { action, path });
63
+ this.dispose(workspace.id);
64
+ this.onInvalidated(workspace.id);
65
+ return result;
66
+ });
67
+ }
68
+ async serializeMutation(workspaceId, operation) {
69
+ const previous = this.mutationTails.get(workspaceId) ?? Promise.resolve();
70
+ const result = previous.then(operation, operation);
71
+ const tail = result.then(() => undefined, () => undefined);
72
+ this.mutationTails.set(workspaceId, tail);
73
+ try {
74
+ return await result;
75
+ }
76
+ finally {
77
+ if (this.mutationTails.get(workspaceId) === tail)
78
+ this.mutationTails.delete(workspaceId);
79
+ }
80
+ }
81
+ index(workspace) {
82
+ let index = this.state.fileIndexes.get(workspace.id);
83
+ if (index === undefined) {
84
+ if (this.state.fileIndexes.size >= INDEX_CACHE_LIMIT) {
85
+ const oldest = this.state.fileIndexes.entries().next().value;
86
+ if (oldest !== undefined) {
87
+ oldest[1].dispose();
88
+ this.state.fileIndexes.delete(oldest[0]);
89
+ }
90
+ }
91
+ index = new WorkspaceFileIndex(workspace.path, {
92
+ onInvalidated: () => this.onInvalidated(workspace.id)
93
+ });
94
+ }
95
+ this.state.fileIndexes.delete(workspace.id);
96
+ this.state.fileIndexes.set(workspace.id, index);
97
+ return index;
98
+ }
99
+ dispose(workspaceId) {
100
+ if (workspaceId !== undefined) {
101
+ this.state.fileIndexes.get(workspaceId)?.dispose();
102
+ this.state.fileIndexes.delete(workspaceId);
103
+ return;
104
+ }
105
+ for (const index of this.state.fileIndexes.values())
106
+ index.dispose();
107
+ this.state.fileIndexes.clear();
108
+ }
109
+ }
@@ -0,0 +1,55 @@
1
+ import type { NodeOperationHandler } from '../connector/node-operation-router.js';
2
+ import type { NodeRuntimeState } from '../runtime-state.js';
3
+ import type { RunnerName } from '@myagentroam/protocol';
4
+ export interface QueuedRunStart<TAttachment = unknown> {
5
+ readonly runId: string;
6
+ readonly sessionId: string;
7
+ readonly workspaceId: string;
8
+ readonly runner: RunnerName;
9
+ readonly input: string;
10
+ readonly attachments: readonly TAttachment[];
11
+ readonly attachmentPaths: readonly string[];
12
+ readonly cwd: string;
13
+ readonly externalSessionId: string | null;
14
+ readonly model: string | null;
15
+ readonly effort: string | null;
16
+ readonly access: string | null;
17
+ readonly collaborationMode: 'default' | 'plan';
18
+ readonly serviceTier?: 'fast';
19
+ readonly secretEnvironment: Readonly<Record<string, string>>;
20
+ }
21
+ export interface WorkspaceQueueWorkbenchServiceOptions {
22
+ readonly runtime: NodeRuntimeState;
23
+ readonly emit: (workspaceId: string, sessionId: string) => void;
24
+ readonly clearImages: (runId: string) => void;
25
+ readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'FAILED' | 'CANCELLED') => void;
26
+ readonly emitConversation: (turn: unknown) => void;
27
+ readonly control: (operation: string, payload: Record<string, unknown>) => void;
28
+ readonly stopped: () => boolean;
29
+ readonly markInterrupted: (runId: string) => void;
30
+ readonly prepareInput: (runner: RunnerName, input: string, collaborationMode: 'default' | 'plan') => string;
31
+ }
32
+ export declare class WorkspaceQueueWorkbenchService<TAttachment> {
33
+ private readonly options;
34
+ private readonly starts;
35
+ constructor(options: WorkspaceQueueWorkbenchServiceOptions);
36
+ remember(start: QueuedRunStart<TAttachment>): void;
37
+ pending(runId: string): QueuedRunStart<TAttachment> | undefined;
38
+ remove(runId: string): void;
39
+ clear(): void;
40
+ migrateSession(previousId: string, nextId: string): void;
41
+ operations(): Readonly<Record<string, NodeOperationHandler>>;
42
+ private get;
43
+ private pauseOrResume;
44
+ private reorder;
45
+ private update;
46
+ private delete;
47
+ private execute;
48
+ private requireSession;
49
+ private requireRun;
50
+ replace(queuedRunId: string, activeRunId: string, prioritize?: boolean): void;
51
+ schedule(sessionId: string): void;
52
+ dispatch(sessionId: string): Promise<void>;
53
+ present(workspaceId: string, sessionId: string): unknown;
54
+ private updatePending;
55
+ }
@@ -0,0 +1,226 @@
1
+ import { isPlainRecord } from '../runner/codex/conversation-parser.js';
2
+ import { isTerminalRunStatus } from '../util/node-operation-parsers.js';
3
+ import { withNonImageAttachmentPrompt } from '../runner/codex/conversation-parser.js';
4
+ export class WorkspaceQueueWorkbenchService {
5
+ options;
6
+ starts = new Map();
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ remember(start) {
11
+ this.starts.set(start.runId, start);
12
+ }
13
+ pending(runId) {
14
+ return this.starts.get(runId);
15
+ }
16
+ remove(runId) {
17
+ this.starts.delete(runId);
18
+ }
19
+ clear() {
20
+ this.starts.clear();
21
+ }
22
+ migrateSession(previousId, nextId) {
23
+ for (const [runId, pending] of this.starts) {
24
+ if (pending.sessionId === previousId)
25
+ this.starts.set(runId, { ...pending, sessionId: nextId, externalSessionId: nextId });
26
+ }
27
+ }
28
+ operations() {
29
+ return {
30
+ 'workspace.queue.get': (data) => this.get(record(data)),
31
+ 'workspace.queue.resume': (data) => this.pauseOrResume('resume', record(data)),
32
+ 'workspace.queue.pause': (data) => this.pauseOrResume('pause', record(data)),
33
+ 'workspace.queue.reorder': (data) => this.reorder(record(data)),
34
+ 'workspace.queue.message.update': (data) => this.update(record(data)),
35
+ 'workspace.queue.message.delete': (data) => this.delete(record(data)),
36
+ 'workspace.queue.message.execute': (data) => this.execute(record(data))
37
+ };
38
+ }
39
+ get(input) {
40
+ if (typeof input.workspaceId !== 'string' || typeof input.sessionId !== 'string')
41
+ throw new Error('WORKSPACE_NOT_FOUND');
42
+ return { queue: this.present(input.workspaceId, input.sessionId) };
43
+ }
44
+ pauseOrResume(mode, input) {
45
+ const session = this.requireSession(input);
46
+ if (mode === 'resume') {
47
+ this.options.runtime.resumeSessionQueue(session.id);
48
+ this.schedule(session.id);
49
+ }
50
+ else
51
+ this.options.runtime.pauseSessionQueue(session.id);
52
+ this.options.emit(session.workspaceId, session.id);
53
+ return { queue: this.present(session.workspaceId, session.id) };
54
+ }
55
+ reorder(input) {
56
+ if (!Number.isInteger(input.expectedVersion) ||
57
+ !Array.isArray(input.runIds) ||
58
+ input.runIds.some((id) => typeof id !== 'string'))
59
+ throw new Error('QUEUE_INVALID');
60
+ const session = this.requireSession(input);
61
+ this.options.runtime.reorderSessionQueue(session.id, input.expectedVersion, input.runIds);
62
+ this.options.emit(session.workspaceId, session.id);
63
+ return { queue: this.present(session.workspaceId, session.id) };
64
+ }
65
+ update(input) {
66
+ if (typeof input.runId !== 'string' ||
67
+ typeof input.content !== 'string' ||
68
+ input.content.trim().length === 0)
69
+ throw new Error('QUEUE_INVALID');
70
+ const run = this.requireRun(input.runId);
71
+ this.updatePending(run.id, input.content.trim());
72
+ this.options.emit(run.workspaceId, run.sessionId);
73
+ return { queue: this.present(run.workspaceId, run.sessionId) };
74
+ }
75
+ delete(input) {
76
+ if (typeof input.runId !== 'string')
77
+ throw new Error('QUEUE_INVALID');
78
+ const run = this.requireRun(input.runId);
79
+ this.options.runtime.deleteQueuedRun(run.id);
80
+ this.remove(run.id);
81
+ this.options.clearImages(run.id);
82
+ this.options.emit(run.workspaceId, run.sessionId);
83
+ return { queue: this.present(run.workspaceId, run.sessionId) };
84
+ }
85
+ execute(input) {
86
+ if (typeof input.runId !== 'string')
87
+ throw new Error('QUEUE_INVALID');
88
+ const queued = this.requireRun(input.runId);
89
+ if (!this.options.runtime.isQueuedRun(queued.id) || queued.status !== 'QUEUED')
90
+ throw new Error('QUEUE_ITEM_INVALID');
91
+ if (!this.starts.has(queued.id))
92
+ throw new Error('QUEUE_INPUT_LOST');
93
+ const session = this.options.runtime.getAgentSession(queued.sessionId);
94
+ if (session === undefined)
95
+ throw new Error('SESSION_NOT_FOUND');
96
+ this.options.runtime.resumeSessionQueue(session.id);
97
+ const active = this.options.runtime
98
+ .listRunsForSession(session.id)
99
+ .find((run) => run.status === 'STARTING' || run.status === 'RUNNING');
100
+ this.options.runtime.prioritizeQueuedRun(queued.id);
101
+ this.options.emit(queued.workspaceId, session.id);
102
+ if (active !== undefined) {
103
+ this.replace(queued.id, active.id, false);
104
+ return { run: queued, delivery: 'RESTARTING' };
105
+ }
106
+ this.schedule(session.id);
107
+ return { run: queued, delivery: 'STARTING' };
108
+ }
109
+ requireSession(input) {
110
+ if (typeof input.workspaceId !== 'string' || typeof input.sessionId !== 'string')
111
+ throw new Error('WORKSPACE_NOT_FOUND');
112
+ const session = this.options.runtime.getAgentSession(input.sessionId);
113
+ if (session?.workspaceId !== input.workspaceId)
114
+ throw new Error('SESSION_NOT_FOUND');
115
+ return session;
116
+ }
117
+ requireRun(runId) {
118
+ const run = this.options.runtime.getRun(runId);
119
+ if (run === undefined)
120
+ throw new Error('RUN_NOT_FOUND');
121
+ return run;
122
+ }
123
+ replace(queuedRunId, activeRunId, prioritize = true) {
124
+ const queued = this.options.runtime.getRun(queuedRunId);
125
+ const active = this.options.runtime.getRun(activeRunId);
126
+ if (queued === undefined ||
127
+ active === undefined ||
128
+ queued.sessionId !== active.sessionId ||
129
+ !this.options.runtime.isQueuedRun(queued.id))
130
+ throw new Error('QUEUE_ITEM_INVALID');
131
+ if (prioritize)
132
+ this.options.runtime.prioritizeQueuedRun(queued.id);
133
+ this.options.markInterrupted(active.id);
134
+ this.options.runtime.transitionRun(active.id, 'CANCELLING');
135
+ this.options.control('run.interrupt', { runId: active.id });
136
+ }
137
+ schedule(sessionId) {
138
+ const timer = setTimeout(() => {
139
+ if (!this.options.stopped())
140
+ void this.dispatch(sessionId);
141
+ }, 0);
142
+ timer.unref();
143
+ }
144
+ async dispatch(sessionId) {
145
+ const run = this.options.runtime.claimNextQueuedRun(sessionId);
146
+ if (run === undefined)
147
+ return;
148
+ this.options.emit(run.workspaceId, sessionId);
149
+ const pending = this.starts.get(run.id);
150
+ if (pending === undefined) {
151
+ this.options.emitRun(run.id, 'run.rejected', { code: 'QUEUE_INPUT_LOST' }, 'FAILED');
152
+ return;
153
+ }
154
+ try {
155
+ const turn = this.options.runtime.conversationTurnForRun(run.id);
156
+ if (turn !== undefined)
157
+ this.options.emitConversation(turn);
158
+ const current = this.options.runtime.getRun(run.id);
159
+ if (current !== undefined && isTerminalRunStatus(current.status))
160
+ return;
161
+ if (current?.status === 'CANCELLING') {
162
+ this.options.emitRun(run.id, 'run.completed', { status: 'CANCELLED' }, 'CANCELLED');
163
+ return;
164
+ }
165
+ this.options.control('run.start', {
166
+ runId: pending.runId,
167
+ sessionId: pending.sessionId,
168
+ workspaceId: pending.workspaceId,
169
+ runner: pending.runner,
170
+ input: pending.input,
171
+ ...(pending.attachments.length === 0 ? {} : { attachments: pending.attachments }),
172
+ cwd: pending.cwd,
173
+ ...(pending.externalSessionId === null
174
+ ? {}
175
+ : { externalSessionId: pending.externalSessionId }),
176
+ ...(pending.model === null ? {} : { model: pending.model }),
177
+ ...(pending.effort === null ? {} : { effort: pending.effort }),
178
+ ...(pending.access === null ? {} : { access: pending.access }),
179
+ collaborationMode: pending.collaborationMode,
180
+ ...(pending.serviceTier === undefined ? {} : { serviceTier: pending.serviceTier }),
181
+ secretEnvironment: pending.secretEnvironment
182
+ });
183
+ }
184
+ catch (error) {
185
+ this.options.emitRun(run.id, 'run.rejected', { code: error instanceof Error ? error.message : 'QUEUE_START_FAILED' }, 'FAILED');
186
+ }
187
+ }
188
+ present(workspaceId, sessionId) {
189
+ const session = this.options.runtime.getAgentSession(sessionId);
190
+ if (session?.workspaceId !== workspaceId)
191
+ throw new Error('SESSION_NOT_FOUND');
192
+ const queue = this.options.runtime.sessionQueue(sessionId);
193
+ return {
194
+ version: queue.version,
195
+ paused: queue.paused,
196
+ items: queue.runIds.flatMap((runId) => {
197
+ const run = this.options.runtime.getRun(runId);
198
+ if (run === undefined)
199
+ return [];
200
+ const queuedSession = this.options.runtime.getAgentSession(run.sessionId);
201
+ return [
202
+ {
203
+ run,
204
+ sessionId: run.sessionId,
205
+ sessionTitle: queuedSession?.customTitle ?? queuedSession?.runnerTitle ?? '未命名会话',
206
+ content: this.options.runtime.queuedMessageText(run.id)
207
+ }
208
+ ];
209
+ })
210
+ };
211
+ }
212
+ updatePending(runId, content) {
213
+ const pending = this.starts.get(runId);
214
+ const runnerInput = withNonImageAttachmentPrompt(content, pending?.attachmentPaths ?? []);
215
+ this.options.runtime.updateQueuedMessage(runId, runnerInput);
216
+ if (pending === undefined)
217
+ return;
218
+ this.starts.set(runId, {
219
+ ...pending,
220
+ input: this.options.prepareInput(pending.runner, runnerInput, pending.collaborationMode)
221
+ });
222
+ }
223
+ }
224
+ function record(value) {
225
+ return isPlainRecord(value) ? value : {};
226
+ }
@@ -0,0 +1,15 @@
1
+ import type { NodeConfig } from '../config.js';
2
+ import type { NodeOperationHandler } from '../connector/node-operation-router.js';
3
+ import type { NodeDatabase } from '../database.js';
4
+ import type { NodeRuntimeState } from '../runtime-state.js';
5
+ export declare class WorkspaceService {
6
+ private readonly database;
7
+ private readonly config;
8
+ private readonly runtime;
9
+ private readonly onRemoved;
10
+ constructor(database: () => NodeDatabase, config: () => NodeConfig, runtime: NodeRuntimeState, onRemoved: (workspaceId: string) => void);
11
+ operations(): Readonly<Record<string, NodeOperationHandler>>;
12
+ private register;
13
+ private listDirectories;
14
+ private remove;
15
+ }
@@ -0,0 +1,67 @@
1
+ import { rmdir } from 'node:fs/promises';
2
+ import { createWorkspaceDirectory, inspectWorkspace, listWorkspaceDirectories } from '../workspace.js';
3
+ export class WorkspaceService {
4
+ database;
5
+ config;
6
+ runtime;
7
+ onRemoved;
8
+ constructor(database, config, runtime, onRemoved) {
9
+ this.database = database;
10
+ this.config = config;
11
+ this.runtime = runtime;
12
+ this.onRemoved = onRemoved;
13
+ }
14
+ operations() {
15
+ return {
16
+ 'workspace.register': (data) => this.register(data),
17
+ 'workspace.directories.list': (data) => this.listDirectories(data),
18
+ 'workspace.remove': (data) => this.remove(data),
19
+ 'workspace.list': () => ({ workspaces: this.database().listWorkspaces() })
20
+ };
21
+ }
22
+ async register(data) {
23
+ const input = data;
24
+ if (typeof input.path !== 'string')
25
+ throw new Error('WORKSPACE_INVALID');
26
+ if (input.createDirectoryName !== undefined && typeof input.createDirectoryName !== 'string')
27
+ throw new Error('WORKSPACE_DIRECTORY_NAME_INVALID');
28
+ let createdPath;
29
+ try {
30
+ const path = typeof input.createDirectoryName === 'string'
31
+ ? (createdPath = await createWorkspaceDirectory(input.path, input.createDirectoryName, this.config().allowedRoots))
32
+ : input.path;
33
+ const inspected = await inspectWorkspace(path, this.config().allowedRoots);
34
+ return {
35
+ workspace: this.database().saveWorkspace({
36
+ path: inspected.path,
37
+ kind: inspected.kind,
38
+ gitSummary: inspected.gitSummary ?? null
39
+ })
40
+ };
41
+ }
42
+ catch (error) {
43
+ if (createdPath !== undefined)
44
+ await rmdir(createdPath).catch(() => undefined);
45
+ throw error;
46
+ }
47
+ }
48
+ async listDirectories(data) {
49
+ const input = data;
50
+ if (input.path !== undefined && typeof input.path !== 'string')
51
+ throw new Error('WORKSPACE_DIRECTORY_INVALID');
52
+ return {
53
+ directory: await listWorkspaceDirectories(input.path, this.config().allowedRoots)
54
+ };
55
+ }
56
+ remove(data) {
57
+ const input = data;
58
+ if (typeof input.workspaceId !== 'string')
59
+ throw new Error('WORKSPACE_NOT_FOUND');
60
+ if (this.runtime.hasActiveWorkspaceLease(input.workspaceId))
61
+ throw new Error('WORKSPACE_BUSY');
62
+ const workspace = this.database().removeWorkspace(input.workspaceId);
63
+ this.runtime.removeWorkspace(input.workspaceId);
64
+ this.onRemoved(workspace.id);
65
+ return { workspace };
66
+ }
67
+ }
@@ -0,0 +1,23 @@
1
+ import type { NodeAgentSession } from '../database.js';
2
+ import { WorkspaceDomainState } from './workspace-domain-state.js';
3
+ export interface WorkspaceSessionServiceOptions {
4
+ discover: (workspaceId: string) => Promise<readonly NodeAgentSession[]>;
5
+ listPage: (workspaceId: string) => Promise<unknown>;
6
+ active: (workspaceId: string) => boolean;
7
+ emitRevision: (workspaceId: string, revision: number) => void;
8
+ onPending?: (workspaceId: string, waitMs: number) => void;
9
+ }
10
+ export declare class WorkspaceSessionService {
11
+ private readonly state;
12
+ private readonly options;
13
+ private readonly cacheGenerations;
14
+ constructor(state: WorkspaceDomainState, options: WorkspaceSessionServiceOptions);
15
+ ensureWatch(workspaceId: string): void;
16
+ stopWatch(workspaceId: string): void;
17
+ cached(workspaceId: string): Promise<readonly NodeAgentSession[]>;
18
+ clear(): void;
19
+ invalidate(workspaceId: string): void;
20
+ private bumpCacheGeneration;
21
+ private refreshWatch;
22
+ private performRefresh;
23
+ }
@@ -0,0 +1,139 @@
1
+ const WATCH_INTERVAL_MS = 30_000;
2
+ const CACHE_TTL_MS = 15_000;
3
+ const INITIAL_WAIT_MS = 500;
4
+ export class WorkspaceSessionService {
5
+ state;
6
+ options;
7
+ cacheGenerations = new Map();
8
+ constructor(state, options) {
9
+ this.state = state;
10
+ this.options = options;
11
+ }
12
+ ensureWatch(workspaceId) {
13
+ if (this.state.sessionWatchTimers.has(workspaceId))
14
+ return;
15
+ const timer = setInterval(() => void this.refreshWatch(workspaceId), WATCH_INTERVAL_MS);
16
+ timer.unref();
17
+ this.state.sessionWatchTimers.set(workspaceId, timer);
18
+ void this.refreshWatch(workspaceId);
19
+ }
20
+ stopWatch(workspaceId) {
21
+ const timer = this.state.sessionWatchTimers.get(workspaceId);
22
+ if (timer !== undefined)
23
+ clearInterval(timer);
24
+ this.state.sessionWatchTimers.delete(workspaceId);
25
+ this.state.sessionWatchSignatures.delete(workspaceId);
26
+ this.state.sessionWatchReads.delete(workspaceId);
27
+ }
28
+ async cached(workspaceId) {
29
+ const cached = this.state.sessionCache.get(workspaceId);
30
+ if (cached !== undefined && cached.expiresAt > Date.now())
31
+ return cached.sessions;
32
+ let initialDiscoveryTimedOut = false;
33
+ let discovery = this.state.sessionCacheReads.get(workspaceId);
34
+ if (discovery === undefined) {
35
+ const generation = this.cacheGenerations.get(workspaceId) ?? 0;
36
+ const read = this.options
37
+ .discover(workspaceId)
38
+ .then((sessions) => {
39
+ if ((this.cacheGenerations.get(workspaceId) ?? 0) !== generation)
40
+ return sessions;
41
+ const previous = this.state.sessionCache.get(workspaceId)?.sessions;
42
+ this.state.sessionCache.set(workspaceId, {
43
+ sessions,
44
+ expiresAt: Date.now() + CACHE_TTL_MS
45
+ });
46
+ if (initialDiscoveryTimedOut ||
47
+ (previous !== undefined && sessionSignature(previous) !== sessionSignature(sessions))) {
48
+ const revision = (this.state.watchRevisions.get(workspaceId) ?? 0) + 1;
49
+ this.state.watchRevisions.set(workspaceId, revision);
50
+ this.options.emitRevision(workspaceId, revision);
51
+ }
52
+ return sessions;
53
+ })
54
+ .finally(() => {
55
+ if (this.state.sessionCacheReads.get(workspaceId) === read)
56
+ this.state.sessionCacheReads.delete(workspaceId);
57
+ if (!this.state.sessionCacheReads.has(workspaceId) &&
58
+ !this.state.sessionCache.has(workspaceId) &&
59
+ (this.cacheGenerations.get(workspaceId) ?? 0) !== generation)
60
+ this.cacheGenerations.delete(workspaceId);
61
+ });
62
+ discovery = read;
63
+ this.state.sessionCacheReads.set(workspaceId, discovery);
64
+ }
65
+ if (cached !== undefined)
66
+ return cached.sessions;
67
+ const discovered = await within(discovery, INITIAL_WAIT_MS);
68
+ if (discovered !== undefined)
69
+ return discovered;
70
+ initialDiscoveryTimedOut = true;
71
+ this.options.onPending?.(workspaceId, INITIAL_WAIT_MS);
72
+ return [];
73
+ }
74
+ clear() {
75
+ for (const timer of this.state.sessionWatchTimers.values())
76
+ clearInterval(timer);
77
+ this.state.sessionWatchTimers.clear();
78
+ this.state.sessionWatchSignatures.clear();
79
+ this.state.sessionWatchReads.clear();
80
+ for (const workspaceId of new Set([
81
+ ...this.state.sessionCache.keys(),
82
+ ...this.state.sessionCacheReads.keys()
83
+ ]))
84
+ this.bumpCacheGeneration(workspaceId);
85
+ this.state.sessionCache.clear();
86
+ this.state.sessionCacheReads.clear();
87
+ }
88
+ invalidate(workspaceId) {
89
+ this.bumpCacheGeneration(workspaceId);
90
+ this.state.sessionCache.delete(workspaceId);
91
+ this.state.sessionCacheReads.delete(workspaceId);
92
+ this.stopWatch(workspaceId);
93
+ }
94
+ bumpCacheGeneration(workspaceId) {
95
+ this.cacheGenerations.set(workspaceId, (this.cacheGenerations.get(workspaceId) ?? 0) + 1);
96
+ }
97
+ async refreshWatch(workspaceId) {
98
+ const current = this.state.sessionWatchReads.get(workspaceId);
99
+ if (current !== undefined)
100
+ return current;
101
+ const refresh = this.performRefresh(workspaceId).finally(() => this.state.sessionWatchReads.delete(workspaceId));
102
+ this.state.sessionWatchReads.set(workspaceId, refresh);
103
+ return refresh;
104
+ }
105
+ async performRefresh(workspaceId) {
106
+ if (!this.options.active(workspaceId))
107
+ return this.stopWatch(workspaceId);
108
+ try {
109
+ const signature = JSON.stringify(await this.options.listPage(workspaceId));
110
+ const previous = this.state.sessionWatchSignatures.get(workspaceId);
111
+ this.state.sessionWatchSignatures.set(workspaceId, signature);
112
+ if (previous === undefined || previous === signature)
113
+ return;
114
+ const revision = (this.state.watchRevisions.get(workspaceId) ?? 0) + 1;
115
+ this.state.watchRevisions.set(workspaceId, revision);
116
+ this.options.emitRevision(workspaceId, revision);
117
+ }
118
+ catch {
119
+ // The bounded interval retries without fabricating session state.
120
+ }
121
+ }
122
+ }
123
+ function sessionSignature(sessions) {
124
+ return JSON.stringify(sessions.map((session) => [
125
+ session.id,
126
+ session.runnerTitle,
127
+ session.customTitle,
128
+ session.lastActivityAt,
129
+ session.pinnedAt,
130
+ session.pinOrder,
131
+ session.externalDiscovered
132
+ ]));
133
+ }
134
+ async function within(promise, timeoutMs) {
135
+ return Promise.race([
136
+ promise,
137
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
138
+ ]);
139
+ }
@@ -0,0 +1,37 @@
1
+ import type { NodeMessageAttachmentInput, NodeWorkspace } from '../database.js';
2
+ import { WorkspaceDomainState } from './workspace-domain-state.js';
3
+ export interface ResolvedComposerAttachment {
4
+ readonly attachment: NodeMessageAttachmentInput;
5
+ readonly path: string;
6
+ }
7
+ export declare class WorkspaceUploadService {
8
+ private readonly state;
9
+ private readonly onCompleted;
10
+ constructor(state: WorkspaceDomainState, onCompleted: (workspaceId: string) => void);
11
+ preflight(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<({
12
+ name: string;
13
+ status: string;
14
+ path: null;
15
+ } | {
16
+ name: string;
17
+ status: import("../workspace.js").WorkspaceUploadPreflightStatus;
18
+ path: string | null;
19
+ })[]>;
20
+ create(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<UploadStatus>;
21
+ write(input: Record<string, unknown>): Promise<UploadStatus>;
22
+ status(uploadId: unknown): UploadStatus;
23
+ complete(uploadId: unknown): Promise<UploadStatus>;
24
+ cancel(uploadId: unknown): Promise<void>;
25
+ resolve(workspaceId: string, ids: readonly string[]): Promise<readonly ResolvedComposerAttachment[]>;
26
+ shutdown(): Promise<void>;
27
+ private require;
28
+ private remove;
29
+ private attachmentTarget;
30
+ }
31
+ interface UploadStatus {
32
+ readonly uploadId: string;
33
+ readonly path: string;
34
+ readonly receivedBytes: number;
35
+ readonly size: number;
36
+ }
37
+ export {};