@zq-silk/yui 0.6.9 → 0.6.11
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/README.md +10 -3
- package/dist/cli/commandCatalog.js +1 -1
- package/dist/commands/taskCommands.js +62 -27
- package/dist/commands/taskContextCommand.js +27 -1
- package/dist/commands/taskRoleRuntimeStatus.js +17 -6
- package/dist/controller/agentRuntimeObserver.js +6 -3
- package/dist/controller/controller.js +29 -47
- package/dist/controller/fileSchedulerStoreAdapter.js +546 -255
- package/dist/controller/runtime.js +8 -1
- package/dist/controller/runtimeEventInbox.js +16 -5
- package/dist/controller/runtimeHookRunFence.js +51 -5
- package/dist/controller/runtimeObservationHook.js +8 -2
- package/dist/coordination/workMailbox.js +408 -28
- package/dist/coordination/workMailboxQueue.js +12 -10
- package/dist/executor/agentExecutor.js +102 -94
- package/dist/executor/executorRegistry.js +47 -2
- package/dist/executor/fileRoleLaunchPlanner.js +4 -2
- package/dist/lifecycle/exactRunTerminalization.js +1 -7
- package/dist/repository/taskWorkspaceCoordinator.js +9 -4
- package/dist/runtime/agentDriver.js +83 -4
- package/dist/runtime/agentDriverObservation.js +25 -10
- package/dist/runtime/builtinAgentDrivers.js +168 -18
- package/dist/runtime/codexAppServerRuntime.js +355 -0
- package/dist/runtime/continuationManager.js +117 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/lifecycleReservation.js +4 -3
- package/dist/runtime/promptEnvelope.js +14 -3
- package/dist/runtime/providerContinuation.js +225 -0
- package/dist/runtime/providerContinuationReconciliationService.js +172 -0
- package/dist/runtime/providerRuntimeIdentity.js +232 -0
- package/dist/runtime/providerRuntimeReconciler.js +166 -0
- package/dist/runtime/runtimeContinuationProjection.js +34 -0
- package/dist/runtime/runtimeObservation.js +198 -9
- package/dist/runtime/runtimeProjection.js +162 -7
- package/dist/scheduler/activeRoleRunDelivery.js +314 -1
- package/dist/scheduler/leaderWakeupProcessor.js +1 -1
- package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
- package/dist/scheduler/roleRunLiveness.js +8 -7
- package/dist/scheduler/roleRunStall.js +4 -2
- package/dist/scheduler/taskExecutionProjection.js +2 -2
- package/dist/storage/migration/productionRegistry.js +474 -1
- package/dist/storage/sqliteSchema.js +102 -21
- package/dist/storage/sqliteStore.js +52 -110
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +40 -53
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +233 -4
- package/dist/web/assets/client/i18n.js +166 -2
- package/dist/web/assets/client/view.js +30 -13
- package/dist/web/assets/styles/cards.js +62 -0
- package/dist/web/assets/styles/widgets.js +1 -0
- package/dist/web/webSnapshot.js +11 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +14 -9
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { hasRecentTurnId, rememberRecentTurnId, validatePendingTurnCompletion, validateRecentTurnIds } from "./turnCompletion.js";
|
|
3
3
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
|
|
4
|
+
import { validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
|
|
5
|
+
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
4
6
|
export function createRoleSessionSet(owner, activeAgentId, now) {
|
|
5
7
|
const base = {
|
|
6
8
|
owner: normalizeOwner(owner),
|
|
@@ -12,9 +14,9 @@ export function createRoleSessionSet(owner, activeAgentId, now) {
|
|
|
12
14
|
? { ...base, schemaVersion: 3 }
|
|
13
15
|
: {
|
|
14
16
|
...base,
|
|
15
|
-
schemaVersion:
|
|
17
|
+
schemaVersion: 5,
|
|
16
18
|
inFlight: null,
|
|
17
|
-
|
|
19
|
+
providerBinding: null
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
export function activeRoleAgentSession(set) {
|
|
@@ -157,8 +159,7 @@ export function updateRoleAgentSessionStatus(set, agentId, status, now) {
|
|
|
157
159
|
}
|
|
158
160
|
export function retireTaskRoleSessionsForWorkspace(set, now) {
|
|
159
161
|
validateRoleSessionSet(set);
|
|
160
|
-
if (set.inFlight !== null
|
|
161
|
-
|| set.pendingTurnCompletion !== null) {
|
|
162
|
+
if (set.inFlight !== null) {
|
|
162
163
|
throw new Error("Cannot retire a Task Role session with unsettled Run state.");
|
|
163
164
|
}
|
|
164
165
|
const live = Object.values(set.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
|
|
@@ -183,7 +184,7 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
|
|
|
183
184
|
*/
|
|
184
185
|
export function retireConfirmedAbsentInactiveTaskRolePlaceholders(set, now) {
|
|
185
186
|
validateRoleSessionSet(set);
|
|
186
|
-
if (set.inFlight !== null
|
|
187
|
+
if (set.inFlight !== null) {
|
|
187
188
|
throw new Error("Cannot retire a Task Role placeholder with unsettled Run state.");
|
|
188
189
|
}
|
|
189
190
|
const timestamp = now.toISOString();
|
|
@@ -278,9 +279,6 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
|
278
279
|
if (normalized.agentId !== set.activeAgentId) {
|
|
279
280
|
throw new Error("Task Role Run Agent does not match the active Agent.");
|
|
280
281
|
}
|
|
281
|
-
if (set.pendingTurnCompletion !== null) {
|
|
282
|
-
throw new Error("Task Role session set has an unsettled Turn completion.");
|
|
283
|
-
}
|
|
284
282
|
if (set.inFlight !== null) {
|
|
285
283
|
if (sameRunFence(set.inFlight, normalized))
|
|
286
284
|
return set;
|
|
@@ -294,6 +292,38 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
|
294
292
|
};
|
|
295
293
|
return validateRoleSessionSet(updated);
|
|
296
294
|
}
|
|
295
|
+
export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
|
|
296
|
+
validateRoleSessionSet(set);
|
|
297
|
+
const normalized = validateProviderRuntimeBinding(binding);
|
|
298
|
+
if (set.inFlight === null || set.inFlight.runId !== normalized.runId) {
|
|
299
|
+
throw new Error("Provider Runtime Binding does not match the in-flight Run.");
|
|
300
|
+
}
|
|
301
|
+
if (set.providerBinding !== null) {
|
|
302
|
+
if (JSON.stringify(set.providerBinding) === JSON.stringify(normalized))
|
|
303
|
+
return set;
|
|
304
|
+
throw new Error("Task Role already has a Provider Runtime Binding.");
|
|
305
|
+
}
|
|
306
|
+
return validateRoleSessionSet({
|
|
307
|
+
...set,
|
|
308
|
+
providerBinding: normalized,
|
|
309
|
+
updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
export function updateTaskRoleProviderRuntime(set, binding, updatedAt) {
|
|
313
|
+
validateRoleSessionSet(set);
|
|
314
|
+
const normalized = validateProviderRuntimeBinding(binding);
|
|
315
|
+
if (set.providerBinding === null
|
|
316
|
+
|| normalized.runId !== set.providerBinding.runId
|
|
317
|
+
|| normalized.providerNamespace !== set.providerBinding.providerNamespace
|
|
318
|
+
|| normalized.accountScope !== set.providerBinding.accountScope) {
|
|
319
|
+
throw new Error("Provider Runtime Binding identity cannot change in place.");
|
|
320
|
+
}
|
|
321
|
+
return validateRoleSessionSet({
|
|
322
|
+
...set,
|
|
323
|
+
providerBinding: normalized,
|
|
324
|
+
updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
|
|
325
|
+
});
|
|
326
|
+
}
|
|
297
327
|
export function markTaskRoleRunPushed(set, fence, pushedAt) {
|
|
298
328
|
validateRoleSessionSet(set);
|
|
299
329
|
assertTaskRoleSessionSet(set);
|
|
@@ -332,38 +362,55 @@ export function markTaskRoleRunDelivered(set, fence, deliveredAt) {
|
|
|
332
362
|
};
|
|
333
363
|
return validateRoleSessionSet(updated);
|
|
334
364
|
}
|
|
335
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Records a provider Turn boundary without changing the durable Yui Run.
|
|
367
|
+
*
|
|
368
|
+
* A native session may finish one foreground Turn while provider-owned
|
|
369
|
+
* subagents, mailbox work, or later user corrections still belong to the same
|
|
370
|
+
* application-level Run. Only an explicit Yui workflow outcome may clear the
|
|
371
|
+
* Run fence; this transition merely makes the native session available for a
|
|
372
|
+
* subsequent input and remembers the provider Turn idempotently.
|
|
373
|
+
*/
|
|
374
|
+
export function recordTaskRoleTurnBoundary(set, input, completedAt) {
|
|
336
375
|
validateRoleSessionSet(set);
|
|
337
376
|
assertTaskRoleSessionSet(set);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
377
|
+
const agentId = requireSafeIdentity(input.agentId, "Agent id");
|
|
378
|
+
const nativeSessionId = requireText(input.nativeSessionId, "Native session id");
|
|
379
|
+
const turnId = requireSafeIdentity(input.turnId, "Turn id");
|
|
380
|
+
const session = set.sessions[agentId];
|
|
381
|
+
if (session === undefined || session.nativeSessionId !== nativeSessionId) {
|
|
382
|
+
throw new Error("Completed Turn has no matching Role Agent native session.");
|
|
343
383
|
}
|
|
344
|
-
if (session
|
|
384
|
+
if (session.recentCompletedTurnIds.includes(turnId))
|
|
345
385
|
return set;
|
|
346
|
-
if (set.
|
|
347
|
-
|
|
348
|
-
return set;
|
|
349
|
-
throw new Error("Task Role session set already has a pending Turn completion.");
|
|
350
|
-
}
|
|
351
|
-
const inFlight = set.inFlight;
|
|
352
|
-
if (inFlight === null) {
|
|
353
|
-
throw new Error("Observed Turn has no matching in-flight Run.");
|
|
354
|
-
}
|
|
355
|
-
if (inFlight.agentId !== observed.agentId || inFlight.runId !== observed.runId) {
|
|
356
|
-
throw new Error("Observed Turn Run does not match the in-flight Run.");
|
|
386
|
+
if (set.inFlight !== null && set.inFlight.agentId !== agentId) {
|
|
387
|
+
throw new Error("Completed Turn Agent does not match the in-flight Run.");
|
|
357
388
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
389
|
+
const timestamp = requireDate(completedAt, "Turn completedAt");
|
|
390
|
+
const status = session.status === "stopped" || session.status === "broken"
|
|
391
|
+
? session.status
|
|
392
|
+
: "ready";
|
|
393
|
+
return validateRoleSessionSet({
|
|
362
394
|
...set,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
395
|
+
sessions: {
|
|
396
|
+
...set.sessions,
|
|
397
|
+
[agentId]: {
|
|
398
|
+
...session,
|
|
399
|
+
status,
|
|
400
|
+
recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, turnId),
|
|
401
|
+
updatedAt: timestamp
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
updatedAt: timestamp
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
export function recordObservedTaskRoleCompletion(set, completion) {
|
|
408
|
+
const observed = validatePendingTurnCompletion(completion);
|
|
409
|
+
return recordTaskRoleTurnBoundary(set, {
|
|
410
|
+
agentId: observed.agentId,
|
|
411
|
+
nativeSessionId: observed.nativeSessionId,
|
|
412
|
+
turnId: observed.turnId
|
|
413
|
+
}, new Date(observed.observedAt));
|
|
367
414
|
}
|
|
368
415
|
export function clearTaskRoleRun(set, fence, clearedAt) {
|
|
369
416
|
validateRoleSessionSet(set);
|
|
@@ -373,23 +420,10 @@ export function clearTaskRoleRun(set, fence, clearedAt) {
|
|
|
373
420
|
if (Date.parse(timestamp) < Date.parse(inFlight.preparedAt)) {
|
|
374
421
|
throw new Error("Task Role Run clearedAt must not be earlier than preparedAt.");
|
|
375
422
|
}
|
|
376
|
-
const pending = set.pendingTurnCompletion;
|
|
377
|
-
const session = set.sessions[inFlight.agentId];
|
|
378
|
-
const sessions = pending !== null && session !== undefined
|
|
379
|
-
? {
|
|
380
|
-
...set.sessions,
|
|
381
|
-
[inFlight.agentId]: {
|
|
382
|
-
...session,
|
|
383
|
-
recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, pending.turnId),
|
|
384
|
-
updatedAt: timestamp
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
: set.sessions;
|
|
388
423
|
const updated = {
|
|
389
424
|
...set,
|
|
390
|
-
sessions,
|
|
391
425
|
inFlight: null,
|
|
392
|
-
|
|
426
|
+
providerBinding: null,
|
|
393
427
|
updatedAt: timestamp
|
|
394
428
|
};
|
|
395
429
|
return validateRoleSessionSet(updated);
|
|
@@ -402,18 +436,9 @@ export function clearTaskRoleRun(set, fence, clearedAt) {
|
|
|
402
436
|
export function terminalizeTaskRoleRunSession(set, fence, terminalAt) {
|
|
403
437
|
validateRoleSessionSet(set);
|
|
404
438
|
const inFlight = set.inFlight;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
&& pending.runId === fence.runId
|
|
409
|
-
? settleTaskRoleCompletion(set, {
|
|
410
|
-
agentId: fence.agentId,
|
|
411
|
-
runId: fence.runId,
|
|
412
|
-
turnId: pending.turnId
|
|
413
|
-
}, terminalAt)
|
|
414
|
-
: inFlight === null
|
|
415
|
-
? set
|
|
416
|
-
: clearTaskRoleRun(set, fence, terminalAt);
|
|
439
|
+
let updated = inFlight === null
|
|
440
|
+
? set
|
|
441
|
+
: clearTaskRoleRun(set, fence, terminalAt);
|
|
417
442
|
const session = updated.sessions[updated.activeAgentId];
|
|
418
443
|
if (session?.status === "running") {
|
|
419
444
|
updated = updateRoleAgentSessionStatus(updated, updated.activeAgentId, "ready", terminalAt);
|
|
@@ -441,7 +466,7 @@ export function resetTaskRoleSession(set, now) {
|
|
|
441
466
|
sessions,
|
|
442
467
|
...(history === undefined ? {} : { history }),
|
|
443
468
|
inFlight: null,
|
|
444
|
-
|
|
469
|
+
providerBinding: null,
|
|
445
470
|
updatedAt: timestamp
|
|
446
471
|
});
|
|
447
472
|
}
|
|
@@ -451,38 +476,27 @@ export function settleTaskRoleCompletion(set, expected, settledAt) {
|
|
|
451
476
|
const agentId = requireSafeIdentity(expected.agentId, "Agent id");
|
|
452
477
|
const runId = requireSafeIdentity(expected.runId, "Run id");
|
|
453
478
|
const turnId = requireSafeIdentity(expected.turnId, "Turn id");
|
|
454
|
-
const pending = set.pendingTurnCompletion;
|
|
455
|
-
if (pending === null)
|
|
456
|
-
throw new Error("Task Role session set has no pending Turn completion.");
|
|
457
|
-
if (pending.agentId !== agentId
|
|
458
|
-
|| pending.runId !== runId
|
|
459
|
-
|| pending.turnId !== turnId) {
|
|
460
|
-
throw new Error("Pending Turn completion does not match the expected Turn.");
|
|
461
|
-
}
|
|
462
479
|
const inFlight = set.inFlight;
|
|
463
480
|
if (inFlight === null || inFlight.agentId !== agentId || inFlight.runId !== runId) {
|
|
464
481
|
throw new Error("Pending Turn completion does not match the in-flight Run.");
|
|
465
482
|
}
|
|
466
483
|
const session = set.sessions[agentId];
|
|
467
|
-
if (session === undefined || session.
|
|
468
|
-
throw new Error("
|
|
484
|
+
if (session === undefined || !session.recentCompletedTurnIds.includes(turnId)) {
|
|
485
|
+
throw new Error("Turn completion has not been observed for the Role Agent session.");
|
|
469
486
|
}
|
|
470
487
|
const timestamp = requireDate(settledAt, "Turn settledAt");
|
|
471
|
-
if (Date.parse(timestamp) < Date.parse(pending.observedAt)) {
|
|
472
|
-
throw new Error("Turn settledAt must not be earlier than observedAt.");
|
|
473
|
-
}
|
|
474
488
|
const updated = {
|
|
475
489
|
...set,
|
|
476
490
|
sessions: {
|
|
477
491
|
...set.sessions,
|
|
478
492
|
[agentId]: {
|
|
479
493
|
...session,
|
|
480
|
-
recentCompletedTurnIds:
|
|
494
|
+
recentCompletedTurnIds: session.recentCompletedTurnIds,
|
|
481
495
|
updatedAt: timestamp
|
|
482
496
|
}
|
|
483
497
|
},
|
|
484
498
|
inFlight: null,
|
|
485
|
-
|
|
499
|
+
providerBinding: null,
|
|
486
500
|
updatedAt: timestamp
|
|
487
501
|
};
|
|
488
502
|
return validateRoleSessionSet(updated);
|
|
@@ -498,7 +512,7 @@ export function validateRoleSessionSet(set) {
|
|
|
498
512
|
throw new Error("Global Role session set schema version is invalid.");
|
|
499
513
|
}
|
|
500
514
|
if (Object.hasOwn(set, "inFlight")
|
|
501
|
-
|| Object.hasOwn(set, "
|
|
515
|
+
|| Object.hasOwn(set, "providerBinding")) {
|
|
502
516
|
throw new Error("Global Role session set must not contain Task Role lifecycle fields.");
|
|
503
517
|
}
|
|
504
518
|
const history = set.history;
|
|
@@ -513,11 +527,11 @@ export function validateRoleSessionSet(set) {
|
|
|
513
527
|
}
|
|
514
528
|
}
|
|
515
529
|
else {
|
|
516
|
-
if (set.schemaVersion !==
|
|
530
|
+
if (set.schemaVersion !== 5) {
|
|
517
531
|
throw new Error("Task Role session set schema version is invalid.");
|
|
518
532
|
}
|
|
519
533
|
if (!Object.hasOwn(set, "inFlight")
|
|
520
|
-
|| !Object.hasOwn(set, "
|
|
534
|
+
|| !Object.hasOwn(set, "providerBinding")) {
|
|
521
535
|
throw new Error("Task Role session set must contain its Turn fence.");
|
|
522
536
|
}
|
|
523
537
|
const taskSet = set;
|
|
@@ -546,28 +560,22 @@ export function validateRoleSessionSet(set) {
|
|
|
546
560
|
const inFlight = taskSet.inFlight === null
|
|
547
561
|
? null
|
|
548
562
|
: validateTaskRoleInFlight(taskSet.inFlight);
|
|
549
|
-
const
|
|
563
|
+
const providerBinding = taskSet.providerBinding === null
|
|
550
564
|
? null
|
|
551
|
-
:
|
|
552
|
-
if (inFlight === null && pending !== null) {
|
|
553
|
-
throw new Error("Pending Turn completion requires an in-flight Run.");
|
|
554
|
-
}
|
|
565
|
+
: validateProviderRuntimeBinding(taskSet.providerBinding);
|
|
555
566
|
if (inFlight !== null && inFlight.agentId !== set.activeAgentId) {
|
|
556
567
|
throw new Error("Task Role in-flight Run Agent must be active.");
|
|
557
568
|
}
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|| inFlight.runId !== pending.runId
|
|
562
|
-
|| inFlight.pushedAt === undefined) {
|
|
563
|
-
throw new Error("Pending Turn completion must match a pushed in-flight Run.");
|
|
569
|
+
if (providerBinding !== null) {
|
|
570
|
+
if (inFlight === null || providerBinding.runId !== inFlight.runId) {
|
|
571
|
+
throw new Error("Provider Runtime Binding must match the in-flight Run.");
|
|
564
572
|
}
|
|
565
|
-
const session = taskSet.sessions[
|
|
573
|
+
const session = taskSet.sessions[inFlight.agentId];
|
|
566
574
|
if (session === undefined) {
|
|
567
|
-
throw new Error("
|
|
575
|
+
throw new Error("Provider Runtime Binding has no active Role Agent session.");
|
|
568
576
|
}
|
|
569
|
-
if (
|
|
570
|
-
throw new Error("
|
|
577
|
+
if (providerBinding.providerNamespace !== builtinDriverIdForAdapter(session.adapterId)) {
|
|
578
|
+
throw new Error("Provider Runtime Binding namespace does not match the Agent adapter.");
|
|
571
579
|
}
|
|
572
580
|
}
|
|
573
581
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { createPromptEnvelope, createSessionLaunchRequest } from "../runtime/index.js";
|
|
2
|
+
import { createPromptEnvelope, createRuntimeBinding, createSessionLaunchRequest } from "../runtime/index.js";
|
|
3
|
+
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
3
4
|
/**
|
|
4
5
|
* rr13/test: Test-only liveness seam. Integration tests that spawn a real
|
|
5
6
|
* Controller subprocess cannot inject a fake TmuxDeliveryPort, and a saved
|
|
@@ -25,6 +26,9 @@ export class ExecutorRegistry {
|
|
|
25
26
|
this.readiness = readiness;
|
|
26
27
|
this.runtimePorts = runtimePorts;
|
|
27
28
|
}
|
|
29
|
+
canRouteProviderInput(adapterId) {
|
|
30
|
+
return adapterId === "codex" && this.runtimePorts?.providerInputRouting !== undefined;
|
|
31
|
+
}
|
|
28
32
|
async prepareRoleSession(input) {
|
|
29
33
|
if (input.mode === "resume" && !hasText(input.nativeSessionId)) {
|
|
30
34
|
throw new Error("Role session resume requires a native session id.");
|
|
@@ -173,7 +177,7 @@ export class ExecutorRegistry {
|
|
|
173
177
|
envelope: createPromptEnvelope({
|
|
174
178
|
id: input.receiptId,
|
|
175
179
|
source: {
|
|
176
|
-
kind: "agent-run",
|
|
180
|
+
kind: input.receiptId === formatAgentRunReceiptId(input.delivery.prepared.taskId, runId) ? "agent-run" : "run-input",
|
|
177
181
|
taskId: input.delivery.prepared.taskId,
|
|
178
182
|
localId: runId
|
|
179
183
|
},
|
|
@@ -192,6 +196,47 @@ export class ExecutorRegistry {
|
|
|
192
196
|
}
|
|
193
197
|
return outcome;
|
|
194
198
|
}
|
|
199
|
+
async routeProviderInput(input) {
|
|
200
|
+
const prepared = this.requirePrepared(input.delivery.prepared);
|
|
201
|
+
if (prepared.binding === undefined || this.runtimePorts?.providerInputRouting === undefined) {
|
|
202
|
+
return "unavailable";
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
return await this.runtimePorts.providerInputRouting.route({
|
|
206
|
+
binding: prepared.binding,
|
|
207
|
+
attemptId: input.attemptId,
|
|
208
|
+
mode: input.mode,
|
|
209
|
+
text: input.text,
|
|
210
|
+
fence: input.fence
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
// A routed mutation is fenced by its durable inputDelivery, not by this
|
|
215
|
+
// process-local preparation. Never let a later Turn reuse a cached
|
|
216
|
+
// Activation binding after this attempt (including an unknown result).
|
|
217
|
+
this.#prepared.delete(input.delivery.prepared.deliveryId);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async reconcileProviderInput(input) {
|
|
221
|
+
if (this.runtimePorts?.providerInputRouting === undefined) {
|
|
222
|
+
return "unavailable";
|
|
223
|
+
}
|
|
224
|
+
return this.runtimePorts.providerInputRouting.reconcile({
|
|
225
|
+
binding: createRuntimeBinding({
|
|
226
|
+
id: `metadata:${input.taskId}:${input.roleName}:${input.launchId}`,
|
|
227
|
+
launchId: input.launchId,
|
|
228
|
+
owner: { scope: "task", taskId: input.taskId, roleName: input.roleName },
|
|
229
|
+
agentId: input.agentId,
|
|
230
|
+
adapterId: input.adapterId,
|
|
231
|
+
hostRef: "metadata-only",
|
|
232
|
+
hostCreated: false,
|
|
233
|
+
nativeSessionId: input.nativeSessionId
|
|
234
|
+
}),
|
|
235
|
+
attemptId: input.attemptId,
|
|
236
|
+
mode: input.mode,
|
|
237
|
+
fence: input.fence
|
|
238
|
+
});
|
|
239
|
+
}
|
|
195
240
|
async notifyOperatorInputOnce(input) {
|
|
196
241
|
const probe = this.readiness(input.adapterId, "operator");
|
|
197
242
|
return this.tmux.sendRoleInputOnceIfReadyAsync === undefined
|
|
@@ -384,7 +384,8 @@ export class FileRoleLaunchPlanner {
|
|
|
384
384
|
args = addCodexLifecycleHooks(args, launchMode, this.#cliPath);
|
|
385
385
|
// End option parsing before the opaque prompt so a wakeup beginning
|
|
386
386
|
// with '-' can never be reinterpreted as a Codex CLI flag.
|
|
387
|
-
|
|
387
|
+
if (managedRun.pushedAt === undefined)
|
|
388
|
+
args.push("--", managedRun.input);
|
|
388
389
|
}
|
|
389
390
|
session = launchMode === "resume"
|
|
390
391
|
? readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective)
|
|
@@ -503,7 +504,8 @@ export class FileRoleLaunchPlanner {
|
|
|
503
504
|
},
|
|
504
505
|
launch: scopedLaunch,
|
|
505
506
|
session,
|
|
506
|
-
...((binding.adapterId === "codex"
|
|
507
|
+
...((binding.adapterId === "codex" && managedRun?.pushedAt === undefined
|
|
508
|
+
|| managedClaudeRun) && input.runId !== undefined
|
|
507
509
|
? { initialPromptRunId: input.runId }
|
|
508
510
|
: {})
|
|
509
511
|
};
|
|
@@ -253,7 +253,7 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
253
253
|
}
|
|
254
254
|
else {
|
|
255
255
|
const mailboxSettlement = settleExactWorkExecution(store, roleTarget, { type: "run", taskId: run.taskId, id: run.id });
|
|
256
|
-
if (mailboxSettlement === "absent" && isReview) {
|
|
256
|
+
if (mailboxSettlement === "absent" && isReview && run.deliveredAt === undefined) {
|
|
257
257
|
return obsolete(run, "review-mailbox-missing");
|
|
258
258
|
}
|
|
259
259
|
}
|
|
@@ -545,12 +545,6 @@ function matchesSessionFence(sessions, input) {
|
|
|
545
545
|
if (session?.nativeSessionId !== input.nativeSessionId)
|
|
546
546
|
return false;
|
|
547
547
|
}
|
|
548
|
-
const pending = sessions.pendingTurnCompletion;
|
|
549
|
-
if (pending !== null && (pending.agentId !== input.agentId
|
|
550
|
-
|| pending.runId !== input.runId
|
|
551
|
-
|| (input.nativeSessionId !== undefined
|
|
552
|
-
&& pending.nativeSessionId !== input.nativeSessionId)))
|
|
553
|
-
return false;
|
|
554
548
|
const inFlight = sessions.inFlight;
|
|
555
549
|
return inFlight === null || (inFlight.agentId === input.agentId
|
|
556
550
|
&& inFlight.runId === input.runId
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { retireConfirmedAbsentInactiveTaskRolePlaceholders } from "../executor/agentExecutor.js";
|
|
3
3
|
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
|
+
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
4
5
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
5
6
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
6
7
|
import { WorkspaceCleanupBlockedError } from "./taskWorkspacePreparer.js";
|
|
@@ -148,6 +149,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
148
149
|
if (task.status !== "completed" && task.status !== "retired") {
|
|
149
150
|
throw new Error(`Task must be completed or retired before archive cleanup: ${task.id}.`);
|
|
150
151
|
}
|
|
152
|
+
this.#assertNoProviderContinuationWriters(task.id);
|
|
151
153
|
const managedWorkspaces = [...this.store.listManagedWorkspaces(task.id)]
|
|
152
154
|
.sort((left, right) => managedWorkspaceKey(left.owner)
|
|
153
155
|
.localeCompare(managedWorkspaceKey(right.owner)));
|
|
@@ -314,6 +316,13 @@ export class TaskWorkspaceCoordinator {
|
|
|
314
316
|
if (current === null || !isDeepStrictEqual(current, expected)) {
|
|
315
317
|
throw new WorkspaceCleanupBlockedError("task-changed", `task:${expected.id}`, true, `Task changed during archive cleanup: ${expected.id}.`);
|
|
316
318
|
}
|
|
319
|
+
this.#assertNoProviderContinuationWriters(expected.id);
|
|
320
|
+
}
|
|
321
|
+
#assertNoProviderContinuationWriters(taskId) {
|
|
322
|
+
const blockers = blockingProviderContinuations(this.store.listEvents(taskId));
|
|
323
|
+
if (blockers.length > 0) {
|
|
324
|
+
throw new WorkspaceCleanupBlockedError("active-run", `task:${taskId}`, true, `Task has Provider continuations that may still write its Workspace: ${taskId}.`);
|
|
325
|
+
}
|
|
317
326
|
}
|
|
318
327
|
#assertWorkItemRuntimeQuiescent(item) {
|
|
319
328
|
const activeRun = this.store.listAgentRuns(item.taskId)
|
|
@@ -367,10 +376,6 @@ export class TaskWorkspaceCoordinator {
|
|
|
367
376
|
if (sessions?.inFlight !== null && sessions?.inFlight !== undefined) {
|
|
368
377
|
throw new Error(`Role has unsettled Run state: ${taskId}/${roleName}.`);
|
|
369
378
|
}
|
|
370
|
-
if (sessions?.pendingTurnCompletion !== null
|
|
371
|
-
&& sessions?.pendingTurnCompletion !== undefined) {
|
|
372
|
-
throw new Error(`Role has unsettled Run state: ${taskId}/${roleName}.`);
|
|
373
|
-
}
|
|
374
379
|
if (this.store.getWorkMailbox !== undefined && hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
|
|
375
380
|
throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
|
|
376
381
|
}
|
|
@@ -13,6 +13,18 @@ const DELIVERY_MODES = [
|
|
|
13
13
|
"best-effort",
|
|
14
14
|
"host-only"
|
|
15
15
|
];
|
|
16
|
+
function requireCapabilityObject(value, label) {
|
|
17
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
18
|
+
throw new Error(`Agent Driver ${label} capabilities must be an object.`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function evidenceQuality(value, label) {
|
|
23
|
+
if (value !== "exact" && value !== "partial" && value !== "unavailable") {
|
|
24
|
+
throw new Error(`${label} capability is invalid.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
16
28
|
export function validateAgentDriverCapabilities(input) {
|
|
17
29
|
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
18
30
|
throw new Error("Agent Driver capabilities must be an object.");
|
|
@@ -56,6 +68,28 @@ export function validateAgentDriverCapabilities(input) {
|
|
|
56
68
|
if (!DELIVERY_MODES.includes(observation.delivery)) {
|
|
57
69
|
throw new Error("Agent Driver delivery capability is invalid.");
|
|
58
70
|
}
|
|
71
|
+
const conversation = requireCapabilityObject(input.conversation, "conversation");
|
|
72
|
+
evidenceQuality(conversation.persistentIdentity, "Provider Conversation identity");
|
|
73
|
+
evidenceQuality(conversation.readback, "Provider Conversation readback");
|
|
74
|
+
if (typeof conversation.crossProcessResume !== "boolean") {
|
|
75
|
+
throw new Error("Agent Driver Conversation resume capability must be boolean.");
|
|
76
|
+
}
|
|
77
|
+
const inputRouting = requireCapabilityObject(input.input, "input");
|
|
78
|
+
if (typeof inputRouting.startTurn !== "boolean"
|
|
79
|
+
|| !["fenced", "unavailable"].includes(String(inputRouting.steer))
|
|
80
|
+
|| !["fenced", "unavailable"].includes(String(inputRouting.inject))
|
|
81
|
+
|| !["exact", "unavailable"].includes(String(inputRouting.acceptance))
|
|
82
|
+
|| !["exact", "unavailable"].includes(String(inputRouting.idempotency))) {
|
|
83
|
+
throw new Error("Agent Driver input routing capabilities are invalid.");
|
|
84
|
+
}
|
|
85
|
+
const descendants = requireCapabilityObject(input.descendants, "descendants");
|
|
86
|
+
evidenceQuality(descendants.lineage, "Provider descendant lineage");
|
|
87
|
+
evidenceQuality(descendants.detachedQuery, "Provider detached descendant query");
|
|
88
|
+
evidenceQuality(descendants.resultRouting, "Provider descendant result routing");
|
|
89
|
+
const bounded = requireCapabilityObject(input.bounded, "bounded");
|
|
90
|
+
if (typeof bounded.structuredTerminal !== "boolean") {
|
|
91
|
+
throw new Error("Agent Driver bounded terminal capability must be boolean.");
|
|
92
|
+
}
|
|
59
93
|
return Object.freeze({
|
|
60
94
|
surfaces: Object.freeze(surfaces),
|
|
61
95
|
control: Object.freeze({
|
|
@@ -65,6 +99,24 @@ export function validateAgentDriverCapabilities(input) {
|
|
|
65
99
|
interrupt: control.interrupt,
|
|
66
100
|
stop: control.stop
|
|
67
101
|
}),
|
|
102
|
+
conversation: Object.freeze({
|
|
103
|
+
persistentIdentity: conversation.persistentIdentity,
|
|
104
|
+
crossProcessResume: conversation.crossProcessResume,
|
|
105
|
+
readback: conversation.readback
|
|
106
|
+
}),
|
|
107
|
+
input: Object.freeze({
|
|
108
|
+
startTurn: inputRouting.startTurn,
|
|
109
|
+
steer: inputRouting.steer,
|
|
110
|
+
inject: inputRouting.inject,
|
|
111
|
+
acceptance: inputRouting.acceptance,
|
|
112
|
+
idempotency: inputRouting.idempotency
|
|
113
|
+
}),
|
|
114
|
+
descendants: Object.freeze({
|
|
115
|
+
lineage: descendants.lineage,
|
|
116
|
+
detachedQuery: descendants.detachedQuery,
|
|
117
|
+
resultRouting: descendants.resultRouting
|
|
118
|
+
}),
|
|
119
|
+
bounded: Object.freeze({ structuredTerminal: bounded.structuredTerminal }),
|
|
68
120
|
observation: Object.freeze({
|
|
69
121
|
sessionIdentity: observation.sessionIdentity,
|
|
70
122
|
sessionBootstrap: observation.sessionBootstrap,
|
|
@@ -83,17 +135,19 @@ export function managedRuntimeAdmission(capabilities) {
|
|
|
83
135
|
const missing = [];
|
|
84
136
|
if (!actual.control.start)
|
|
85
137
|
missing.push("start");
|
|
86
|
-
if (!actual.control.resume)
|
|
138
|
+
if (!actual.control.resume || !actual.conversation.crossProcessResume)
|
|
87
139
|
missing.push("resume");
|
|
88
|
-
if (!actual.control.sendTurn)
|
|
140
|
+
if (!actual.control.sendTurn || !actual.input.startTurn)
|
|
89
141
|
missing.push("send-turn");
|
|
90
142
|
if (!actual.control.interrupt)
|
|
91
143
|
missing.push("interrupt");
|
|
92
144
|
if (!actual.control.stop)
|
|
93
145
|
missing.push("stop");
|
|
94
|
-
if (actual.observation.sessionIdentity !== "exact"
|
|
146
|
+
if (actual.observation.sessionIdentity !== "exact"
|
|
147
|
+
|| actual.conversation.persistentIdentity !== "exact")
|
|
95
148
|
missing.push("exact-session-identity");
|
|
96
|
-
if (actual.observation.promptAcceptance !== "exact"
|
|
149
|
+
if (actual.observation.promptAcceptance !== "exact"
|
|
150
|
+
|| actual.input.acceptance !== "exact")
|
|
97
151
|
missing.push("exact-prompt-acceptance");
|
|
98
152
|
if (actual.observation.turnLifecycle !== "exact")
|
|
99
153
|
missing.push("exact-turn-lifecycle");
|
|
@@ -101,6 +155,17 @@ export function managedRuntimeAdmission(capabilities) {
|
|
|
101
155
|
? Object.freeze({ admitted: true })
|
|
102
156
|
: Object.freeze({ admitted: false, missing: Object.freeze(missing) });
|
|
103
157
|
}
|
|
158
|
+
export function boundedRuntimeAdmission(capabilities) {
|
|
159
|
+
const actual = validateAgentDriverCapabilities(capabilities);
|
|
160
|
+
const missing = [];
|
|
161
|
+
if (!actual.control.start)
|
|
162
|
+
missing.push("start");
|
|
163
|
+
if (!actual.bounded.structuredTerminal)
|
|
164
|
+
missing.push("structured-terminal");
|
|
165
|
+
return missing.length === 0
|
|
166
|
+
? Object.freeze({ admitted: true })
|
|
167
|
+
: Object.freeze({ admitted: false, missing: Object.freeze(missing) });
|
|
168
|
+
}
|
|
104
169
|
export class AgentDriverRegistry {
|
|
105
170
|
#drivers = new Map();
|
|
106
171
|
#driverIdsByAdapter = new Map();
|
|
@@ -194,8 +259,22 @@ export function normalizeAgentDriverHookClassification(input) {
|
|
|
194
259
|
if (input.terminal !== undefined && typeof input.terminal !== "boolean") {
|
|
195
260
|
throw new Error("Agent Driver Hook terminal classification is invalid.");
|
|
196
261
|
}
|
|
262
|
+
if (input.continuationId !== undefined) {
|
|
263
|
+
requireText(input.continuationId, "Agent Driver Hook continuation id");
|
|
264
|
+
if (!Number.isSafeInteger(input.continuationGeneration)
|
|
265
|
+
|| input.continuationGeneration < 1) {
|
|
266
|
+
throw new Error("Agent Driver Hook continuation generation is invalid.");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (input.continuationGeneration !== undefined) {
|
|
270
|
+
throw new Error("Agent Driver Hook continuation generation requires an id.");
|
|
271
|
+
}
|
|
197
272
|
return Object.freeze({
|
|
198
273
|
...(input.startupSession === undefined ? {} : { startupSession: input.startupSession }),
|
|
274
|
+
...(input.continuationId === undefined ? {} : {
|
|
275
|
+
continuationId: input.continuationId,
|
|
276
|
+
continuationGeneration: input.continuationGeneration
|
|
277
|
+
}),
|
|
199
278
|
terminal: input.terminal ?? false
|
|
200
279
|
});
|
|
201
280
|
}
|