@myagentroam/node 0.9.122 → 0.9.125
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.js +6 -79
- package/dist/connector.js +217 -6
- package/dist/database.js +2 -1
- package/dist/migrations/v014.js +107 -0
- package/dist/runner/abstract-runner.js +11 -5
- package/dist/runner/claude-code-runner.js +3 -0
- package/dist/runner/codex/conversation-parser.js +142 -2
- package/dist/runner/codex/managed-run-controller.js +26 -18
- package/dist/runner/codex-runner.js +127 -57
- package/dist/runner/opencode-runner.js +14 -12
- package/dist/runtime-state.js +6 -1
- package/dist/service/continuous-run-coordinator.js +8 -2
- package/dist/service/conversation-history-service.js +51 -10
- package/dist/service/managed-runner-session-service.js +2 -1
- package/dist/service/native-session-projection-service.js +3 -1
- package/dist/service/node-control-message-service.js +2 -0
- package/dist/service/run-event-service.js +18 -2
- package/dist/service/runner-interaction-service.js +9 -8
- package/dist/service/scheduled-run-authorization-control.js +72 -0
- package/dist/service/scheduled-session-coordinator.js +95 -0
- package/dist/service/scheduled-session-recurrence.js +72 -0
- package/dist/service/scheduled-session-store.js +234 -0
- package/dist/service/scheduled-task-mcp-server.js +179 -0
- package/dist/service/scheduled-task-mcp-stdio.js +63 -0
- package/dist/service/session-catalog-service.js +3 -1
- package/dist/service/session-lifecycle-service.js +6 -0
- package/dist/service/session-message-service.js +25 -2
- package/dist/service/session-presentation-service.js +19 -2
- package/dist/service/session-schedule-service.js +153 -0
- package/dist/util/runner-native-session-parsers.js +19 -11
- package/package.json +4 -4
|
@@ -8,7 +8,6 @@ import { homedir } from 'node:os';
|
|
|
8
8
|
import { openCodeConversationPage } from './opencode/conversation-parser.js';
|
|
9
9
|
import { nodeLog } from '../operational.js';
|
|
10
10
|
const PROFILE_ENTRY_TTL_MS = 30 * 60_000;
|
|
11
|
-
const PROFILE_STALE_TTL_MS = 60 * 60_000;
|
|
12
11
|
const PROFILE_FAILURE_RETRY_MS = 30_000;
|
|
13
12
|
const PROFILE_REFRESH_TIMEOUT_MS = 90_000;
|
|
14
13
|
export class OpenCodeRunner extends AbstractRunner {
|
|
@@ -21,6 +20,7 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
21
20
|
profileCache;
|
|
22
21
|
profileRetryAt = 0;
|
|
23
22
|
profileRefresh;
|
|
23
|
+
profileRefreshTimer;
|
|
24
24
|
profileGeneration = 0;
|
|
25
25
|
execution = new Map();
|
|
26
26
|
managedTurns = new Map();
|
|
@@ -40,7 +40,6 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
40
40
|
return { ...base, models: [] };
|
|
41
41
|
}
|
|
42
42
|
cachedProfile(capabilities, environment = {}) {
|
|
43
|
-
this.pruneProfileCaches(Date.now());
|
|
44
43
|
const cached = this.profileCache;
|
|
45
44
|
return cached === undefined
|
|
46
45
|
? this.profile(capabilities)
|
|
@@ -51,7 +50,6 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
51
50
|
return this.profile(capabilities);
|
|
52
51
|
}
|
|
53
52
|
const now = Date.now();
|
|
54
|
-
this.pruneProfileCaches(now);
|
|
55
53
|
const cached = this.profileCache;
|
|
56
54
|
if (cached !== undefined && cached.expiresAt > now) {
|
|
57
55
|
nodeLog('runner.opencode.profile.cache-hit', {
|
|
@@ -93,11 +91,11 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
93
91
|
this.contextLimitsExpiresAt = expiresAt;
|
|
94
92
|
this.profileCache = {
|
|
95
93
|
expiresAt,
|
|
96
|
-
discardAt: Date.now() + PROFILE_STALE_TTL_MS,
|
|
97
94
|
models,
|
|
98
95
|
contextLimits
|
|
99
96
|
};
|
|
100
97
|
this.profileRetryAt = 0;
|
|
98
|
+
this.scheduleProfileRefresh(PROFILE_ENTRY_TTL_MS, capabilities, environment);
|
|
101
99
|
nodeLog('runner.opencode.profile.providers.completed', {
|
|
102
100
|
models: models.length,
|
|
103
101
|
durationMs: Math.round(performance.now() - startedAt)
|
|
@@ -108,6 +106,7 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
108
106
|
if (generation !== this.profileGeneration)
|
|
109
107
|
return this.profile(capabilities);
|
|
110
108
|
this.profileRetryAt = Date.now() + PROFILE_FAILURE_RETRY_MS;
|
|
109
|
+
this.scheduleProfileRefresh(PROFILE_FAILURE_RETRY_MS, capabilities, environment);
|
|
111
110
|
nodeLog('runner.opencode.profile.providers.failed', {
|
|
112
111
|
durationMs: Math.round(performance.now() - startedAt)
|
|
113
112
|
});
|
|
@@ -153,17 +152,17 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
153
152
|
};
|
|
154
153
|
}
|
|
155
154
|
cachedModels(environment) {
|
|
156
|
-
this.pruneProfileCaches(Date.now());
|
|
157
155
|
void environment;
|
|
158
156
|
return this.profileCache?.models ?? [];
|
|
159
157
|
}
|
|
160
|
-
|
|
161
|
-
if (this.
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
this.
|
|
166
|
-
|
|
158
|
+
scheduleProfileRefresh(delayMs, capabilities, environment) {
|
|
159
|
+
if (this.profileRefreshTimer !== undefined)
|
|
160
|
+
clearTimeout(this.profileRefreshTimer);
|
|
161
|
+
this.profileRefreshTimer = setTimeout(() => {
|
|
162
|
+
this.profileRefreshTimer = undefined;
|
|
163
|
+
void this.refreshProfile(capabilities, undefined, environment, true);
|
|
164
|
+
}, delayMs);
|
|
165
|
+
this.profileRefreshTimer.unref();
|
|
167
166
|
}
|
|
168
167
|
mcpConfiguration(raw, secrets) {
|
|
169
168
|
const servers = super.mcpConfiguration(raw, secrets);
|
|
@@ -331,6 +330,9 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
331
330
|
this.managedTurns.clear();
|
|
332
331
|
this.profileCache = undefined;
|
|
333
332
|
this.profileRefresh = undefined;
|
|
333
|
+
if (this.profileRefreshTimer !== undefined)
|
|
334
|
+
clearTimeout(this.profileRefreshTimer);
|
|
335
|
+
this.profileRefreshTimer = undefined;
|
|
334
336
|
this.profileRetryAt = 0;
|
|
335
337
|
this.contextLimits = new Map();
|
|
336
338
|
this.contextLimitsExpiresAt = 0;
|
package/dist/runtime-state.js
CHANGED
|
@@ -288,9 +288,12 @@ export class NodeRuntimeState {
|
|
|
288
288
|
const value = title.trim().replace(/\s+/g, ' ').slice(0, 160);
|
|
289
289
|
if (value.length === 0 ||
|
|
290
290
|
current.customTitle !== null ||
|
|
291
|
-
current.runnerTitle === value ||
|
|
292
291
|
(current.titleSource === 'AUTO' && isDefaultRunnerTitle(value)))
|
|
293
292
|
return current;
|
|
293
|
+
if (current.runnerTitle === value)
|
|
294
|
+
return current.titleSource === 'RUNNER'
|
|
295
|
+
? current
|
|
296
|
+
: this.replaceSession(id, { titleSource: 'RUNNER' });
|
|
294
297
|
return this.replaceSession(id, { runnerTitle: value, titleSource: 'RUNNER' });
|
|
295
298
|
}
|
|
296
299
|
createMessageRun(input) {
|
|
@@ -316,6 +319,7 @@ export class NodeRuntimeState {
|
|
|
316
319
|
recoveryState: 'ACTIVE',
|
|
317
320
|
version: 0,
|
|
318
321
|
clientMessageId: input.clientMessageId,
|
|
322
|
+
...(input.source === undefined ? {} : { source: input.source }),
|
|
319
323
|
...(input.initiatedByUserId === undefined
|
|
320
324
|
? {}
|
|
321
325
|
: { initiatedByUserId: input.initiatedByUserId }),
|
|
@@ -338,6 +342,7 @@ export class NodeRuntimeState {
|
|
|
338
342
|
payload: {
|
|
339
343
|
clientMessageId: input.clientMessageId,
|
|
340
344
|
text: input.content,
|
|
345
|
+
...(input.source === undefined ? {} : { source: input.source }),
|
|
341
346
|
contexts: [],
|
|
342
347
|
attachments,
|
|
343
348
|
delivery: 'ACCEPTED'
|
|
@@ -286,6 +286,7 @@ export class ContinuousRunCoordinator {
|
|
|
286
286
|
const goalMcp = pending.goal === undefined
|
|
287
287
|
? undefined
|
|
288
288
|
: await this.options.goalMcp?.(pending);
|
|
289
|
+
const scheduledMcp = await this.options.scheduledMcp?.(pending);
|
|
289
290
|
if (!this.goalCurrent(pending)) {
|
|
290
291
|
this.dispatchedExecutions.delete(executionId);
|
|
291
292
|
this.rejectStaleGoal(executionId);
|
|
@@ -311,7 +312,11 @@ export class ContinuousRunCoordinator {
|
|
|
311
312
|
collaborationMode: pending.collaborationMode,
|
|
312
313
|
...(pending.serviceTier === undefined ? {} : { serviceTier: pending.serviceTier }),
|
|
313
314
|
secretEnvironment: pending.secretEnvironment,
|
|
314
|
-
...(goalMcp === undefined
|
|
315
|
+
...(goalMcp === undefined && scheduledMcp === undefined
|
|
316
|
+
? {}
|
|
317
|
+
: {
|
|
318
|
+
mcpSecretEnvironment: { ...goalMcp?.secrets, ...scheduledMcp?.secrets }
|
|
319
|
+
}),
|
|
315
320
|
...(pending.personalInstructions === undefined
|
|
316
321
|
? {}
|
|
317
322
|
: { personalInstructions: pending.personalInstructions }),
|
|
@@ -323,7 +328,8 @@ export class ContinuousRunCoordinator {
|
|
|
323
328
|
...(pending.computerUse === undefined ? {} : { computerUse: pending.computerUse }),
|
|
324
329
|
mcpInstallations: [
|
|
325
330
|
...this.options.mcps(pending.workspaceId),
|
|
326
|
-
...(goalMcp === undefined ? [] : [goalMcp.installation])
|
|
331
|
+
...(goalMcp === undefined ? [] : [goalMcp.installation]),
|
|
332
|
+
...(scheduledMcp === undefined ? [] : [scheduledMcp.installation])
|
|
327
333
|
]
|
|
328
334
|
});
|
|
329
335
|
}
|
|
@@ -123,7 +123,7 @@ export class ConversationHistoryService {
|
|
|
123
123
|
if (activeCursor !== undefined ||
|
|
124
124
|
this.options.hasManagedActiveRun(session) ||
|
|
125
125
|
this.hasManagedRuntimeHistory(session))
|
|
126
|
-
return this.
|
|
126
|
+
return this.readManagedRuntime(session, input, activeCursor, snapshotSequence, readAt);
|
|
127
127
|
return this.readHistory(session, input, snapshotSequence, readAt);
|
|
128
128
|
}
|
|
129
129
|
async readHistoryPage(session, input) {
|
|
@@ -140,7 +140,7 @@ export class ConversationHistoryService {
|
|
|
140
140
|
cursor: null
|
|
141
141
|
}
|
|
142
142
|
: undefined;
|
|
143
|
-
return this.
|
|
143
|
+
return this.readManagedRuntime(session, input, olderCursor, this.options.snapshotSequence(), Date.now());
|
|
144
144
|
}
|
|
145
145
|
return this.readHistory(session, input, this.options.snapshotSequence(), Date.now());
|
|
146
146
|
}
|
|
@@ -220,7 +220,7 @@ export class ConversationHistoryService {
|
|
|
220
220
|
const runtimeTurn = runtimeTurnsByOfficialId.get(turn.id);
|
|
221
221
|
const hydratedTurn = runtimeTurn === undefined
|
|
222
222
|
? turn
|
|
223
|
-
:
|
|
223
|
+
: mergeRuntimeUserMetadata(turn, runtimeTurn, this.options.attachments);
|
|
224
224
|
const runtimeSupplementalItems = runtimeSupplementalItemsByOfficialId.get(turn.id) ?? [];
|
|
225
225
|
const items = [
|
|
226
226
|
...hydratedTurn.items.filter((item) => (item.kind !== 'approval_request' && item.kind !== 'user_input_request') ||
|
|
@@ -367,6 +367,38 @@ export class ConversationHistoryService {
|
|
|
367
367
|
deferredOlderHistory: true
|
|
368
368
|
});
|
|
369
369
|
}
|
|
370
|
+
async readManagedRuntime(session, input, cursor, snapshotSequence, readAt) {
|
|
371
|
+
const page = await this.readManagedActive(session, input, cursor, snapshotSequence, readAt);
|
|
372
|
+
if (input.cursor !== undefined ||
|
|
373
|
+
session.externalSessionId === null ||
|
|
374
|
+
this.options.hasManagedActiveRun(session) ||
|
|
375
|
+
!this.hasManagedRuntimeHistory(session) ||
|
|
376
|
+
!page.turns.some((turn) => turn.rewind === undefined &&
|
|
377
|
+
terminalConversationTurn(turn.status) &&
|
|
378
|
+
conversationClientMessageId(turn) !== null))
|
|
379
|
+
return page;
|
|
380
|
+
// A completed managed Turn keeps its live items and Run identity until Runtime cleanup.
|
|
381
|
+
// Only the SDK's matched native Turn may supply a destructive history boundary.
|
|
382
|
+
const official = await this.readOfficial(session, {
|
|
383
|
+
limit: Math.min(500, Math.max(30, page.turns.length + 1))
|
|
384
|
+
}).catch(() => undefined);
|
|
385
|
+
if (official === undefined)
|
|
386
|
+
return page;
|
|
387
|
+
return {
|
|
388
|
+
...page,
|
|
389
|
+
turns: page.turns.map((turn) => {
|
|
390
|
+
if (turn.rewind !== undefined || !terminalConversationTurn(turn.status))
|
|
391
|
+
return turn;
|
|
392
|
+
const messageId = conversationClientMessageId(turn);
|
|
393
|
+
if (messageId === null)
|
|
394
|
+
return turn;
|
|
395
|
+
const native = official.turns.find((candidate) => candidate.rewind !== undefined &&
|
|
396
|
+
candidate.userItemId !== null &&
|
|
397
|
+
conversationClientMessageId(candidate) === messageId);
|
|
398
|
+
return native?.rewind === undefined ? turn : { ...turn, rewind: native.rewind };
|
|
399
|
+
})
|
|
400
|
+
};
|
|
401
|
+
}
|
|
370
402
|
async refreshExternalActivity(session) {
|
|
371
403
|
const runner = this.options.runners.require(session.runner);
|
|
372
404
|
if (!runner.available(this.options.capabilities())) {
|
|
@@ -514,7 +546,7 @@ function latestTurnActivityAt(turn) {
|
|
|
514
546
|
function oldestTurnActivityAt(turns) {
|
|
515
547
|
return turns.length === 0 ? 0 : Math.min(...turns.map(earliestTurnActivityAt));
|
|
516
548
|
}
|
|
517
|
-
function
|
|
549
|
+
function mergeRuntimeUserMetadata(officialTurn, runtimeTurn, attachments) {
|
|
518
550
|
const officialUser = officialTurn.items.find((item) => item.kind === 'user_message');
|
|
519
551
|
const runtimeUser = runtimeTurn.items.find((item) => item.kind === 'user_message');
|
|
520
552
|
if (officialUser === undefined ||
|
|
@@ -524,14 +556,19 @@ function mergeRuntimeUserImageReferences(officialTurn, runtimeTurn, attachments)
|
|
|
524
556
|
return officialTurn;
|
|
525
557
|
const officialPayload = officialUser.payload;
|
|
526
558
|
const runtimePayload = runtimeUser.payload;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
559
|
+
const scheduledSource = runtimePayload.source === 'SCHEDULED' && officialPayload.source !== 'SCHEDULED';
|
|
560
|
+
const officialAttachments = Array.isArray(officialPayload.attachments)
|
|
561
|
+
? officialPayload.attachments
|
|
562
|
+
: [];
|
|
563
|
+
const runtimeAttachments = Array.isArray(runtimePayload.attachments)
|
|
564
|
+
? runtimePayload.attachments
|
|
565
|
+
: [];
|
|
566
|
+
const runtimeImages = runtimeAttachments.filter((attachment) => isPlainRecord(attachment) &&
|
|
530
567
|
typeof attachment.mime === 'string' &&
|
|
531
568
|
attachment.mime.startsWith('image/'));
|
|
532
569
|
let runtimeImageIndex = 0;
|
|
533
570
|
let changed = false;
|
|
534
|
-
const mergedAttachments =
|
|
571
|
+
const mergedAttachments = officialAttachments.map((attachment) => {
|
|
535
572
|
if (!isPlainRecord(attachment) ||
|
|
536
573
|
typeof attachment.mime !== 'string' ||
|
|
537
574
|
!attachment.mime.startsWith('image/'))
|
|
@@ -562,7 +599,7 @@ function mergeRuntimeUserImageReferences(officialTurn, runtimeTurn, attachments)
|
|
|
562
599
|
imageIndex: runtimeAttachment.imageIndex
|
|
563
600
|
};
|
|
564
601
|
});
|
|
565
|
-
if (!changed)
|
|
602
|
+
if (!changed && !scheduledSource)
|
|
566
603
|
return officialTurn;
|
|
567
604
|
return {
|
|
568
605
|
...officialTurn,
|
|
@@ -571,7 +608,8 @@ function mergeRuntimeUserImageReferences(officialTurn, runtimeTurn, attachments)
|
|
|
571
608
|
...item,
|
|
572
609
|
payload: {
|
|
573
610
|
...officialPayload,
|
|
574
|
-
|
|
611
|
+
...(scheduledSource ? { source: 'SCHEDULED' } : {}),
|
|
612
|
+
...(changed ? { attachments: mergedAttachments } : {})
|
|
575
613
|
}
|
|
576
614
|
}
|
|
577
615
|
: item)
|
|
@@ -583,6 +621,9 @@ function sameConversationTurn(left, right) {
|
|
|
583
621
|
const leftMessageId = conversationClientMessageId(left);
|
|
584
622
|
return leftMessageId !== null && leftMessageId === conversationClientMessageId(right);
|
|
585
623
|
}
|
|
624
|
+
function terminalConversationTurn(status) {
|
|
625
|
+
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
|
|
626
|
+
}
|
|
586
627
|
function conversationClientMessageId(turn) {
|
|
587
628
|
for (const item of turn.items) {
|
|
588
629
|
if (item.kind !== 'user_message' || !isPlainRecord(item.payload))
|
|
@@ -25,7 +25,8 @@ export class ManagedRunnerSessionService {
|
|
|
25
25
|
if (title === undefined)
|
|
26
26
|
return;
|
|
27
27
|
const updated = this.runtime.setRunnerTitleFromNative(session.id, title);
|
|
28
|
-
if (updated.runnerTitle !== session.runnerTitle
|
|
28
|
+
if (updated.runnerTitle !== session.runnerTitle ||
|
|
29
|
+
updated.titleSource !== session.titleSource)
|
|
29
30
|
this.emitSession(updated);
|
|
30
31
|
}
|
|
31
32
|
catch {
|
|
@@ -174,7 +174,9 @@ export class NativeSessionProjectionService {
|
|
|
174
174
|
titleSource: metadata?.customTitle === null || metadata?.customTitle === undefined
|
|
175
175
|
? runnerTitle === null
|
|
176
176
|
? 'AUTO'
|
|
177
|
-
: '
|
|
177
|
+
: input.titleOrigin === 'PREVIEW'
|
|
178
|
+
? 'AUTO'
|
|
179
|
+
: 'RUNNER'
|
|
178
180
|
: 'CUSTOM',
|
|
179
181
|
lastActivityAt: timestamp,
|
|
180
182
|
createdAt: timestamp
|
|
@@ -35,6 +35,8 @@ export class NodeControlMessageService {
|
|
|
35
35
|
}
|
|
36
36
|
if (this.options.handleRuntimeCredentialResponse(envelope))
|
|
37
37
|
return;
|
|
38
|
+
if (this.options.handleScheduledRunAuthorizeResponse?.(envelope) === true)
|
|
39
|
+
return;
|
|
38
40
|
if (envelope.type.startsWith('direct.') &&
|
|
39
41
|
this.options.handleDirectTransfer?.(envelope) === true)
|
|
40
42
|
return;
|
|
@@ -72,8 +72,19 @@ export class RunEventService {
|
|
|
72
72
|
eventType,
|
|
73
73
|
payload
|
|
74
74
|
};
|
|
75
|
-
if (eventType !== 'text.delta')
|
|
76
|
-
|
|
75
|
+
if (eventType !== 'text.delta') {
|
|
76
|
+
const errorCode = (eventType === 'run.failed' || eventType === 'run.rejected') &&
|
|
77
|
+
isPlainRecord(payload) &&
|
|
78
|
+
typeof payload.code === 'string'
|
|
79
|
+
? payload.code
|
|
80
|
+
: undefined;
|
|
81
|
+
nodeLog('run.event', {
|
|
82
|
+
runId,
|
|
83
|
+
eventType,
|
|
84
|
+
status,
|
|
85
|
+
...(errorCode === undefined ? {} : { errorCode })
|
|
86
|
+
});
|
|
87
|
+
}
|
|
77
88
|
if (status === 'FAILED')
|
|
78
89
|
this.options.metrics.runnerFailed();
|
|
79
90
|
let recorded = undefined;
|
|
@@ -170,6 +181,7 @@ export class RunEventService {
|
|
|
170
181
|
return;
|
|
171
182
|
}
|
|
172
183
|
this.options.scheduleContextRefresh(run.sessionId);
|
|
184
|
+
this.options.onTerminalRun?.(run, finalStatus);
|
|
173
185
|
const idleSnapshot = this.options.refreshWorkspaceSessions(run.workspaceId);
|
|
174
186
|
this.options.runtime.removeTerminalRun(runId);
|
|
175
187
|
if (terminalNotification)
|
|
@@ -195,6 +207,8 @@ export class RunEventService {
|
|
|
195
207
|
const run = this.options.runtime.getRun(runId);
|
|
196
208
|
if (run === undefined)
|
|
197
209
|
return undefined;
|
|
210
|
+
if (run.source === 'SCHEDULED' && eventType === 'approval.request')
|
|
211
|
+
return undefined;
|
|
198
212
|
const session = this.options.runtime.getAgentSession(run.sessionId);
|
|
199
213
|
if (session === undefined)
|
|
200
214
|
return undefined;
|
|
@@ -209,6 +223,8 @@ export class RunEventService {
|
|
|
209
223
|
if (!Number.isSafeInteger(base.userId))
|
|
210
224
|
return undefined;
|
|
211
225
|
if (status !== undefined && isTerminalRunStatus(status)) {
|
|
226
|
+
if (run.source === 'SCHEDULED')
|
|
227
|
+
return undefined;
|
|
212
228
|
if (suppressTerminal)
|
|
213
229
|
return undefined;
|
|
214
230
|
return {
|
|
@@ -105,11 +105,11 @@ export class RunnerInteractionService {
|
|
|
105
105
|
}
|
|
106
106
|
decideCodexApproval(approvalId, decision) {
|
|
107
107
|
const codex = this.options.codex();
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
108
|
+
const approval = codex.execution.approvals.get(approvalId);
|
|
109
|
+
if (approval === undefined)
|
|
110
110
|
return false;
|
|
111
111
|
try {
|
|
112
|
-
codex.respond(requestId, { decision: decision === 'APPROVED' ? 'accept' : 'decline' });
|
|
112
|
+
codex.respond(approval.requestId, { decision: decision === 'APPROVED' ? 'accept' : 'decline' }, approval.runId);
|
|
113
113
|
}
|
|
114
114
|
catch {
|
|
115
115
|
// A disconnected App Server cannot safely receive a late approval.
|
|
@@ -148,7 +148,7 @@ export class RunnerInteractionService {
|
|
|
148
148
|
async cancelCodexTurn(runId, threadId, turnId, cwd, terminalStatus) {
|
|
149
149
|
const codex = this.options.codex();
|
|
150
150
|
try {
|
|
151
|
-
await codex.interruptTurn(threadId, turnId);
|
|
151
|
+
await codex.interruptTurn(threadId, turnId, runId);
|
|
152
152
|
const run = this.options.runtime.getRun(runId);
|
|
153
153
|
if (run !== undefined && !isTerminalRunStatus(run.status))
|
|
154
154
|
this.options.emitRun(runId, 'run.completed', { status: terminalStatus }, terminalStatus);
|
|
@@ -167,7 +167,7 @@ export class RunnerInteractionService {
|
|
|
167
167
|
respondUserInput(runId, requestId, rawAnswers) {
|
|
168
168
|
if (this.options.runners.respondUserInput(runId, requestId, rawAnswers))
|
|
169
169
|
return;
|
|
170
|
-
if (this.options.codex().execution.userInputs.has(requestId)) {
|
|
170
|
+
if (this.options.codex().execution.userInputs.has(`${runId}\0${requestId}`)) {
|
|
171
171
|
this.respondCodexUserInput(runId, requestId, rawAnswers);
|
|
172
172
|
return;
|
|
173
173
|
}
|
|
@@ -209,7 +209,8 @@ export class RunnerInteractionService {
|
|
|
209
209
|
}
|
|
210
210
|
respondCodexUserInput(runId, requestId, rawAnswers) {
|
|
211
211
|
const codex = this.options.codex();
|
|
212
|
-
const
|
|
212
|
+
const pendingKey = `${runId}\0${requestId}`;
|
|
213
|
+
const pending = codex.execution.userInputs.get(pendingKey);
|
|
213
214
|
if (pending === undefined ||
|
|
214
215
|
pending.runId !== runId ||
|
|
215
216
|
this.options.runtime.getRun(runId) === undefined)
|
|
@@ -219,8 +220,8 @@ export class RunnerInteractionService {
|
|
|
219
220
|
answers[question.id] = {
|
|
220
221
|
answers: this.answers(rawAnswers[question.id], question.allowOther, (value) => question.options.includes(value))
|
|
221
222
|
};
|
|
222
|
-
codex.respond(pending.requestId, { answers });
|
|
223
|
-
codex.execution.userInputs.delete(
|
|
223
|
+
codex.respond(pending.requestId, { answers }, runId);
|
|
224
|
+
codex.execution.userInputs.delete(pendingKey);
|
|
224
225
|
this.options.emitItem(runId, {
|
|
225
226
|
itemId: boundedConversationItemId('codex-input', requestId),
|
|
226
227
|
kind: 'user_input_request',
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createEnvelope, scheduledRunAuthorizeRequestSchema, scheduledRunAuthorizeResponseSchema } from '@myagentroam/protocol';
|
|
2
|
+
export class ScheduledRunAuthorizationControl {
|
|
3
|
+
send;
|
|
4
|
+
#pending = new Map();
|
|
5
|
+
#generation = 0;
|
|
6
|
+
#connected = false;
|
|
7
|
+
#closed = false;
|
|
8
|
+
constructor(send) {
|
|
9
|
+
this.send = send;
|
|
10
|
+
}
|
|
11
|
+
connected() {
|
|
12
|
+
this.disconnected();
|
|
13
|
+
this.#connected = true;
|
|
14
|
+
}
|
|
15
|
+
disconnected() {
|
|
16
|
+
this.#connected = false;
|
|
17
|
+
this.#generation++;
|
|
18
|
+
for (const pending of [...this.#pending.values()])
|
|
19
|
+
pending.reject(new Error('SCHEDULE_SERVER_UNAVAILABLE'));
|
|
20
|
+
}
|
|
21
|
+
close() {
|
|
22
|
+
this.#closed = true;
|
|
23
|
+
this.disconnected();
|
|
24
|
+
}
|
|
25
|
+
handle(envelope) {
|
|
26
|
+
if (envelope.type !== 'schedule.authorize.response')
|
|
27
|
+
return false;
|
|
28
|
+
const pending = envelope.replyTo === undefined ? undefined : this.#pending.get(envelope.replyTo);
|
|
29
|
+
if (pending === undefined || pending.generation !== this.#generation)
|
|
30
|
+
return true;
|
|
31
|
+
const parsed = scheduledRunAuthorizeResponseSchema.safeParse(envelope.payload);
|
|
32
|
+
if (!parsed.success)
|
|
33
|
+
pending.reject(new Error('SCHEDULE_AUTH_RESPONSE_INVALID'));
|
|
34
|
+
else
|
|
35
|
+
pending.resolve(parsed.data);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
authorize(request) {
|
|
39
|
+
if (!this.#connected || this.#closed)
|
|
40
|
+
return Promise.reject(new Error('SCHEDULE_SERVER_UNAVAILABLE'));
|
|
41
|
+
const envelope = createEnvelope('schedule.authorize', scheduledRunAuthorizeRequestSchema.parse(request));
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const settle = (action) => {
|
|
44
|
+
const pending = this.#pending.get(envelope.id);
|
|
45
|
+
if (pending === undefined)
|
|
46
|
+
return;
|
|
47
|
+
this.#pending.delete(envelope.id);
|
|
48
|
+
clearTimeout(pending.timer);
|
|
49
|
+
action();
|
|
50
|
+
};
|
|
51
|
+
const timer = setTimeout(() => settle(() => reject(new Error('SCHEDULE_AUTH_TIMEOUT'))), 15_000);
|
|
52
|
+
timer.unref();
|
|
53
|
+
this.#pending.set(envelope.id, {
|
|
54
|
+
generation: this.#generation,
|
|
55
|
+
timer,
|
|
56
|
+
resolve: (response) => settle(() => {
|
|
57
|
+
if ('error' in response)
|
|
58
|
+
reject(new Error(response.error.code));
|
|
59
|
+
else
|
|
60
|
+
resolve(response.context);
|
|
61
|
+
}),
|
|
62
|
+
reject: (error) => settle(() => reject(error))
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
this.send(envelope);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
settle(() => reject(new Error('SCHEDULE_SERVER_UNAVAILABLE')));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export class ScheduledSessionCoordinator {
|
|
2
|
+
options;
|
|
3
|
+
#now;
|
|
4
|
+
#timer;
|
|
5
|
+
#running = false;
|
|
6
|
+
#closed = true;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
this.#now = options.now ?? Date.now;
|
|
10
|
+
}
|
|
11
|
+
start() {
|
|
12
|
+
if (!this.#closed)
|
|
13
|
+
return;
|
|
14
|
+
this.#closed = false;
|
|
15
|
+
void this.#tick(true);
|
|
16
|
+
}
|
|
17
|
+
changed() {
|
|
18
|
+
if (!this.#closed)
|
|
19
|
+
this.#arm();
|
|
20
|
+
}
|
|
21
|
+
stop() {
|
|
22
|
+
this.#closed = true;
|
|
23
|
+
if (this.#timer !== undefined)
|
|
24
|
+
clearTimeout(this.#timer);
|
|
25
|
+
this.#timer = undefined;
|
|
26
|
+
}
|
|
27
|
+
#arm() {
|
|
28
|
+
if (this.#timer !== undefined)
|
|
29
|
+
clearTimeout(this.#timer);
|
|
30
|
+
this.#timer = undefined;
|
|
31
|
+
if (this.#running || this.#closed)
|
|
32
|
+
return;
|
|
33
|
+
const next = this.options.store().nextDueAt();
|
|
34
|
+
if (next === null)
|
|
35
|
+
return;
|
|
36
|
+
const delay = Math.max(0, Math.min(next - this.#now(), 2_147_483_647));
|
|
37
|
+
this.#timer = setTimeout(() => void this.#tick(false), delay);
|
|
38
|
+
this.#timer.unref();
|
|
39
|
+
}
|
|
40
|
+
async #tick(initial) {
|
|
41
|
+
if (this.#closed || this.#running)
|
|
42
|
+
return;
|
|
43
|
+
this.#running = true;
|
|
44
|
+
try {
|
|
45
|
+
const now = this.#now();
|
|
46
|
+
for (const task of this.options.store().listDue(now)) {
|
|
47
|
+
if (this.#closed || task.nextRunAt === null)
|
|
48
|
+
break;
|
|
49
|
+
const occurrenceId = this.options
|
|
50
|
+
.store()
|
|
51
|
+
.claim(task.sessionId, task.nextRunAt, task.revision);
|
|
52
|
+
if (occurrenceId === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
let failureCode;
|
|
55
|
+
let submitting = false;
|
|
56
|
+
try {
|
|
57
|
+
if (initial && task.nextRunAt < now)
|
|
58
|
+
failureCode = 'SCHEDULE_NODE_RESTARTED';
|
|
59
|
+
else if (this.options.busy(task))
|
|
60
|
+
failureCode = 'SCHEDULE_SESSION_BUSY';
|
|
61
|
+
if (failureCode === undefined) {
|
|
62
|
+
const context = await this.options.authorize(task, occurrenceId);
|
|
63
|
+
const currentTask = this.options.store().get(task.sessionId);
|
|
64
|
+
if (this.#closed)
|
|
65
|
+
failureCode = 'SCHEDULE_NODE_STOPPED';
|
|
66
|
+
else if (currentTask?.revision !== task.revision || currentTask.enabled !== true)
|
|
67
|
+
failureCode = 'SCHEDULE_TASK_CHANGED';
|
|
68
|
+
else if (this.options.busy(task))
|
|
69
|
+
failureCode = 'SCHEDULE_SESSION_BUSY';
|
|
70
|
+
else {
|
|
71
|
+
submitting = true;
|
|
72
|
+
const submitted = await this.options.submit(task, occurrenceId, context);
|
|
73
|
+
this.options.store().finish(occurrenceId, 'SUBMITTED', undefined, submitted.runId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
failureCode =
|
|
79
|
+
error instanceof Error && /^[A-Z][A-Z0-9_]{0,127}$/.test(error.message)
|
|
80
|
+
? error.message
|
|
81
|
+
: 'SCHEDULE_EXECUTION_FAILED';
|
|
82
|
+
}
|
|
83
|
+
if (failureCode !== undefined) {
|
|
84
|
+
const status = submitting && failureCode !== 'SCHEDULE_SESSION_BUSY' ? 'FAILED' : 'SKIPPED';
|
|
85
|
+
this.options.store().finish(occurrenceId, status, failureCode);
|
|
86
|
+
this.options.notify(task, occurrenceId, failureCode, status);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
this.#running = false;
|
|
92
|
+
this.#arm();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function parseScheduledCron(cron) {
|
|
2
|
+
const fields = cron.trim().split(/\s+/);
|
|
3
|
+
if (fields.length !== 5 || fields[2] !== '*' || fields[3] !== '*')
|
|
4
|
+
throw new Error('SCHEDULE_CRON_INVALID');
|
|
5
|
+
const minute = boundedInteger(fields[0], 0, 59);
|
|
6
|
+
if (fields[1] === '*' && fields[4] === '*')
|
|
7
|
+
return { kind: 'HOURLY', minute };
|
|
8
|
+
const hour = boundedInteger(fields[1], 0, 23);
|
|
9
|
+
if (fields[4] === '*')
|
|
10
|
+
return { kind: 'DAILY', minute, hour };
|
|
11
|
+
return { kind: 'WEEKLY', minute, hour, weekday: boundedInteger(fields[4], 0, 6) };
|
|
12
|
+
}
|
|
13
|
+
export function assertScheduleTimezone(timezone) {
|
|
14
|
+
if (timezone.length < 1 || timezone.length > 128)
|
|
15
|
+
throw new Error('SCHEDULE_TIMEZONE_INVALID');
|
|
16
|
+
try {
|
|
17
|
+
new Intl.DateTimeFormat('en-US', { timeZone: timezone });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('SCHEDULE_TIMEZONE_INVALID');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function nextScheduledAt(cron, timezone, after) {
|
|
24
|
+
const cadence = parseScheduledCron(cron);
|
|
25
|
+
assertScheduleTimezone(timezone);
|
|
26
|
+
if (!Number.isSafeInteger(after) || after < 0)
|
|
27
|
+
throw new Error('SCHEDULE_TIME_INVALID');
|
|
28
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
29
|
+
timeZone: timezone,
|
|
30
|
+
year: 'numeric',
|
|
31
|
+
month: '2-digit',
|
|
32
|
+
day: '2-digit',
|
|
33
|
+
weekday: 'short',
|
|
34
|
+
hour: '2-digit',
|
|
35
|
+
minute: '2-digit',
|
|
36
|
+
hourCycle: 'h23'
|
|
37
|
+
});
|
|
38
|
+
const previousKey = localKey(formatter, after);
|
|
39
|
+
let candidate = Math.floor(after / 60_000) * 60_000 + 60_000;
|
|
40
|
+
for (let index = 0; index < 8 * 24 * 60 + 120; index++, candidate += 60_000) {
|
|
41
|
+
const local = localParts(formatter, candidate);
|
|
42
|
+
if (local.minute === cadence.minute &&
|
|
43
|
+
(cadence.kind === 'HOURLY' || local.hour === cadence.hour) &&
|
|
44
|
+
(cadence.kind !== 'WEEKLY' || local.weekday === cadence.weekday) &&
|
|
45
|
+
local.key !== previousKey)
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
throw new Error('SCHEDULE_NEXT_TIME_UNAVAILABLE');
|
|
49
|
+
}
|
|
50
|
+
function boundedInteger(value, min, max) {
|
|
51
|
+
if (value === undefined || !/^(0|[1-9][0-9]*)$/.test(value))
|
|
52
|
+
throw new Error('SCHEDULE_CRON_INVALID');
|
|
53
|
+
const number = Number(value);
|
|
54
|
+
if (!Number.isInteger(number) || number < min || number > max)
|
|
55
|
+
throw new Error('SCHEDULE_CRON_INVALID');
|
|
56
|
+
return number;
|
|
57
|
+
}
|
|
58
|
+
function localParts(formatter, time) {
|
|
59
|
+
const fields = Object.fromEntries(formatter.formatToParts(time).map((part) => [part.type, part.value]));
|
|
60
|
+
const hour = Number(fields['hour']);
|
|
61
|
+
const minute = Number(fields['minute']);
|
|
62
|
+
const weekday = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(fields['weekday'] ?? '');
|
|
63
|
+
return {
|
|
64
|
+
hour,
|
|
65
|
+
minute,
|
|
66
|
+
weekday,
|
|
67
|
+
key: `${fields['year']}-${fields['month']}-${fields['day']}T${fields['hour']}:${fields['minute']}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function localKey(formatter, time) {
|
|
71
|
+
return localParts(formatter, time).key;
|
|
72
|
+
}
|