@zq-silk/yui 0.6.8 → 0.6.10
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 +63 -28
- 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 +572 -258
- 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 +101 -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 +217 -6
- package/dist/runtime/runtimeProjection.js +172 -11
- package/dist/scheduler/activeRoleRunDelivery.js +314 -1
- package/dist/scheduler/leaderWakeupProcessor.js +2 -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/task/nextAction.js +1 -1
- 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 +33 -24
- package/skills/yui-operator/SKILL.md +7 -5
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
export function createRoleSessionSet(owner, activeAgentId, now) {
|
|
5
6
|
const base = {
|
|
6
7
|
owner: normalizeOwner(owner),
|
|
@@ -12,9 +13,9 @@ export function createRoleSessionSet(owner, activeAgentId, now) {
|
|
|
12
13
|
? { ...base, schemaVersion: 3 }
|
|
13
14
|
: {
|
|
14
15
|
...base,
|
|
15
|
-
schemaVersion:
|
|
16
|
+
schemaVersion: 5,
|
|
16
17
|
inFlight: null,
|
|
17
|
-
|
|
18
|
+
providerBinding: null
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
export function activeRoleAgentSession(set) {
|
|
@@ -157,8 +158,7 @@ export function updateRoleAgentSessionStatus(set, agentId, status, now) {
|
|
|
157
158
|
}
|
|
158
159
|
export function retireTaskRoleSessionsForWorkspace(set, now) {
|
|
159
160
|
validateRoleSessionSet(set);
|
|
160
|
-
if (set.inFlight !== null
|
|
161
|
-
|| set.pendingTurnCompletion !== null) {
|
|
161
|
+
if (set.inFlight !== null) {
|
|
162
162
|
throw new Error("Cannot retire a Task Role session with unsettled Run state.");
|
|
163
163
|
}
|
|
164
164
|
const live = Object.values(set.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
|
|
@@ -183,7 +183,7 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
|
|
|
183
183
|
*/
|
|
184
184
|
export function retireConfirmedAbsentInactiveTaskRolePlaceholders(set, now) {
|
|
185
185
|
validateRoleSessionSet(set);
|
|
186
|
-
if (set.inFlight !== null
|
|
186
|
+
if (set.inFlight !== null) {
|
|
187
187
|
throw new Error("Cannot retire a Task Role placeholder with unsettled Run state.");
|
|
188
188
|
}
|
|
189
189
|
const timestamp = now.toISOString();
|
|
@@ -278,9 +278,6 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
|
278
278
|
if (normalized.agentId !== set.activeAgentId) {
|
|
279
279
|
throw new Error("Task Role Run Agent does not match the active Agent.");
|
|
280
280
|
}
|
|
281
|
-
if (set.pendingTurnCompletion !== null) {
|
|
282
|
-
throw new Error("Task Role session set has an unsettled Turn completion.");
|
|
283
|
-
}
|
|
284
281
|
if (set.inFlight !== null) {
|
|
285
282
|
if (sameRunFence(set.inFlight, normalized))
|
|
286
283
|
return set;
|
|
@@ -294,6 +291,38 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
|
294
291
|
};
|
|
295
292
|
return validateRoleSessionSet(updated);
|
|
296
293
|
}
|
|
294
|
+
export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
|
|
295
|
+
validateRoleSessionSet(set);
|
|
296
|
+
const normalized = validateProviderRuntimeBinding(binding);
|
|
297
|
+
if (set.inFlight === null || set.inFlight.runId !== normalized.runId) {
|
|
298
|
+
throw new Error("Provider Runtime Binding does not match the in-flight Run.");
|
|
299
|
+
}
|
|
300
|
+
if (set.providerBinding !== null) {
|
|
301
|
+
if (JSON.stringify(set.providerBinding) === JSON.stringify(normalized))
|
|
302
|
+
return set;
|
|
303
|
+
throw new Error("Task Role already has a Provider Runtime Binding.");
|
|
304
|
+
}
|
|
305
|
+
return validateRoleSessionSet({
|
|
306
|
+
...set,
|
|
307
|
+
providerBinding: normalized,
|
|
308
|
+
updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
export function updateTaskRoleProviderRuntime(set, binding, updatedAt) {
|
|
312
|
+
validateRoleSessionSet(set);
|
|
313
|
+
const normalized = validateProviderRuntimeBinding(binding);
|
|
314
|
+
if (set.providerBinding === null
|
|
315
|
+
|| normalized.runId !== set.providerBinding.runId
|
|
316
|
+
|| normalized.providerNamespace !== set.providerBinding.providerNamespace
|
|
317
|
+
|| normalized.accountScope !== set.providerBinding.accountScope) {
|
|
318
|
+
throw new Error("Provider Runtime Binding identity cannot change in place.");
|
|
319
|
+
}
|
|
320
|
+
return validateRoleSessionSet({
|
|
321
|
+
...set,
|
|
322
|
+
providerBinding: normalized,
|
|
323
|
+
updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
|
|
324
|
+
});
|
|
325
|
+
}
|
|
297
326
|
export function markTaskRoleRunPushed(set, fence, pushedAt) {
|
|
298
327
|
validateRoleSessionSet(set);
|
|
299
328
|
assertTaskRoleSessionSet(set);
|
|
@@ -332,38 +361,55 @@ export function markTaskRoleRunDelivered(set, fence, deliveredAt) {
|
|
|
332
361
|
};
|
|
333
362
|
return validateRoleSessionSet(updated);
|
|
334
363
|
}
|
|
335
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Records a provider Turn boundary without changing the durable Yui Run.
|
|
366
|
+
*
|
|
367
|
+
* A native session may finish one foreground Turn while provider-owned
|
|
368
|
+
* subagents, mailbox work, or later user corrections still belong to the same
|
|
369
|
+
* application-level Run. Only an explicit Yui workflow outcome may clear the
|
|
370
|
+
* Run fence; this transition merely makes the native session available for a
|
|
371
|
+
* subsequent input and remembers the provider Turn idempotently.
|
|
372
|
+
*/
|
|
373
|
+
export function recordTaskRoleTurnBoundary(set, input, completedAt) {
|
|
336
374
|
validateRoleSessionSet(set);
|
|
337
375
|
assertTaskRoleSessionSet(set);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
376
|
+
const agentId = requireSafeIdentity(input.agentId, "Agent id");
|
|
377
|
+
const nativeSessionId = requireText(input.nativeSessionId, "Native session id");
|
|
378
|
+
const turnId = requireSafeIdentity(input.turnId, "Turn id");
|
|
379
|
+
const session = set.sessions[agentId];
|
|
380
|
+
if (session === undefined || session.nativeSessionId !== nativeSessionId) {
|
|
381
|
+
throw new Error("Completed Turn has no matching Role Agent native session.");
|
|
343
382
|
}
|
|
344
|
-
if (session
|
|
383
|
+
if (session.recentCompletedTurnIds.includes(turnId))
|
|
345
384
|
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.");
|
|
385
|
+
if (set.inFlight !== null && set.inFlight.agentId !== agentId) {
|
|
386
|
+
throw new Error("Completed Turn Agent does not match the in-flight Run.");
|
|
357
387
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
388
|
+
const timestamp = requireDate(completedAt, "Turn completedAt");
|
|
389
|
+
const status = session.status === "stopped" || session.status === "broken"
|
|
390
|
+
? session.status
|
|
391
|
+
: "ready";
|
|
392
|
+
return validateRoleSessionSet({
|
|
362
393
|
...set,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
394
|
+
sessions: {
|
|
395
|
+
...set.sessions,
|
|
396
|
+
[agentId]: {
|
|
397
|
+
...session,
|
|
398
|
+
status,
|
|
399
|
+
recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, turnId),
|
|
400
|
+
updatedAt: timestamp
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
updatedAt: timestamp
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
export function recordObservedTaskRoleCompletion(set, completion) {
|
|
407
|
+
const observed = validatePendingTurnCompletion(completion);
|
|
408
|
+
return recordTaskRoleTurnBoundary(set, {
|
|
409
|
+
agentId: observed.agentId,
|
|
410
|
+
nativeSessionId: observed.nativeSessionId,
|
|
411
|
+
turnId: observed.turnId
|
|
412
|
+
}, new Date(observed.observedAt));
|
|
367
413
|
}
|
|
368
414
|
export function clearTaskRoleRun(set, fence, clearedAt) {
|
|
369
415
|
validateRoleSessionSet(set);
|
|
@@ -373,23 +419,10 @@ export function clearTaskRoleRun(set, fence, clearedAt) {
|
|
|
373
419
|
if (Date.parse(timestamp) < Date.parse(inFlight.preparedAt)) {
|
|
374
420
|
throw new Error("Task Role Run clearedAt must not be earlier than preparedAt.");
|
|
375
421
|
}
|
|
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
422
|
const updated = {
|
|
389
423
|
...set,
|
|
390
|
-
sessions,
|
|
391
424
|
inFlight: null,
|
|
392
|
-
|
|
425
|
+
providerBinding: null,
|
|
393
426
|
updatedAt: timestamp
|
|
394
427
|
};
|
|
395
428
|
return validateRoleSessionSet(updated);
|
|
@@ -402,18 +435,9 @@ export function clearTaskRoleRun(set, fence, clearedAt) {
|
|
|
402
435
|
export function terminalizeTaskRoleRunSession(set, fence, terminalAt) {
|
|
403
436
|
validateRoleSessionSet(set);
|
|
404
437
|
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);
|
|
438
|
+
let updated = inFlight === null
|
|
439
|
+
? set
|
|
440
|
+
: clearTaskRoleRun(set, fence, terminalAt);
|
|
417
441
|
const session = updated.sessions[updated.activeAgentId];
|
|
418
442
|
if (session?.status === "running") {
|
|
419
443
|
updated = updateRoleAgentSessionStatus(updated, updated.activeAgentId, "ready", terminalAt);
|
|
@@ -441,7 +465,7 @@ export function resetTaskRoleSession(set, now) {
|
|
|
441
465
|
sessions,
|
|
442
466
|
...(history === undefined ? {} : { history }),
|
|
443
467
|
inFlight: null,
|
|
444
|
-
|
|
468
|
+
providerBinding: null,
|
|
445
469
|
updatedAt: timestamp
|
|
446
470
|
});
|
|
447
471
|
}
|
|
@@ -451,38 +475,27 @@ export function settleTaskRoleCompletion(set, expected, settledAt) {
|
|
|
451
475
|
const agentId = requireSafeIdentity(expected.agentId, "Agent id");
|
|
452
476
|
const runId = requireSafeIdentity(expected.runId, "Run id");
|
|
453
477
|
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
478
|
const inFlight = set.inFlight;
|
|
463
479
|
if (inFlight === null || inFlight.agentId !== agentId || inFlight.runId !== runId) {
|
|
464
480
|
throw new Error("Pending Turn completion does not match the in-flight Run.");
|
|
465
481
|
}
|
|
466
482
|
const session = set.sessions[agentId];
|
|
467
|
-
if (session === undefined || session.
|
|
468
|
-
throw new Error("
|
|
483
|
+
if (session === undefined || !session.recentCompletedTurnIds.includes(turnId)) {
|
|
484
|
+
throw new Error("Turn completion has not been observed for the Role Agent session.");
|
|
469
485
|
}
|
|
470
486
|
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
487
|
const updated = {
|
|
475
488
|
...set,
|
|
476
489
|
sessions: {
|
|
477
490
|
...set.sessions,
|
|
478
491
|
[agentId]: {
|
|
479
492
|
...session,
|
|
480
|
-
recentCompletedTurnIds:
|
|
493
|
+
recentCompletedTurnIds: session.recentCompletedTurnIds,
|
|
481
494
|
updatedAt: timestamp
|
|
482
495
|
}
|
|
483
496
|
},
|
|
484
497
|
inFlight: null,
|
|
485
|
-
|
|
498
|
+
providerBinding: null,
|
|
486
499
|
updatedAt: timestamp
|
|
487
500
|
};
|
|
488
501
|
return validateRoleSessionSet(updated);
|
|
@@ -498,7 +511,7 @@ export function validateRoleSessionSet(set) {
|
|
|
498
511
|
throw new Error("Global Role session set schema version is invalid.");
|
|
499
512
|
}
|
|
500
513
|
if (Object.hasOwn(set, "inFlight")
|
|
501
|
-
|| Object.hasOwn(set, "
|
|
514
|
+
|| Object.hasOwn(set, "providerBinding")) {
|
|
502
515
|
throw new Error("Global Role session set must not contain Task Role lifecycle fields.");
|
|
503
516
|
}
|
|
504
517
|
const history = set.history;
|
|
@@ -513,11 +526,11 @@ export function validateRoleSessionSet(set) {
|
|
|
513
526
|
}
|
|
514
527
|
}
|
|
515
528
|
else {
|
|
516
|
-
if (set.schemaVersion !==
|
|
529
|
+
if (set.schemaVersion !== 5) {
|
|
517
530
|
throw new Error("Task Role session set schema version is invalid.");
|
|
518
531
|
}
|
|
519
532
|
if (!Object.hasOwn(set, "inFlight")
|
|
520
|
-
|| !Object.hasOwn(set, "
|
|
533
|
+
|| !Object.hasOwn(set, "providerBinding")) {
|
|
521
534
|
throw new Error("Task Role session set must contain its Turn fence.");
|
|
522
535
|
}
|
|
523
536
|
const taskSet = set;
|
|
@@ -546,28 +559,22 @@ export function validateRoleSessionSet(set) {
|
|
|
546
559
|
const inFlight = taskSet.inFlight === null
|
|
547
560
|
? null
|
|
548
561
|
: validateTaskRoleInFlight(taskSet.inFlight);
|
|
549
|
-
const
|
|
562
|
+
const providerBinding = taskSet.providerBinding === null
|
|
550
563
|
? null
|
|
551
|
-
:
|
|
552
|
-
if (inFlight === null && pending !== null) {
|
|
553
|
-
throw new Error("Pending Turn completion requires an in-flight Run.");
|
|
554
|
-
}
|
|
564
|
+
: validateProviderRuntimeBinding(taskSet.providerBinding);
|
|
555
565
|
if (inFlight !== null && inFlight.agentId !== set.activeAgentId) {
|
|
556
566
|
throw new Error("Task Role in-flight Run Agent must be active.");
|
|
557
567
|
}
|
|
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.");
|
|
568
|
+
if (providerBinding !== null) {
|
|
569
|
+
if (inFlight === null || providerBinding.runId !== inFlight.runId) {
|
|
570
|
+
throw new Error("Provider Runtime Binding must match the in-flight Run.");
|
|
564
571
|
}
|
|
565
|
-
const session = taskSet.sessions[
|
|
572
|
+
const session = taskSet.sessions[inFlight.agentId];
|
|
566
573
|
if (session === undefined) {
|
|
567
|
-
throw new Error("
|
|
574
|
+
throw new Error("Provider Runtime Binding has no active Role Agent session.");
|
|
568
575
|
}
|
|
569
|
-
if (
|
|
570
|
-
throw new Error("
|
|
576
|
+
if (providerBinding.providerNamespace !== session.adapterId) {
|
|
577
|
+
throw new Error("Provider Runtime Binding namespace does not match the Agent adapter.");
|
|
571
578
|
}
|
|
572
579
|
}
|
|
573
580
|
}
|
|
@@ -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
|
}
|