@myagentroam/node 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codex-app-server.d.ts +24 -0
- package/dist/codex-app-server.js +100 -5
- package/dist/connector.js +18 -15
- package/dist/control-outbound-scheduler.d.ts +4 -1
- package/dist/control-outbound-scheduler.js +39 -6
- package/dist/database.d.ts +2 -19
- package/dist/database.js +1 -62
- package/dist/event-buffer.d.ts +2 -5
- package/dist/event-buffer.js +3 -12
- package/dist/native-session-history.js +33 -12
- package/dist/runner/claude/managed-run-controller.d.ts +0 -1
- package/dist/runner/claude/managed-run-controller.js +17 -12
- package/dist/runner/codex/conversation-parser.d.ts +1 -1
- package/dist/runner/codex/conversation-parser.js +3 -0
- package/dist/runner/codex/managed-run-controller.d.ts +11 -2
- package/dist/runner/codex/managed-run-controller.js +79 -24
- package/dist/runner/codex-runner.d.ts +3 -0
- package/dist/runner/codex-runner.js +42 -3
- package/dist/runner/opencode/conversation-parser.js +1 -1
- package/dist/runner/opencode/managed-run-controller.d.ts +7 -2
- package/dist/runner/opencode/managed-run-controller.js +72 -14
- package/dist/runner/opencode-runner.d.ts +3 -0
- package/dist/runner/opencode-runner.js +25 -0
- package/dist/runner-profiles.js +10 -6
- package/dist/runtime-state.d.ts +7 -12
- package/dist/runtime-state.js +43 -53
- package/dist/service/attachment-domain-state.d.ts +2 -0
- package/dist/service/attachment-domain-state.js +2 -0
- package/dist/service/conversation-history-service.d.ts +8 -0
- package/dist/service/conversation-history-service.js +181 -9
- package/dist/service/conversation-segment-service.js +17 -10
- package/dist/service/external-session-resume-service.js +9 -0
- package/dist/service/fake-managed-run-service.d.ts +0 -1
- package/dist/service/fake-managed-run-service.js +0 -2
- package/dist/service/native-session-projection-service.js +5 -0
- package/dist/service/native-session-watch-service.d.ts +8 -2
- package/dist/service/native-session-watch-service.js +28 -10
- package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
- package/dist/service/node-connection-lifecycle-service.js +0 -1
- package/dist/service/node-control-channel.js +1 -1
- package/dist/service/node-control-message-service.d.ts +0 -1
- package/dist/service/node-control-message-service.js +9 -6
- package/dist/service/node-request-service.js +2 -1
- package/dist/service/node-workspace-coordinator.d.ts +1 -4
- package/dist/service/node-workspace-coordinator.js +2 -12
- package/dist/service/run-attachment-service.js +30 -6
- package/dist/service/run-event-service.d.ts +1 -0
- package/dist/service/run-event-service.js +80 -0
- package/dist/service/run-workbench-service.d.ts +1 -1
- package/dist/service/run-workbench-service.js +1 -5
- package/dist/service/runner-interaction-service.d.ts +1 -2
- package/dist/service/runner-interaction-service.js +1 -4
- package/dist/service/session-context-service.d.ts +1 -0
- package/dist/service/session-context-service.js +14 -0
- package/dist/service/session-domain-state.d.ts +1 -0
- package/dist/service/session-domain-state.js +1 -0
- package/dist/service/session-lifecycle-service.js +4 -12
- package/dist/service/session-message-service.d.ts +1 -0
- package/dist/service/session-message-service.js +109 -85
- package/dist/service/skill-directory-service.d.ts +3 -1
- package/dist/service/skill-directory-service.js +28 -4
- package/dist/service/skill-node-operation-service.d.ts +1 -1
- package/dist/service/skill-node-operation-service.js +4 -2
- package/dist/service/workbench-event-service.d.ts +1 -13
- package/dist/service/workbench-event-service.js +2 -36
- package/dist/service/workbench-manifest-service.d.ts +0 -1
- package/dist/service/workbench-manifest-service.js +0 -1
- package/dist/service/workspace-change-service.js +1 -1
- package/dist/service/workspace-domain-state.d.ts +1 -0
- package/dist/service/workspace-queue-workbench-service.d.ts +13 -2
- package/dist/service/workspace-queue-workbench-service.js +8 -2
- package/dist/service/workspace-upload-service.d.ts +2 -0
- package/dist/service/workspace-upload-service.js +28 -6
- package/dist/service/workspace-workbench-service.d.ts +0 -3
- package/dist/service/workspace-workbench-service.js +0 -17
- package/dist/util/runner-error.d.ts +6 -0
- package/dist/util/runner-error.js +52 -0
- package/dist/util/runner-native-session-parsers.d.ts +2 -1
- package/dist/util/runner-native-session-parsers.js +63 -15
- package/dist/workspace.js +33 -8
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ const INTERVAL_MS = 2_000;
|
|
|
3
3
|
const TTL_MS = 45_000;
|
|
4
4
|
const MAX_FAILURES = 3;
|
|
5
5
|
const INITIAL_TURN_LIMIT = 30;
|
|
6
|
+
const EMITTED_TURN_LIMIT = 10;
|
|
6
7
|
export class NativeSessionWatchService {
|
|
7
8
|
options;
|
|
8
9
|
watches = new Map();
|
|
@@ -32,7 +33,7 @@ export class NativeSessionWatchService {
|
|
|
32
33
|
return { renewed: true };
|
|
33
34
|
}
|
|
34
35
|
const watch = this.watch(session);
|
|
35
|
-
const turnSnapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT);
|
|
36
|
+
const turnSnapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT, false);
|
|
36
37
|
if (watch !== undefined && turnSnapshot === undefined) {
|
|
37
38
|
this.stop(session.id);
|
|
38
39
|
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
@@ -68,6 +69,9 @@ export class NativeSessionWatchService {
|
|
|
68
69
|
refresh: undefined,
|
|
69
70
|
sessionSignature: undefined,
|
|
70
71
|
signature: undefined,
|
|
72
|
+
page: undefined,
|
|
73
|
+
observedPage: undefined,
|
|
74
|
+
authority: undefined,
|
|
71
75
|
failures: 0,
|
|
72
76
|
timer: undefined
|
|
73
77
|
};
|
|
@@ -110,7 +114,7 @@ export class NativeSessionWatchService {
|
|
|
110
114
|
get size() {
|
|
111
115
|
return this.watches.size;
|
|
112
116
|
}
|
|
113
|
-
async refresh(sessionId, limit = 10) {
|
|
117
|
+
async refresh(sessionId, limit = 10, emit = true) {
|
|
114
118
|
const watch = this.watches.get(sessionId);
|
|
115
119
|
if (watch === undefined)
|
|
116
120
|
return undefined;
|
|
@@ -120,7 +124,7 @@ export class NativeSessionWatchService {
|
|
|
120
124
|
this.stop(sessionId);
|
|
121
125
|
return undefined;
|
|
122
126
|
}
|
|
123
|
-
const refresh = this.performRefresh(sessionId, watch, limit);
|
|
127
|
+
const refresh = this.performRefresh(sessionId, watch, limit, emit);
|
|
124
128
|
watch.refresh = refresh;
|
|
125
129
|
try {
|
|
126
130
|
return await refresh;
|
|
@@ -130,7 +134,7 @@ export class NativeSessionWatchService {
|
|
|
130
134
|
watch.refresh = undefined;
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
|
-
async performRefresh(sessionId, watch, limit) {
|
|
137
|
+
async performRefresh(sessionId, watch, limit, emit) {
|
|
134
138
|
try {
|
|
135
139
|
const session = await this.options.resolve(sessionId);
|
|
136
140
|
if (session === undefined ||
|
|
@@ -148,14 +152,28 @@ export class NativeSessionWatchService {
|
|
|
148
152
|
}
|
|
149
153
|
const page = await this.options.readPage(session, limit);
|
|
150
154
|
watch.failures = 0;
|
|
151
|
-
const turns = page.turns.length <=
|
|
152
|
-
|
|
155
|
+
const turns = page.turns.length <= EMITTED_TURN_LIMIT
|
|
156
|
+
? page.turns
|
|
157
|
+
: page.turns.slice(-EMITTED_TURN_LIMIT);
|
|
158
|
+
const observed = { ...page, turns };
|
|
159
|
+
const convergence = this.options.convergePage(watch.page, watch.observedPage, observed, watch.authority);
|
|
160
|
+
const converged = convergence.page;
|
|
161
|
+
const accepted = converged.turns.length <= EMITTED_TURN_LIMIT + 1
|
|
162
|
+
? converged
|
|
163
|
+
: {
|
|
164
|
+
...converged,
|
|
165
|
+
turns: converged.turns.slice(-(EMITTED_TURN_LIMIT + 1))
|
|
166
|
+
};
|
|
167
|
+
watch.observedPage = observed;
|
|
168
|
+
watch.authority = convergence.authority;
|
|
169
|
+
const signature = JSON.stringify({ source: accepted.source, turns: accepted.turns });
|
|
153
170
|
if (signature === watch.signature)
|
|
154
|
-
return
|
|
171
|
+
return accepted;
|
|
155
172
|
watch.signature = signature;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
173
|
+
watch.page = accepted;
|
|
174
|
+
if (emit)
|
|
175
|
+
this.options.emitPage(session, accepted);
|
|
176
|
+
return emit ? accepted : page;
|
|
159
177
|
}
|
|
160
178
|
catch {
|
|
161
179
|
watch.failures += 1;
|
|
@@ -3,7 +3,6 @@ import { type NodeConfig } from '../config.js';
|
|
|
3
3
|
import type { NodeMetrics } from '../operational.js';
|
|
4
4
|
import type { NodeControlChannel } from './node-control-channel.js';
|
|
5
5
|
import type { NodeTerminalChannel } from './node-terminal-channel.js';
|
|
6
|
-
import type { WorkbenchEventService } from './workbench-event-service.js';
|
|
7
6
|
interface NodeConnectionLifecycleServiceOptions {
|
|
8
7
|
readonly config: () => NodeConfig | undefined;
|
|
9
8
|
readonly setConfig: (config: NodeConfig) => void;
|
|
@@ -15,7 +14,6 @@ interface NodeConnectionLifecycleServiceOptions {
|
|
|
15
14
|
readonly setCapabilities: (capabilities: NodeCapabilities) => void;
|
|
16
15
|
readonly control: NodeControlChannel;
|
|
17
16
|
readonly terminal: NodeTerminalChannel;
|
|
18
|
-
readonly events: WorkbenchEventService;
|
|
19
17
|
readonly metrics: NodeMetrics;
|
|
20
18
|
readonly heartbeatMs: number;
|
|
21
19
|
readonly refreshMs: number;
|
|
@@ -48,7 +48,6 @@ export class NodeConnectionLifecycleService {
|
|
|
48
48
|
this.options.terminal.connect();
|
|
49
49
|
this.options.control.send('heartbeat', {});
|
|
50
50
|
this.options.metrics.heartbeatSent();
|
|
51
|
-
this.options.events.replay();
|
|
52
51
|
this.heartbeat = setInterval(() => {
|
|
53
52
|
this.options.metrics.heartbeatSent();
|
|
54
53
|
this.options.control.send('heartbeat', {});
|
|
@@ -5,7 +5,7 @@ import { nodeConnectEndpoint } from '../runner/codex/conversation-parser.js';
|
|
|
5
5
|
export class NodeControlChannel {
|
|
6
6
|
options;
|
|
7
7
|
socket;
|
|
8
|
-
outbound = new ControlOutboundScheduler(() => this.socket);
|
|
8
|
+
outbound = new ControlOutboundScheduler(() => this.socket, () => this.socket?.close(1013, 'NODE_CONTROL_BACKPRESSURE_TIMEOUT'));
|
|
9
9
|
reconnectTimer;
|
|
10
10
|
attempts = 0;
|
|
11
11
|
constructor(options) {
|
|
@@ -8,7 +8,6 @@ interface NodeControlMessageServiceOptions {
|
|
|
8
8
|
readonly close: (code: number, reason: string) => void;
|
|
9
9
|
readonly inspectWorkspace: (envelope: NodeEnvelope) => Promise<void>;
|
|
10
10
|
readonly handleNodeRequest: (envelope: NodeEnvelope) => Promise<void>;
|
|
11
|
-
readonly acknowledgeWorkbenchEvent: (replyTo: string | undefined) => void;
|
|
12
11
|
readonly reconcileRuns: (envelope: NodeEnvelope) => void;
|
|
13
12
|
readonly runners: RunnerRegistry;
|
|
14
13
|
readonly deliverSessionChannelMessage: (envelope: NodeEnvelope) => void;
|
|
@@ -33,12 +33,8 @@ export class NodeControlMessageService {
|
|
|
33
33
|
void this.options.handleNodeRequest(envelope);
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
-
if (envelope.type === 'ack')
|
|
37
|
-
const payload = envelope.payload;
|
|
38
|
-
if (payload.type === 'workbench.event')
|
|
39
|
-
this.options.acknowledgeWorkbenchEvent(envelope.replyTo);
|
|
36
|
+
if (envelope.type === 'ack')
|
|
40
37
|
return;
|
|
41
|
-
}
|
|
42
38
|
if (envelope.type === 'runs.reconcile') {
|
|
43
39
|
this.options.reconcileRuns(envelope);
|
|
44
40
|
return;
|
|
@@ -49,7 +45,14 @@ export class NodeControlMessageService {
|
|
|
49
45
|
}
|
|
50
46
|
if (envelope.type === 'run.start') {
|
|
51
47
|
this.options.send('ack', { type: envelope.type }, envelope.id);
|
|
52
|
-
|
|
48
|
+
try {
|
|
49
|
+
this.options.runners.startManagedRun(envelope);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// A delayed internal start may arrive after the terminal projection was removed.
|
|
53
|
+
if (!(error instanceof Error) || error.message !== 'RUN_TERMINAL')
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
53
56
|
return;
|
|
54
57
|
}
|
|
55
58
|
if (envelope.type === 'session.channel.message') {
|
|
@@ -23,7 +23,8 @@ export class NodeRequestService {
|
|
|
23
23
|
? {
|
|
24
24
|
...payload.data,
|
|
25
25
|
__workspaceWatchSessionHash: payload.userAuthorization
|
|
26
|
-
.sessionHash
|
|
26
|
+
.sessionHash,
|
|
27
|
+
__requestUserId: payload.userAuthorization.userId
|
|
27
28
|
}
|
|
28
29
|
: payload.data;
|
|
29
30
|
const result = await this.options.execute(payload.operation, data);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { NodeEnvelope } from '@myagentroam/protocol';
|
|
2
2
|
import type { NodeConfig } from '../config.js';
|
|
3
3
|
import type { NodeAgentSession, NodeDatabase, NodeWorkspace } from '../database.js';
|
|
4
|
-
import type { NodeRuntimeState } from '../runtime-state.js';
|
|
5
4
|
import type { TerminalSummary } from '../terminal.js';
|
|
6
5
|
import type { WorkspaceChangeService } from './workspace-change-service.js';
|
|
7
6
|
import type { WorkspaceSessionService } from './workspace-session-service.js';
|
|
@@ -9,14 +8,13 @@ import type { WorkspaceWatchService } from './workspace-watch-service.js';
|
|
|
9
8
|
interface NodeWorkspaceCoordinatorOptions {
|
|
10
9
|
readonly config: () => NodeConfig | undefined;
|
|
11
10
|
readonly database: () => NodeDatabase | undefined;
|
|
12
|
-
readonly runtime: NodeRuntimeState;
|
|
13
11
|
readonly watches: WorkspaceWatchService;
|
|
14
12
|
readonly sessions: WorkspaceSessionService;
|
|
15
13
|
readonly changes: WorkspaceChangeService;
|
|
16
14
|
readonly emit: (workspaceId: string, topic: 'files' | 'changes', revision: number) => void;
|
|
17
15
|
readonly send: (type: string, payload: unknown, replyTo?: string) => void;
|
|
18
16
|
}
|
|
19
|
-
/** Coordinates Workspace invalidation, inspection
|
|
17
|
+
/** Coordinates Workspace invalidation, inspection and persisted Terminal summaries. */
|
|
20
18
|
export declare class NodeWorkspaceCoordinator {
|
|
21
19
|
private readonly options;
|
|
22
20
|
constructor(options: NodeWorkspaceCoordinatorOptions);
|
|
@@ -27,6 +25,5 @@ export declare class NodeWorkspaceCoordinator {
|
|
|
27
25
|
invalidateChanges(workspaceId: string): void;
|
|
28
26
|
persistTerminal(summary: TerminalSummary): void;
|
|
29
27
|
inspect(envelope: NodeEnvelope): Promise<void>;
|
|
30
|
-
captureRunSnapshot(runId: string, cwd: string): Promise<void>;
|
|
31
28
|
}
|
|
32
29
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { inspectWorkspace
|
|
2
|
-
/** Coordinates Workspace invalidation, inspection
|
|
1
|
+
import { inspectWorkspace } from '../workspace.js';
|
|
2
|
+
/** Coordinates Workspace invalidation, inspection and persisted Terminal summaries. */
|
|
3
3
|
export class NodeWorkspaceCoordinator {
|
|
4
4
|
options;
|
|
5
5
|
constructor(options) {
|
|
@@ -67,14 +67,4 @@ export class NodeWorkspaceCoordinator {
|
|
|
67
67
|
this.options.send('workspace.inspect.result', { error: 'WORKSPACE_INVALID' }, envelope.id);
|
|
68
68
|
}
|
|
69
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
70
|
}
|
|
@@ -4,6 +4,7 @@ import { lstat, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:f
|
|
|
4
4
|
import { AttachmentDomainState } from './attachment-domain-state.js';
|
|
5
5
|
const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
6
6
|
const IMAGE_CACHE_TTL_MS = 60 * 60_000;
|
|
7
|
+
const NATIVE_IMAGE_INDEX_TTL_MS = 60 * 60_000;
|
|
7
8
|
export class RunAttachmentService {
|
|
8
9
|
nativeImageRoots;
|
|
9
10
|
state = new AttachmentDomainState();
|
|
@@ -43,21 +44,28 @@ export class RunAttachmentService {
|
|
|
43
44
|
throw new Error('MESSAGE_ATTACHMENT_DIRECTORY_INVALID');
|
|
44
45
|
const runDirectory = join(filesDirectory, runId);
|
|
45
46
|
await mkdir(runDirectory, { mode: 0o700 });
|
|
47
|
+
this.state.runFileDirectories.set(runId, runDirectory);
|
|
46
48
|
try {
|
|
47
49
|
await Promise.all(attachments.map((attachment) => writeFile(join(runDirectory, attachment.name), Buffer.from(attachment.dataBase64, 'base64'), { flag: 'wx', mode: 0o600 })));
|
|
48
50
|
return attachments.map((attachment) => `.mar/temp-files/${runId}/${attachment.name}`);
|
|
49
51
|
}
|
|
50
52
|
catch (error) {
|
|
53
|
+
this.state.runFileDirectories.delete(runId);
|
|
51
54
|
await rm(runDirectory, { recursive: true, force: true });
|
|
52
55
|
throw error;
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
cleanup(runId) {
|
|
56
59
|
const directory = this.state.runDirectories.get(runId);
|
|
57
|
-
if (directory
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
if (directory !== undefined) {
|
|
61
|
+
this.state.runDirectories.delete(runId);
|
|
62
|
+
void rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
63
|
+
}
|
|
64
|
+
const fileDirectory = this.state.runFileDirectories.get(runId);
|
|
65
|
+
if (fileDirectory !== undefined) {
|
|
66
|
+
this.state.runFileDirectories.delete(runId);
|
|
67
|
+
void rm(fileDirectory, { recursive: true, force: true }).catch(() => undefined);
|
|
68
|
+
}
|
|
61
69
|
}
|
|
62
70
|
cache(runId, images) {
|
|
63
71
|
if (images.length === 0)
|
|
@@ -122,8 +130,18 @@ export class RunAttachmentService {
|
|
|
122
130
|
}
|
|
123
131
|
attachments.push({ name: image.name, mime: image.mime, runId, imageIndex });
|
|
124
132
|
}
|
|
125
|
-
if (sources.length > 0)
|
|
133
|
+
if (sources.length > 0) {
|
|
126
134
|
this.state.nativeHistoryImages.set(runId, sources);
|
|
135
|
+
const previousTimer = this.state.nativeHistoryImageTimers.get(runId);
|
|
136
|
+
if (previousTimer !== undefined)
|
|
137
|
+
clearTimeout(previousTimer);
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
this.state.nativeHistoryImageTimers.delete(runId);
|
|
140
|
+
this.state.nativeHistoryImages.delete(runId);
|
|
141
|
+
}, NATIVE_IMAGE_INDEX_TTL_MS);
|
|
142
|
+
timer.unref();
|
|
143
|
+
this.state.nativeHistoryImageTimers.set(runId, timer);
|
|
144
|
+
}
|
|
127
145
|
return attachments;
|
|
128
146
|
}
|
|
129
147
|
async readNative(runId, imageIndex) {
|
|
@@ -161,10 +179,16 @@ export class RunAttachmentService {
|
|
|
161
179
|
this.state.runImages.delete(runId);
|
|
162
180
|
}
|
|
163
181
|
dispose() {
|
|
164
|
-
for (const runId of
|
|
182
|
+
for (const runId of new Set([
|
|
183
|
+
...this.state.runDirectories.keys(),
|
|
184
|
+
...this.state.runFileDirectories.keys()
|
|
185
|
+
]))
|
|
165
186
|
this.cleanup(runId);
|
|
166
187
|
for (const runId of this.state.runImages.keys())
|
|
167
188
|
this.clear(runId);
|
|
189
|
+
for (const timer of this.state.nativeHistoryImageTimers.values())
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
this.state.nativeHistoryImageTimers.clear();
|
|
168
192
|
this.state.nativeHistoryImages.clear();
|
|
169
193
|
}
|
|
170
194
|
}
|
|
@@ -50,6 +50,9 @@ export class RunEventService {
|
|
|
50
50
|
this.emitNow(runId, 'text.delta', { text: pending.text });
|
|
51
51
|
}
|
|
52
52
|
emitNow(runId, eventType, payload, status) {
|
|
53
|
+
if (this.options.runtime.getRun(runId) === undefined)
|
|
54
|
+
return;
|
|
55
|
+
const notificationPayload = this.notificationPayload(runId, eventType, payload, status);
|
|
53
56
|
const event = {
|
|
54
57
|
runId,
|
|
55
58
|
sequence: (this.sequences.get(runId) ?? 0) + 1,
|
|
@@ -76,6 +79,7 @@ export class RunEventService {
|
|
|
76
79
|
/* A stale relayed run may race the storage boundary. */
|
|
77
80
|
}
|
|
78
81
|
if (status !== undefined && isTerminalRunStatus(status)) {
|
|
82
|
+
this.options.state.active.delete(runId);
|
|
79
83
|
this.options.state.interruptRequested.delete(runId);
|
|
80
84
|
const terminalRun = this.options.runtime.getRun(runId);
|
|
81
85
|
if (terminalRun !== undefined && this.options.isBootstrapSession(terminalRun.sessionId))
|
|
@@ -91,6 +95,19 @@ export class RunEventService {
|
|
|
91
95
|
this.options.emitWorkbench('conversation', {
|
|
92
96
|
event: { ...event, createdAt: persisted?.createdAt ?? Date.now() }
|
|
93
97
|
});
|
|
98
|
+
if (notificationPayload !== undefined) {
|
|
99
|
+
const occurredAt = persisted?.createdAt ?? Date.now();
|
|
100
|
+
nodeLog('run.event', { runId, eventType: 'notification' });
|
|
101
|
+
this.options.emitWorkbench('conversation', {
|
|
102
|
+
event: {
|
|
103
|
+
runId,
|
|
104
|
+
sequence: event.sequence,
|
|
105
|
+
eventType: 'notification',
|
|
106
|
+
payload: { ...notificationPayload, occurredAt },
|
|
107
|
+
createdAt: occurredAt
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
94
111
|
if (status === undefined || !isTerminalRunStatus(status))
|
|
95
112
|
return;
|
|
96
113
|
const turn = this.options.runtime.conversationTurnForRun(runId);
|
|
@@ -102,7 +119,70 @@ export class RunEventService {
|
|
|
102
119
|
const run = this.options.runtime.getRun(runId);
|
|
103
120
|
if (run !== undefined) {
|
|
104
121
|
this.options.scheduleContextRefresh(run.sessionId);
|
|
122
|
+
this.options.runtime.removeTerminalRun(runId);
|
|
123
|
+
this.sequences.delete(runId);
|
|
105
124
|
void this.options.dispatchQueue(run.sessionId);
|
|
106
125
|
}
|
|
107
126
|
}
|
|
127
|
+
notificationPayload(runId, eventType, payload, status) {
|
|
128
|
+
if (eventType === 'notification')
|
|
129
|
+
return undefined;
|
|
130
|
+
const run = this.options.runtime.getRun(runId);
|
|
131
|
+
if (run === undefined)
|
|
132
|
+
return undefined;
|
|
133
|
+
const session = this.options.runtime.getAgentSession(run.sessionId);
|
|
134
|
+
if (session === undefined)
|
|
135
|
+
return undefined;
|
|
136
|
+
const base = {
|
|
137
|
+
userId: run.initiatedByUserId,
|
|
138
|
+
workspaceId: run.workspaceId,
|
|
139
|
+
sessionId: run.sessionId,
|
|
140
|
+
sessionTitle: session.customTitle ?? session.runnerTitle ?? session.id,
|
|
141
|
+
runId
|
|
142
|
+
};
|
|
143
|
+
if (!Number.isSafeInteger(base.userId))
|
|
144
|
+
return undefined;
|
|
145
|
+
if (status !== undefined && isTerminalRunStatus(status)) {
|
|
146
|
+
if (this.options.state.interruptRequested.has(runId))
|
|
147
|
+
return undefined;
|
|
148
|
+
if (status === 'SUCCEEDED' &&
|
|
149
|
+
this.options.runtime.sessionQueue(run.sessionId).runIds.length > 0)
|
|
150
|
+
return undefined;
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
notificationId: `run:${run.workspaceId}:${runId}`,
|
|
154
|
+
kind: 'RUN_TERMINAL',
|
|
155
|
+
outcome: status
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (eventType === 'approval.request' && isPlainRecord(payload)) {
|
|
159
|
+
const approvalId = payload.approvalId;
|
|
160
|
+
if (typeof approvalId !== 'string')
|
|
161
|
+
return undefined;
|
|
162
|
+
return {
|
|
163
|
+
...base,
|
|
164
|
+
notificationId: `approval:${run.workspaceId}:${runId}:${approvalId}`,
|
|
165
|
+
kind: 'APPROVAL_REQUIRED'
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (eventType !== 'conversation.item' || !isPlainRecord(payload))
|
|
169
|
+
return undefined;
|
|
170
|
+
if (payload.kind === 'plan' && typeof payload.itemId === 'string')
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
notificationId: `plan:${run.workspaceId}:${runId}:${payload.itemId}`,
|
|
174
|
+
kind: 'PLAN_READY'
|
|
175
|
+
};
|
|
176
|
+
if (payload.kind === 'user_input_request' &&
|
|
177
|
+
typeof payload.itemId === 'string' &&
|
|
178
|
+
isPlainRecord(payload.payload) &&
|
|
179
|
+
Array.isArray(payload.payload.questions) &&
|
|
180
|
+
payload.payload.questions.length > 0)
|
|
181
|
+
return {
|
|
182
|
+
...base,
|
|
183
|
+
notificationId: `user-input:${run.workspaceId}:${runId}:${payload.itemId}`,
|
|
184
|
+
kind: 'USER_INPUT_REQUIRED'
|
|
185
|
+
};
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
108
188
|
}
|
|
@@ -6,6 +6,7 @@ export interface RunWorkbenchServiceOptions {
|
|
|
6
6
|
readonly respondUserInput: (runId: string, requestId: string, answers: Record<string, unknown>) => void;
|
|
7
7
|
readonly deleteQueuedStart: (runId: string) => void;
|
|
8
8
|
readonly clearImages: (runId: string) => void;
|
|
9
|
+
readonly cleanupAttachments: (runId: string) => void;
|
|
9
10
|
readonly emitQueue: (workspaceId: string, sessionId: string) => void;
|
|
10
11
|
readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'CANCELLED') => void;
|
|
11
12
|
readonly control: (operation: string, payload: Record<string, unknown>) => void;
|
|
@@ -16,7 +17,6 @@ export declare class RunWorkbenchService {
|
|
|
16
17
|
constructor(options: RunWorkbenchServiceOptions);
|
|
17
18
|
operations(): Readonly<Record<string, NodeOperationHandler>>;
|
|
18
19
|
private get;
|
|
19
|
-
private snapshot;
|
|
20
20
|
private image;
|
|
21
21
|
private respond;
|
|
22
22
|
private interruptSession;
|
|
@@ -8,7 +8,6 @@ export class RunWorkbenchService {
|
|
|
8
8
|
operations() {
|
|
9
9
|
return {
|
|
10
10
|
'run.get': (data) => this.get(record(data)),
|
|
11
|
-
'run.snapshot': (data) => this.snapshot(record(data)),
|
|
12
11
|
'run.image.read': (data) => this.image(record(data)),
|
|
13
12
|
'run.user-input.respond': (data) => this.respond(record(data)),
|
|
14
13
|
'session.interrupt': (data) => this.interruptSession(record(data)),
|
|
@@ -21,10 +20,6 @@ export class RunWorkbenchService {
|
|
|
21
20
|
get(input) {
|
|
22
21
|
return { run: this.requireRun(input.runId) };
|
|
23
22
|
}
|
|
24
|
-
snapshot(input) {
|
|
25
|
-
const run = this.requireRun(input.runId);
|
|
26
|
-
return { snapshot: this.options.runtime.getSnapshot(run.id) ?? null };
|
|
27
|
-
}
|
|
28
23
|
async image(input) {
|
|
29
24
|
if (typeof input.runId !== 'string' ||
|
|
30
25
|
typeof input.imageIndex !== 'number' ||
|
|
@@ -63,6 +58,7 @@ export class RunWorkbenchService {
|
|
|
63
58
|
this.options.runtime.deleteQueuedRun(run.id);
|
|
64
59
|
this.options.deleteQueuedStart(run.id);
|
|
65
60
|
this.options.clearImages(run.id);
|
|
61
|
+
this.options.cleanupAttachments(run.id);
|
|
66
62
|
this.options.emitQueue(run.workspaceId, run.sessionId);
|
|
67
63
|
return { cancelled: true };
|
|
68
64
|
}
|
|
@@ -13,7 +13,6 @@ interface RunnerInteractionServiceOptions {
|
|
|
13
13
|
readonly runners: RunnerRegistry;
|
|
14
14
|
readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'FAILED' | 'CANCELLED' | 'INTERRUPTED') => void;
|
|
15
15
|
readonly emitItem: (runId: string, item: PublicConversationItem) => void;
|
|
16
|
-
readonly captureSnapshot: (runId: string, cwd: string) => Promise<void>;
|
|
17
16
|
readonly approvalTimeoutMs: number;
|
|
18
17
|
readonly publishApproval: (input: {
|
|
19
18
|
readonly runId: string;
|
|
@@ -33,7 +32,7 @@ export declare class RunnerInteractionService {
|
|
|
33
32
|
cancelClaudeRun(runId: string, intent: 'CANCEL' | 'INTERRUPT'): boolean;
|
|
34
33
|
cancelCodexTurn(runId: string, threadId: string, turnId: string, cwd: string, terminalStatus: 'CANCELLED' | 'INTERRUPTED'): Promise<void>;
|
|
35
34
|
respondUserInput(runId: string, requestId: string, rawAnswers: Record<string, unknown>): void;
|
|
36
|
-
completeCancelledRun(runId: string
|
|
35
|
+
completeCancelledRun(runId: string): boolean;
|
|
37
36
|
private respondCodexUserInput;
|
|
38
37
|
private answers;
|
|
39
38
|
}
|
|
@@ -162,7 +162,6 @@ export class RunnerInteractionService {
|
|
|
162
162
|
codex.execution.runs.delete(runId);
|
|
163
163
|
codex.releaseEnvironment(runId);
|
|
164
164
|
this.options.state.active.delete(runId);
|
|
165
|
-
void this.options.captureSnapshot(runId, cwd);
|
|
166
165
|
}
|
|
167
166
|
}
|
|
168
167
|
respondUserInput(runId, requestId, rawAnswers) {
|
|
@@ -194,7 +193,7 @@ export class RunnerInteractionService {
|
|
|
194
193
|
merge: true
|
|
195
194
|
});
|
|
196
195
|
}
|
|
197
|
-
completeCancelledRun(runId
|
|
196
|
+
completeCancelledRun(runId) {
|
|
198
197
|
const run = this.options.runtime.getRun(runId);
|
|
199
198
|
if (run === undefined)
|
|
200
199
|
return false;
|
|
@@ -206,8 +205,6 @@ export class RunnerInteractionService {
|
|
|
206
205
|
return false;
|
|
207
206
|
this.options.codex().execution.runs.delete(runId);
|
|
208
207
|
this.options.state.active.delete(runId);
|
|
209
|
-
if (cwd !== undefined)
|
|
210
|
-
void this.options.captureSnapshot(runId, cwd);
|
|
211
208
|
return true;
|
|
212
209
|
}
|
|
213
210
|
respondCodexUserInput(runId, requestId, rawAnswers) {
|
|
@@ -24,6 +24,7 @@ export interface SessionContextServiceOptions {
|
|
|
24
24
|
export declare class SessionContextService {
|
|
25
25
|
private readonly options;
|
|
26
26
|
private readonly snapshots;
|
|
27
|
+
private readonly snapshotTimers;
|
|
27
28
|
private readonly timers;
|
|
28
29
|
private readonly versions;
|
|
29
30
|
constructor(options: SessionContextServiceOptions);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const REFRESH_DELAYS_MS = [100, 400, 1_000];
|
|
2
|
+
const CONTEXT_SNAPSHOT_TTL_MS = 30 * 60_000;
|
|
2
3
|
export class SessionContextService {
|
|
3
4
|
options;
|
|
4
5
|
snapshots = new Map();
|
|
6
|
+
snapshotTimers = new Map();
|
|
5
7
|
timers = new Map();
|
|
6
8
|
versions = new Map();
|
|
7
9
|
constructor(options) {
|
|
@@ -20,6 +22,15 @@ export class SessionContextService {
|
|
|
20
22
|
percentage: Math.round((usage.usedTokens / usage.maxTokens) * 1000) / 10
|
|
21
23
|
};
|
|
22
24
|
this.snapshots.set(session.id, context);
|
|
25
|
+
const previousTimer = this.snapshotTimers.get(session.id);
|
|
26
|
+
if (previousTimer !== undefined)
|
|
27
|
+
clearTimeout(previousTimer);
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
this.snapshotTimers.delete(session.id);
|
|
30
|
+
this.snapshots.delete(session.id);
|
|
31
|
+
}, CONTEXT_SNAPSHOT_TTL_MS);
|
|
32
|
+
timer.unref();
|
|
33
|
+
this.snapshotTimers.set(session.id, timer);
|
|
23
34
|
return { context, pending: false };
|
|
24
35
|
}
|
|
25
36
|
schedule(sessionId) {
|
|
@@ -71,7 +82,10 @@ export class SessionContextService {
|
|
|
71
82
|
dispose() {
|
|
72
83
|
for (const timer of this.timers.values())
|
|
73
84
|
clearTimeout(timer);
|
|
85
|
+
for (const timer of this.snapshotTimers.values())
|
|
86
|
+
clearTimeout(timer);
|
|
74
87
|
this.timers.clear();
|
|
88
|
+
this.snapshotTimers.clear();
|
|
75
89
|
this.versions.clear();
|
|
76
90
|
this.snapshots.clear();
|
|
77
91
|
}
|
|
@@ -5,4 +5,5 @@ export declare class SessionDomainState<TSession, TActivity> {
|
|
|
5
5
|
readonly resumeFailures: Map<string, string>;
|
|
6
6
|
readonly messageResumeFailures: Map<string, string>;
|
|
7
7
|
readonly resumeFailureDetails: Map<string, string>;
|
|
8
|
+
readonly failureTimers: Map<string, NodeJS.Timeout>;
|
|
8
9
|
}
|
|
@@ -112,25 +112,16 @@ export class SessionLifecycleService {
|
|
|
112
112
|
async updateConfiguration(input) {
|
|
113
113
|
if (typeof input.sessionId !== 'string' ||
|
|
114
114
|
typeof input.model !== 'string' ||
|
|
115
|
+
!validSessionOption(input.model) ||
|
|
115
116
|
typeof input.effort !== 'string' ||
|
|
116
117
|
!validSessionEffort(input.effort) ||
|
|
117
|
-
typeof input.access !== 'string'
|
|
118
|
+
typeof input.access !== 'string' ||
|
|
119
|
+
!validSessionOption(input.access))
|
|
118
120
|
throw new Error('SESSION_INVALID');
|
|
119
121
|
const current = this.options.runtime.getAgentSession(input.sessionId) ??
|
|
120
122
|
(await this.options.projection.createMetadataProjection(input.sessionId));
|
|
121
123
|
if (current === undefined)
|
|
122
124
|
throw new Error('SESSION_NOT_FOUND');
|
|
123
|
-
const configuration = {
|
|
124
|
-
runner: current.runner,
|
|
125
|
-
model: input.model,
|
|
126
|
-
effort: input.effort,
|
|
127
|
-
access: input.access
|
|
128
|
-
};
|
|
129
|
-
const workspace = this.options.database().getWorkspace(current.workspaceId);
|
|
130
|
-
const environment = parseSecretEnvironment(input.secretEnvironment);
|
|
131
|
-
if (workspace === undefined ||
|
|
132
|
-
!this.options.runners.supportsConfiguration(configuration, workspace, environment))
|
|
133
|
-
throw new Error('RUNNER_CONFIGURATION_UNSUPPORTED');
|
|
134
125
|
const session = this.options.runtime.updateAgentSessionConfiguration({
|
|
135
126
|
sessionId: current.id,
|
|
136
127
|
model: input.model,
|
|
@@ -209,6 +200,7 @@ export class SessionLifecycleService {
|
|
|
209
200
|
sessionId: messageSessionId,
|
|
210
201
|
clientMessageId: input.clientMessageId,
|
|
211
202
|
content: input.content,
|
|
203
|
+
...(input.__requestUserId === undefined ? {} : { __requestUserId: input.__requestUserId }),
|
|
212
204
|
...(input.secretEnvironment === undefined
|
|
213
205
|
? {}
|
|
214
206
|
: { secretEnvironment: input.secretEnvironment }),
|
|
@@ -23,6 +23,7 @@ interface SessionMessageServiceOptions {
|
|
|
23
23
|
}
|
|
24
24
|
export declare class SessionMessageService {
|
|
25
25
|
private readonly options;
|
|
26
|
+
private readonly pendingMessages;
|
|
26
27
|
constructor(options: SessionMessageServiceOptions);
|
|
27
28
|
operations(): Readonly<Record<string, NodeOperationHandler>>;
|
|
28
29
|
private message;
|