@zq-silk/yui 0.16.0 → 0.16.2
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/ARCHITECTURE.md +12 -0
- package/ARCHITECTURE.zh-CN.md +8 -0
- package/README.md +3 -0
- package/dist/agent/managedRuntimeEnvironment.js +0 -4
- package/dist/agentRun/agentRun.js +7 -17
- package/dist/cli/agentConfigurationPicker.js +24 -21
- package/dist/cli/commandCatalog.js +18 -10
- package/dist/cli/roleWizard.js +41 -30
- package/dist/cli/updateCommand.js +35 -13
- package/dist/cli/updateOrchestrator.js +73 -33
- package/dist/cli/updatePorts.js +51 -20
- package/dist/cli.js +35 -26
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +2 -2
- package/dist/commands/projectCommands.js +1 -1
- package/dist/commands/releaseCommands.js +9 -38
- package/dist/commands/taskCommands.js +84 -81
- package/dist/commands/taskCompletionGate.js +0 -117
- package/dist/commands/taskRoleRuntimeStatus.js +1 -1
- package/dist/commands/taskUpstreamCommands.js +93 -54
- package/dist/context/runContextPack.js +30 -54
- package/dist/context/runInputContract.js +9 -0
- package/dist/context/sessionBootstrapManifest.js +11 -19
- package/dist/controller/agentHostObservation.js +4 -2
- package/dist/controller/controller.js +8 -6
- package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
- package/dist/controller/globalInputDelivery.js +28 -7
- package/dist/controller/globalRuntimeAttention.js +34 -0
- package/dist/controller/operatorNotification.js +31 -0
- package/dist/controller/providerRetryAdmission.js +3 -1
- package/dist/controller/providerRetryDelivery.js +10 -3
- package/dist/controller/runtime.js +1 -27
- package/dist/controller/sessionOwnerReconciliation.js +42 -24
- package/dist/controller/structuredProviderObservation.js +27 -0
- package/dist/controller/updateReconciliation.js +72 -20
- package/dist/doctor/doctor.js +2 -1
- package/dist/executor/agentAdapter.js +28 -96
- package/dist/executor/agentConfigurationCatalog.js +13 -68
- package/dist/executor/agentConfigurationFields.js +120 -0
- package/dist/executor/agentConfigurationProbe.js +55 -80
- package/dist/executor/agentExecutor.js +5 -2
- package/dist/executor/executorRegistry.js +0 -9
- package/dist/executor/fileRoleLaunchPlanner.js +12 -11
- package/dist/interaction/operatorPresentation.js +2 -1
- package/dist/message/messageContinuation.js +8 -9
- package/dist/observability/executionAudit.js +1 -10
- package/dist/observability/runtimeIdentity.js +0 -23
- package/dist/output/agentConfigurationPresentation.js +4 -2
- package/dist/release/releaseHandover.js +2 -2
- package/dist/release/releaseIdempotencyStore.js +0 -23
- package/dist/release/releaseWorkflowPorts.js +8 -4
- package/dist/repository/gitWorkspace.js +66 -19
- package/dist/repository/taskWorkspacePreparer.js +75 -68
- package/dist/resources/resourceRegistry.js +90 -44
- package/dist/resources/sqliteResourceRegistry.js +3 -5
- package/dist/runtime/acpSession.js +21 -5
- package/dist/runtime/agentEndpoint.js +4 -1
- package/dist/runtime/agentHost.js +22 -62
- package/dist/runtime/agentHostCleanup.js +85 -0
- package/dist/runtime/agentHostProtocol.js +1 -1
- package/dist/runtime/builtinAgentDrivers.js +46 -119
- package/dist/runtime/builtinTranscriptObserver.js +10 -6
- package/dist/runtime/codexAppServerRuntime.js +55 -89
- package/dist/runtime/jsonLineChannel.js +35 -7
- package/dist/runtime/providerContinuationReconciliationService.js +42 -3
- package/dist/runtime/providerRetry.js +15 -0
- package/dist/runtime/providerRuntimeReconciler.js +12 -2
- package/dist/runtime/structuredProviderHost.js +106 -81
- package/dist/scheduler/activeRoleRunDelivery.js +5 -1
- package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
- package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
- package/dist/task/nextAction.js +33 -23
- package/dist/task/task.js +2 -1
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app.js +72 -3
- package/dist/web/assets/client/components.js +2 -1
- package/dist/web/assets/client/i18n.js +2 -0
- package/dist/web/assets/client/taskSummary.js +345 -0
- package/dist/web/assets/client/taskSurface.js +44 -26
- package/dist/web/assets/client/view.js +33 -0
- package/dist/web/assets/shell.js +1 -0
- package/dist/web/assets/styles/cards.js +21 -0
- package/dist/web/webServer.js +39 -5
- package/dist/web/webSessions.js +165 -0
- package/dist/web/webSnapshot.js +23 -0
- package/dist/web/webTaskSurface.js +23 -0
- package/docs/managed-turn-and-session-runtime.md +31 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
- package/docs/observability/README.md +47 -0
- package/docs/observability/README.zh-CN.md +37 -0
- package/docs/project-refresh.md +9 -0
- package/docs/project-refresh.zh-CN.md +8 -0
- package/docs/provider-retry.md +35 -0
- package/docs/release-workflow.md +139 -20
- package/docs/release-workflow.zh-CN.md +115 -14
- package/docs/roles-and-configuration.md +30 -0
- package/docs/roles-and-configuration.zh-CN.md +20 -0
- package/docs/task-delivery.md +109 -0
- package/docs/task-delivery.zh-CN.md +80 -0
- package/docs/task-discovery.md +9 -0
- package/docs/task-discovery.zh-CN.md +6 -0
- package/docs/testing/verification-levels.md +33 -12
- package/docs/testing/verification-levels.zh-CN.md +28 -8
- package/i18n/README.zh-CN.md +2 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +11 -0
- package/skills/yui-leader/references/execution.md +22 -0
- package/skills/yui-leader/references/planning.md +6 -2
- package/skills/yui-operator/SKILL.md +45 -13
- package/skills/yui-operator/references/task-delivery.md +99 -0
- package/skills/yui-runtime/SKILL.md +5 -3
- package/skills/yui-runtime/references/publication.md +56 -5
- package/skills/yui-runtime/references/recovery.md +12 -2
- package/dist/runtime/agentHostCompatibility.js +0 -127
|
@@ -5,17 +5,17 @@ import { freezeRunContextSnapshot } from "../context/runContextPack.js";
|
|
|
5
5
|
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
6
6
|
import { interruptThenTerminalState, prepareMessageContinuations } from "../message/messageContinuation.js";
|
|
7
7
|
import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
|
|
8
|
-
import { deferProviderRetry, providerRetryPending, recordProviderFailure } from "../runtime/providerRetry.js";
|
|
8
|
+
import { deferProviderRetry, providerRetryPending, providerRetryProjection, recordProviderFailure } from "../runtime/providerRetry.js";
|
|
9
9
|
import { AgentHostObservationDeferred, recordAgentHostConnection, resolveAgentHostObservation } from "./agentHostObservation.js";
|
|
10
10
|
import { admitOwnedProviderInput, ownedProviderInput } from "./providerRetryAdmission.js";
|
|
11
11
|
import { RuntimeHookRunFenceError } from "./runtimeHookRunFence.js";
|
|
12
|
-
import { appendRunInput, createRun, runPurposeAdmitsTaskState
|
|
12
|
+
import { appendRunInput, createRun, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
13
13
|
import { createRunInput } from "../context/runInputContract.js";
|
|
14
14
|
import { bindExecution, claimPending, completeProcessing, consumePendingBatch, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
|
|
15
15
|
import { enqueueWork, settleRoleRunDispatch as settleRoleRunDispatchMailbox } from "../coordination/workMailboxQueue.js";
|
|
16
16
|
import { transportAgentResult } from "../domain/agentResultTransport.js";
|
|
17
17
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
18
|
-
import { activeLiveRoleAgentSession, bindGlobalRoleProviderRuntime, bindTaskRoleProviderRuntime, createRoleSessionSet, detachRoleAgentSessionHost, recordRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, replaceTaskRoleAgentSession, selectNewTaskRoleSession,
|
|
18
|
+
import { activeLiveRoleAgentSession, bindGlobalRoleProviderRuntime, bindTaskRoleProviderRuntime, createRoleSessionSet, detachRoleAgentSessionHost, recordRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, replaceTaskRoleAgentSession, selectNewTaskRoleSession, roleSessionControlTarget, updateGlobalRoleProviderRuntime, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
19
19
|
import { effectiveLaunchWithTaskMainWorkspace, resolveEffectiveLaunch, roleSessionMayContinue, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
20
20
|
import { answerInputRequest } from "../input/inputRequest.js";
|
|
21
21
|
import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
|
|
@@ -26,6 +26,8 @@ import { activeRoleAgentBinding } from "../role/role.js";
|
|
|
26
26
|
import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
27
27
|
import { standardAgentError } from "../runtime/agentError.js";
|
|
28
28
|
import { recordTaskAgentError } from "./taskAgentError.js";
|
|
29
|
+
import { recordGlobalRuntimeAttention, recordGlobalRuntimeFailure } from "./globalRuntimeAttention.js";
|
|
30
|
+
import { queueOperatorNotification } from "./operatorNotification.js";
|
|
29
31
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
30
32
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
31
33
|
import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
@@ -384,6 +386,16 @@ export class FileSchedulerStoreAdapter {
|
|
|
384
386
|
const matches = receipt !== undefined && binding.run?.attemptId === receipt
|
|
385
387
|
&& (fence.nativeTurnId === undefined || binding.run.nativeTurnId === undefined
|
|
386
388
|
|| binding.run.nativeTurnId === fence.nativeTurnId);
|
|
389
|
+
if ((input.kind === "observer.health" || input.kind === "conversation.observed") && input.payload.failure !== undefined) {
|
|
390
|
+
if (receipt !== undefined && !matches)
|
|
391
|
+
return "obsolete";
|
|
392
|
+
recordGlobalRuntimeAttention(store, fence.roleName, input.eventId, {
|
|
393
|
+
fence, failure: input.payload.failure,
|
|
394
|
+
confirmed: binding.run?.status ?? "no-owned-input",
|
|
395
|
+
unknown: "Native execution outcome and resource quiescence are not proven by this diagnostic."
|
|
396
|
+
}, now);
|
|
397
|
+
return "applied";
|
|
398
|
+
}
|
|
387
399
|
if (input.kind === "turn.accepted") {
|
|
388
400
|
if (input.authority === "transport" && matches) {
|
|
389
401
|
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
|
|
@@ -456,12 +468,27 @@ export class FileSchedulerStoreAdapter {
|
|
|
456
468
|
binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
|
|
457
469
|
}
|
|
458
470
|
if (input.kind === "turn.failed" && matches && input.payload.failure !== undefined) {
|
|
471
|
+
const failureRef = recordGlobalRuntimeFailure(store, fence.roleName, input.eventId, {
|
|
472
|
+
fence, failure: input.payload.failure,
|
|
473
|
+
chainId: binding.retry?.chainId ?? receipt,
|
|
474
|
+
attempts: binding.retry?.attempts ?? 0,
|
|
475
|
+
confirmed: "Native Turn failed. Prior side effects are not undone.",
|
|
476
|
+
unknown: "External effects and remaining unfinished work."
|
|
477
|
+
}, now);
|
|
459
478
|
binding = recordProviderFailure(binding, {
|
|
460
|
-
error: input.payload.failure.error, failureRef
|
|
479
|
+
error: input.payload.failure.error, failureRef, at: now.getTime()
|
|
461
480
|
});
|
|
462
481
|
}
|
|
463
482
|
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
|
|
464
483
|
settleGlobalRetryInput(store, fence.roleName, binding, now);
|
|
484
|
+
if (input.kind === "turn.failed" && matches && input.payload.failure !== undefined
|
|
485
|
+
&& !providerRetryPending(binding)) {
|
|
486
|
+
recordGlobalRuntimeAttention(store, fence.roleName, input.eventId, {
|
|
487
|
+
fence, failure: input.payload.failure, retry: providerRetryProjection(binding),
|
|
488
|
+
confirmed: "Native Turn failed; this does not prove absence of prior side effects.",
|
|
489
|
+
unknown: "External effects must be inspected before continuing."
|
|
490
|
+
}, now);
|
|
491
|
+
}
|
|
465
492
|
return "applied";
|
|
466
493
|
});
|
|
467
494
|
}
|
|
@@ -518,11 +545,19 @@ export class FileSchedulerStoreAdapter {
|
|
|
518
545
|
&& (native.nativeTurnId === undefined || native.nativeTurnId === input.fence.nativeTurnId)
|
|
519
546
|
? "applied" : "obsolete";
|
|
520
547
|
}
|
|
521
|
-
const knownContinuation = input.kind.startsWith("continuation.")
|
|
522
|
-
&&
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
548
|
+
const knownContinuation = (input.kind.startsWith("continuation.") || input.kind === "observer.health")
|
|
549
|
+
&& projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.runId === input.fence.runId
|
|
550
|
+
&& entry.identity.providerNamespace === input.fence.driverId
|
|
551
|
+
&& entry.identity.accountScope === input.fence.agentId
|
|
552
|
+
&& entry.identity.conversationId === input.fence.conversationId
|
|
553
|
+
&& entry.identity.continuationId === input.fence.continuationId));
|
|
554
|
+
if (input.kind === "observer.health" && input.payload.failure !== undefined) {
|
|
555
|
+
const native = sessions?.providerBinding?.run;
|
|
556
|
+
return knownContinuation || session?.nativeSessionId === input.fence.nativeSessionId
|
|
557
|
+
&& (input.fence.receiptId === undefined || native?.attemptId === input.fence.receiptId
|
|
558
|
+
&& (input.fence.nativeTurnId === undefined || native.nativeTurnId === input.fence.nativeTurnId))
|
|
559
|
+
? "applied" : "obsolete";
|
|
560
|
+
}
|
|
526
561
|
const requiresCurrentRuntime = input.kind !== "turn.cancelled" && !knownContinuation;
|
|
527
562
|
const valid = run !== null && (!requiresCurrentRuntime || (run.status === "active" && active?.id === run.id)) && run.roleName === input.fence.roleName && run.effective.agentId === input.fence.agentId && this.drivers.requireByAdapterId(run.effective.adapterId).id === input.fence.driverId && (knownContinuation || (session !== undefined && session.nativeSessionId === input.fence.nativeSessionId)) && runtimeReceiptBelongsToRun(store, input);
|
|
528
563
|
if (!valid) {
|
|
@@ -733,7 +768,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
733
768
|
? "provider-goal-cleared"
|
|
734
769
|
: `provider-goal-${input.payload.goalStatus}`, now);
|
|
735
770
|
}
|
|
736
|
-
if (
|
|
771
|
+
if (["turn.failed", "observer.health", "conversation.observed"].includes(input.kind) && input.payload.failure !== undefined) {
|
|
737
772
|
const failure = input.payload.failure;
|
|
738
773
|
const error = failure.error;
|
|
739
774
|
const run = input.fence.runId === undefined
|
|
@@ -753,10 +788,12 @@ export class FileSchedulerStoreAdapter {
|
|
|
753
788
|
runId: input.fence.runId,
|
|
754
789
|
nativeSessionId: input.fence.nativeSessionId,
|
|
755
790
|
nativeTurnId: input.fence.nativeTurnId,
|
|
791
|
+
attemptId: input.fence.receiptId,
|
|
756
792
|
lastOutput: failure.lastOutput
|
|
757
793
|
},
|
|
758
794
|
}, now);
|
|
759
|
-
if (created &&
|
|
795
|
+
if (created && (input.kind !== "turn.failed"
|
|
796
|
+
|| !providerRetryPending(store.getTaskRoleSessionSet(taskId, input.fence.roleName)?.providerBinding)))
|
|
760
797
|
routeRoleEvent(store, errorEvent, input.fence.roleName, wakeReason("agent-error", errorEvent.id), now);
|
|
761
798
|
}
|
|
762
799
|
if ((input.kind === "operation.completed" || input.kind === "operation.failed")
|
|
@@ -857,6 +894,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
857
894
|
getActiveRun(taskId, roleName) {
|
|
858
895
|
return this.store.getActiveRun(taskId, roleName);
|
|
859
896
|
}
|
|
897
|
+
getContextSnapshot(taskId, snapshotId) {
|
|
898
|
+
return this.store.getContextSnapshot(taskId, snapshotId);
|
|
899
|
+
}
|
|
860
900
|
hasOpenInputRequest(taskId) {
|
|
861
901
|
return this.store.listOpenInputRequests([taskId]).length > 0;
|
|
862
902
|
}
|
|
@@ -879,8 +919,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
879
919
|
adapterId: effectiveSession.effective.adapterId
|
|
880
920
|
};
|
|
881
921
|
}
|
|
882
|
-
|
|
883
|
-
|
|
922
|
+
queueOperatorNotification(input, now) {
|
|
923
|
+
return queueOperatorNotification(this.store, input, now);
|
|
884
924
|
}
|
|
885
925
|
resolveExpiredInputRecommendations(now, taskIds) {
|
|
886
926
|
return this.store.transaction((store) => {
|
|
@@ -1224,8 +1264,26 @@ export class FileSchedulerStoreAdapter {
|
|
|
1224
1264
|
let resolved = settleProviderTurnSubmission(binding, { attemptId: input.attemptId, status: input.status,
|
|
1225
1265
|
reason: input.reason, resolvedAt: input.now.toISOString() });
|
|
1226
1266
|
resolved = this.retrySubmissionResolution(resolved, input, sessions.sessions[sessions.activeAgentId]?.adapterId);
|
|
1267
|
+
if (input.status !== "deferred" && resolved.run?.failure !== undefined) {
|
|
1268
|
+
const failureRef = recordGlobalRuntimeFailure(store, input.roleName, `${input.attemptId}:submission`, {
|
|
1269
|
+
nativeSessionId: currentProviderConversation(resolved).conversationId,
|
|
1270
|
+
attemptId: input.attemptId, status: input.status, failure: resolved.run.failure,
|
|
1271
|
+
chainId: resolved.retry?.chainId ?? input.attemptId, attempts: resolved.retry?.attempts ?? 0
|
|
1272
|
+
}, input.now);
|
|
1273
|
+
resolved = { ...resolved, run: { ...resolved.run, failure: { ...resolved.run.failure, ref: failureRef } },
|
|
1274
|
+
...(resolved.retry?.failedAttemptId === input.attemptId
|
|
1275
|
+
? { retry: { ...resolved.retry, failureRef } } : {}) };
|
|
1276
|
+
}
|
|
1227
1277
|
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, resolved, input.now));
|
|
1228
1278
|
settleGlobalRetryInput(store, input.roleName, resolved, input.now);
|
|
1279
|
+
if (input.status !== "deferred" && !providerRetryPending(resolved)) {
|
|
1280
|
+
recordGlobalRuntimeAttention(store, input.roleName, `${input.attemptId}:submission`, {
|
|
1281
|
+
nativeSessionId: currentProviderConversation(resolved).conversationId,
|
|
1282
|
+
attemptId: input.attemptId, status: input.status,
|
|
1283
|
+
failure: resolved.run?.failure, retry: providerRetryProjection(resolved),
|
|
1284
|
+
unknown: "External effects are not established by submission settlement."
|
|
1285
|
+
}, input.now);
|
|
1286
|
+
}
|
|
1229
1287
|
const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
|
|
1230
1288
|
if (message !== undefined && message.delivery?.via !== "provider") {
|
|
1231
1289
|
const outcome = input.status === "delivery-unknown" ? "delivery-unknown" : "rejected";
|
|
@@ -1681,15 +1739,17 @@ export class FileSchedulerStoreAdapter {
|
|
|
1681
1739
|
const mailbox = store.getWorkMailbox(target);
|
|
1682
1740
|
if (mailbox?.pending == null || mailbox.processing !== null)
|
|
1683
1741
|
return false;
|
|
1742
|
+
const snapshot = freezeRunContextSnapshot(store, {
|
|
1743
|
+
taskId, roleName: "leader", purpose: "planning"
|
|
1744
|
+
}, now);
|
|
1684
1745
|
const run = createRun(store.nextRunId(taskId), taskId, "leader", "new", createRunInput({ source: { type: "yui", channel: "task-dispatch" },
|
|
1685
1746
|
directive: `Plan Task ${taskId} with the user. Persist requirements and decisions. Request activation explicitly; planning grants no delivery authority.`,
|
|
1747
|
+
contextSnapshotRef: contextSnapshotRef(snapshot),
|
|
1686
1748
|
deltaRefIds: [] }), now, {
|
|
1687
1749
|
purpose: "planning", effective: resolveEffectiveLaunch({ role, purpose: "planning" })
|
|
1688
1750
|
});
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
store.saveRun(frozen);
|
|
1692
|
-
store.saveActiveRun(frozen);
|
|
1751
|
+
store.saveRun(run);
|
|
1752
|
+
store.saveActiveRun(run);
|
|
1693
1753
|
// Freeze the initial notification prefix against this planning Run.
|
|
1694
1754
|
// Later messages remain pending; acceptance or an exact terminal settles
|
|
1695
1755
|
// only this batch, so a failed initial launch cannot replay it forever.
|
|
@@ -2256,6 +2316,13 @@ export class FileSchedulerStoreAdapter {
|
|
|
2256
2316
|
recordCanonicalObservationObsolete(store, input, "provider-conversation-mismatch", now);
|
|
2257
2317
|
return "obsolete";
|
|
2258
2318
|
}
|
|
2319
|
+
if (input.payload.failure !== undefined) {
|
|
2320
|
+
// A diagnostic without an owned Turn describes this exact connection,
|
|
2321
|
+
// not a new recoverability observation or proof of resource release.
|
|
2322
|
+
return sessions.activeAgentId === input.fence.agentId
|
|
2323
|
+
&& sessions.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId
|
|
2324
|
+
? "applied" : "obsolete";
|
|
2325
|
+
}
|
|
2259
2326
|
try {
|
|
2260
2327
|
if (input.kind === "conversation.observed") {
|
|
2261
2328
|
binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
|
|
@@ -2864,7 +2931,7 @@ function dormantRuntimeCandidateIsCurrent(store, candidate, allowActiveRun = fal
|
|
|
2864
2931
|
}
|
|
2865
2932
|
const sessions = runtimeOwnerSessionSet(store, owner);
|
|
2866
2933
|
const active = allowActiveRun && owner.scope === "task"
|
|
2867
|
-
?
|
|
2934
|
+
? roleSessionControlTarget(store.getTaskRoleSessionSet(owner.taskId, owner.roleName))
|
|
2868
2935
|
: sessions?.sessions[sessions.activeAgentId];
|
|
2869
2936
|
return active !== undefined && active.agentId === candidate.agentId && active.adapterId === candidate.adapterId
|
|
2870
2937
|
&& active.nativeSessionId === candidate.nativeSessionId && (allowActiveRun || active.updatedAt === candidate.sessionUpdatedAt);
|
|
@@ -13,6 +13,10 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
13
13
|
if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
|
|
14
14
|
continue;
|
|
15
15
|
let sessions = store.getGlobalRoleSessionSet(role.name);
|
|
16
|
+
// A detached Host keeps an active Session and may reconnect. An explicit
|
|
17
|
+
// Session stop/switch must not be undone by background notification work.
|
|
18
|
+
if (sessions !== null && sessions.sessions[sessions.activeAgentId]?.status !== "active")
|
|
19
|
+
continue;
|
|
16
20
|
if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
|
|
17
21
|
store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
|
|
18
22
|
pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
|
|
@@ -37,6 +41,9 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
37
41
|
continue;
|
|
38
42
|
if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))
|
|
39
43
|
continue;
|
|
44
|
+
const matchesTarget = (message) => message.deliveryTarget === undefined
|
|
45
|
+
|| sessions?.activeAgentId === message.deliveryTarget.agentId
|
|
46
|
+
&& sessions.sessions[message.deliveryTarget.agentId]?.nativeSessionId === message.deliveryTarget.nativeSessionId;
|
|
40
47
|
const claims = pending.filter(message => message.interruptThen !== undefined);
|
|
41
48
|
let next;
|
|
42
49
|
if (claims.length > 0) {
|
|
@@ -54,22 +61,36 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
54
61
|
if (next === undefined)
|
|
55
62
|
continue;
|
|
56
63
|
}
|
|
57
|
-
else
|
|
58
|
-
|
|
64
|
+
else {
|
|
65
|
+
// Retain each stale original with its exact failure, but do not spend
|
|
66
|
+
// one reconciliation interval per unsent notice before reaching user
|
|
67
|
+
// input for the successor. An existing attempt/uncertain control still
|
|
68
|
+
// fences the queue; target mismatch alone is not non-acceptance.
|
|
69
|
+
next = pending.find(message => {
|
|
70
|
+
if (matchesTarget(message)
|
|
71
|
+
|| current?.attemptId === `global-input:${role.name}/${message.id}`
|
|
72
|
+
|| message.control !== undefined && message.control.outcome !== "rejected")
|
|
73
|
+
return true;
|
|
74
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, "native-session-changed", new Date()));
|
|
75
|
+
return false;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (next === undefined)
|
|
79
|
+
continue;
|
|
59
80
|
const attemptId = `global-input:${role.name}/${next.id}`;
|
|
60
81
|
if (current?.attemptId === attemptId)
|
|
61
82
|
continue;
|
|
62
83
|
if (next.control !== undefined && next.control.outcome !== "rejected")
|
|
63
84
|
continue;
|
|
64
|
-
|
|
65
|
-
|| sessions?.activeAgentId === next.deliveryTarget.agentId
|
|
66
|
-
&& sessions.sessions[next.deliveryTarget.agentId]?.nativeSessionId === next.deliveryTarget.nativeSessionId;
|
|
67
|
-
if (!matchesTarget()) {
|
|
85
|
+
if (!matchesTarget(next)) {
|
|
68
86
|
store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(next, "native-session-changed", new Date()));
|
|
69
87
|
continue;
|
|
70
88
|
}
|
|
71
89
|
await ensure(role.name);
|
|
72
90
|
sessions = store.getGlobalRoleSessionSet(role.name);
|
|
91
|
+
if (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name })))
|
|
92
|
+
|| sessions?.sessions[sessions.activeAgentId]?.status !== "active")
|
|
93
|
+
continue;
|
|
73
94
|
const binding = sessions?.providerBinding;
|
|
74
95
|
if (binding == null || binding.authority.owner !== "controller")
|
|
75
96
|
continue;
|
|
@@ -78,7 +99,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
78
99
|
if (persisted === undefined || JSON.stringify(persisted) !== JSON.stringify(next))
|
|
79
100
|
continue;
|
|
80
101
|
const claim = next.interruptThen;
|
|
81
|
-
if (!matchesTarget() || claim !== undefined
|
|
102
|
+
if (!matchesTarget(next) || claim !== undefined
|
|
82
103
|
&& (sessions?.activeAgentId !== claim.targetAgentId
|
|
83
104
|
|| nativeSessionId !== claim.targetNativeSessionId
|
|
84
105
|
|| binding.authority.epoch !== claim.targetAuthorityEpoch
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createGlobalRoleMessage } from "../message/message.js";
|
|
3
|
+
import { serializeAgentErrorRaw } from "../runtime/agentError.js";
|
|
4
|
+
/** Global observations have no Task event history. Keep original failed
|
|
5
|
+
* attempts as ordinary record-only Messages before replacing the live binding. */
|
|
6
|
+
export function recordGlobalRuntimeFailure(store, roleName, identity, facts, now) {
|
|
7
|
+
const header = `Yui Global failure evidence ${createHash("sha256").update(identity).digest("hex")}\n`;
|
|
8
|
+
const previous = store.listGlobalRoleMessages(roleName).find(message => message.kind === "system" && message.body.startsWith(header));
|
|
9
|
+
if (previous !== undefined)
|
|
10
|
+
return `${roleName}/${previous.id}`;
|
|
11
|
+
const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + serializeAgentErrorRaw(facts), "system", { type: "system" }, now);
|
|
12
|
+
store.saveGlobalRoleMessage(message);
|
|
13
|
+
return `${roleName}/${message.id}`;
|
|
14
|
+
}
|
|
15
|
+
/** Reuse the Global Message inbox for an exact failure, not a recovery queue.
|
|
16
|
+
* A failed Operator cannot be asked to retry its own diagnostic automatically. */
|
|
17
|
+
export function recordGlobalRuntimeAttention(store, roleName, identity, facts, now) {
|
|
18
|
+
// A Home may have a custom Global Role without an Operator. Keep its own
|
|
19
|
+
// diagnostic readable; never create a Role or launch a substitute Agent.
|
|
20
|
+
const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
|
|
21
|
+
const requestId = `runtime-attention:${createHash("sha256").update(`${roleName}\0${identity}`).digest("hex")}`;
|
|
22
|
+
if (store.listGlobalRoleMessages(recipient).some(message => message.inputControl?.requestId === requestId))
|
|
23
|
+
return;
|
|
24
|
+
const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), recipient, [
|
|
25
|
+
`Global Role ${roleName} needs attention. No recovery or new authority is implied.`,
|
|
26
|
+
"Inspect the exact Session and original input before choosing a next action; unknown effects must not be replayed.",
|
|
27
|
+
`Original failed attempts remain in Role ${roleName}'s record-only failure Messages; failureRef names the latest evidence.`,
|
|
28
|
+
serializeAgentErrorRaw(facts)
|
|
29
|
+
].join("\n"), "system", { type: "system" }, now, { inputControl: { action: "queue", requestId } });
|
|
30
|
+
store.saveGlobalRoleMessage(recipient === roleName ? {
|
|
31
|
+
...message,
|
|
32
|
+
notDelivered: { reason: "runtime-needs-attention; retained for user or successor inspection", at: now.toISOString() }
|
|
33
|
+
} : message);
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { completeProcessing } from "../coordination/workMailbox.js";
|
|
2
|
+
import { createGlobalRoleMessage } from "../message/message.js";
|
|
3
|
+
/**
|
|
4
|
+
* Transfer one exact attention claim to the Global Message queue atomically.
|
|
5
|
+
* Queue ownership is not Provider acceptance or Agent acknowledgement. A failed
|
|
6
|
+
* or uncertain Message retains its identity/evidence; it is never reissued here.
|
|
7
|
+
*/
|
|
8
|
+
export function queueOperatorNotification(store, input, now = new Date()) {
|
|
9
|
+
return store.transaction(tx => {
|
|
10
|
+
const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
|
|
11
|
+
const previous = tx.listGlobalRoleMessages("operator").find(entry => (entry.inputControl ?? entry.interruptThen?.reusedInput)?.requestId === requestId);
|
|
12
|
+
const mailbox = tx.getWorkMailbox({ kind: "operator" });
|
|
13
|
+
const claimed = mailbox?.processing?.batchId === input.batchId;
|
|
14
|
+
if (previous !== undefined) {
|
|
15
|
+
if (claimed)
|
|
16
|
+
tx.saveWorkMailbox(completeProcessing(mailbox, input.batchId));
|
|
17
|
+
return { status: "already-queued", messageId: previous.id };
|
|
18
|
+
}
|
|
19
|
+
if (!claimed)
|
|
20
|
+
throw new Error(`Operator notification claim changed: ${input.batchId}`);
|
|
21
|
+
const sessions = tx.getGlobalRoleSessionSet("operator");
|
|
22
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
23
|
+
if (session?.status !== "active" || sessions?.providerBinding == null)
|
|
24
|
+
return { status: "unavailable" };
|
|
25
|
+
const created = { ...createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", input.text, "system", { type: "system" }, now, { inputControl: { action: "queue", requestId } }),
|
|
26
|
+
deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId } };
|
|
27
|
+
tx.saveGlobalRoleMessage(created);
|
|
28
|
+
tx.saveWorkMailbox(completeProcessing(mailbox, input.batchId));
|
|
29
|
+
return { status: "queued", messageId: created.id };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -24,7 +24,9 @@ export function retryIntentBlocker(store, input, retry) {
|
|
|
24
24
|
return "role-changed";
|
|
25
25
|
if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
|
|
26
26
|
return "role-configuration-changed";
|
|
27
|
-
if (store.listGlobalRoleMessages(input.roleName).some(message =>
|
|
27
|
+
if (store.listGlobalRoleMessages(input.roleName).some(message => (message.kind === "user" || message.kind === "operator"
|
|
28
|
+
|| message.inputControl !== undefined || message.interruptThen !== undefined)
|
|
29
|
+
&& Date.parse(message.createdAt) > Date.parse(retry.intentAt)
|
|
28
30
|
&& message.delivery === undefined && message.notDelivered === undefined))
|
|
29
31
|
return "new-user-input";
|
|
30
32
|
return undefined;
|
|
@@ -6,9 +6,10 @@ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
|
6
6
|
import { redactAgentErrorText } from "../runtime/agentError.js";
|
|
7
7
|
import { AGENT_HOST_CONTROL_PROTOCOL, agentHostControlSocketPath, inspectAgentHostSocket, sendAgentHostRunControl } from "../runtime/agentHost.js";
|
|
8
8
|
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
9
|
-
import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
|
|
9
|
+
import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending, providerRetryProjection } from "../runtime/providerRetry.js";
|
|
10
10
|
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
11
11
|
import { retryIntentBlocker } from "./providerRetryAdmission.js";
|
|
12
|
+
import { recordGlobalRuntimeAttention } from "./globalRuntimeAttention.js";
|
|
12
13
|
/** Hooks in the existing Controller pass and nearest-deadline timer. SQLite's
|
|
13
14
|
* current Provider input is the only pending intent; nothing is queued here. */
|
|
14
15
|
export function createProviderRetryHooks(home, store, ports = {}) {
|
|
@@ -45,8 +46,11 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
45
46
|
save({ ...current, providerBinding: exhausted
|
|
46
47
|
? { ...cancelled, retry: { ...cancelled.retry, status: "exhausted" } } : cancelled });
|
|
47
48
|
const owner = set.owner;
|
|
48
|
-
if (owner.scope === "global")
|
|
49
|
-
|
|
49
|
+
if (owner.scope === "global") {
|
|
50
|
+
const stopped = load(owner)?.providerBinding;
|
|
51
|
+
settleGlobalRetryInput(store, owner.roleName, stopped, new Date(at));
|
|
52
|
+
recordGlobalRuntimeAttention(store, owner.roleName, `${binding.retry.chainId}:stopped`, providerRetryProjection(stopped), new Date(at));
|
|
53
|
+
}
|
|
50
54
|
if (owner.scope === "task") {
|
|
51
55
|
const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-stopped", {
|
|
52
56
|
roleName: owner.roleName, chainId: binding.retry.chainId, reason
|
|
@@ -128,6 +132,9 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
128
132
|
return;
|
|
129
133
|
save({ ...latest, providerBinding: { ...latest.providerBinding,
|
|
130
134
|
retry: { ...latest.providerBinding.retry, reason: detail } } });
|
|
135
|
+
if (owner.scope === "global") {
|
|
136
|
+
recordGlobalRuntimeAttention(store, owner.roleName, `${retry.currentAttemptId}:unconfirmed`, providerRetryProjection(load(owner)?.providerBinding), new Date(at));
|
|
137
|
+
}
|
|
131
138
|
if (owner.scope === "task") {
|
|
132
139
|
const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-unconfirmed", {
|
|
133
140
|
roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
|
|
@@ -10,7 +10,6 @@ import { effectiveLaunchConfig, resolveEffectiveLaunch, roleSessionMayContinue }
|
|
|
10
10
|
import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
|
|
11
11
|
import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
|
|
12
12
|
import { createKernelPorts } from "../kernel/kernelPorts.js";
|
|
13
|
-
import { createGlobalRoleMessage } from "../message/message.js";
|
|
14
13
|
import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
|
|
15
14
|
import { createResourceAutoGc } from "../resources/autoResourceGc.js";
|
|
16
15
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
@@ -117,7 +116,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
117
116
|
store,
|
|
118
117
|
environment: options.environment,
|
|
119
118
|
tmux,
|
|
120
|
-
nativeConnection: (
|
|
119
|
+
nativeConnection: (owner) => planner.planNativeControl(owner),
|
|
121
120
|
onWarning: options.onError
|
|
122
121
|
});
|
|
123
122
|
// The runtime inbox is also the low-latency discovery source during a
|
|
@@ -252,31 +251,6 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
252
251
|
sessionHost,
|
|
253
252
|
promptPush,
|
|
254
253
|
launchCoordinator,
|
|
255
|
-
notifyOperatorInputOnce: async (input) => {
|
|
256
|
-
// The original attention mailbox remains pending until this one
|
|
257
|
-
// Global Message has an actual Provider receipt. Never inject keys
|
|
258
|
-
// into a busy Host console and mistake that for accepted attention.
|
|
259
|
-
const message = store.transaction(tx => {
|
|
260
|
-
const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
|
|
261
|
-
const previous = tx.listGlobalRoleMessages("operator").find(entry => (entry.inputControl ?? entry.interruptThen?.reusedInput)?.requestId === requestId);
|
|
262
|
-
if (previous !== undefined)
|
|
263
|
-
return previous;
|
|
264
|
-
const sessions = tx.getGlobalRoleSessionSet("operator");
|
|
265
|
-
const session = sessions?.sessions[sessions.activeAgentId];
|
|
266
|
-
if (session?.status !== "active" || sessions?.providerBinding == null)
|
|
267
|
-
return null;
|
|
268
|
-
const created = { ...createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", input.text, "system", { type: "system" }, new Date(), { inputControl: { action: "queue", requestId } }),
|
|
269
|
-
deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId } };
|
|
270
|
-
tx.saveGlobalRoleMessage(created);
|
|
271
|
-
return created;
|
|
272
|
-
});
|
|
273
|
-
if (message === null)
|
|
274
|
-
return "unavailable";
|
|
275
|
-
if (message.delivery?.via === "provider")
|
|
276
|
-
return "already-sent";
|
|
277
|
-
runningRuntime?.signal("global-role:operator");
|
|
278
|
-
return "not-ready";
|
|
279
|
-
},
|
|
280
254
|
roleResourceInventory: async (panes, inputs) => {
|
|
281
255
|
const inventory = await scanInventory(panes);
|
|
282
256
|
return inventory.resources.flatMap((resource) => {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
4
|
+
import { createGlobalRoleMessage } from "../message/message.js";
|
|
4
5
|
import { createSessionOwnerIdentity, isLinuxProcessLive, listOwnedProcessTree, readLinuxProcessIdentity, reconcileSessionOwners, terminateSessionOwners } from "../runtime/index.js";
|
|
5
6
|
import { sessionOwnerProcessKey } from "../runtime/sessionOwnerIdentity.js";
|
|
6
7
|
import { AGENT_HOST_CONTROL_PROTOCOL, sendAgentHostCancelControl } from "../runtime/agentHost.js";
|
|
7
8
|
import { settleProviderTurn, cancelQuiescentProviderInput, clearProviderGoal } from "../runtime/providerRuntimeIdentity.js";
|
|
8
|
-
import { updateTaskRoleProviderRuntime,
|
|
9
|
+
import { updateTaskRoleProviderRuntime, updateGlobalRoleProviderRuntime, roleSessionControlTarget } from "../executor/agentExecutor.js";
|
|
9
10
|
import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
|
|
10
11
|
import { stopCodexNativeSession } from "../runtime/nativeSessionControl.js";
|
|
11
12
|
import { routeRoleEvent } from "../scheduler/operatorEvent.js";
|
|
@@ -34,8 +35,8 @@ export class SessionOwnerReconciliation {
|
|
|
34
35
|
this.#tmux = deps.tmux;
|
|
35
36
|
this.#onWarning = deps.onWarning ?? (() => undefined);
|
|
36
37
|
this.#cancelInput = deps.cancelInput ?? sendAgentHostCancelControl;
|
|
37
|
-
this.#nativeConnection = deps.nativeConnection ?? ((
|
|
38
|
-
.planNativeControl(
|
|
38
|
+
this.#nativeConnection = deps.nativeConnection ?? ((owner) => new FileRoleLaunchPlanner(this.#home, this.#store, { environment: this.#environment })
|
|
39
|
+
.planNativeControl(owner));
|
|
39
40
|
this.#stopNative = deps.stopNative ?? stopCodexNativeSession;
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
@@ -173,33 +174,36 @@ export class SessionOwnerReconciliation {
|
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
176
|
async #quiesceInput(owner) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (runtimeCleanupDisposition(this.#store.getWorkMailbox(runtimeLifecycleTarget(owner))) === "detach-host")
|
|
177
|
+
const disposition = runtimeCleanupDisposition(this.#store.getWorkMailbox(runtimeLifecycleTarget(owner)));
|
|
178
|
+
if (disposition === "detach-host")
|
|
179
179
|
return;
|
|
180
|
-
const set =
|
|
180
|
+
const set = owner.scope === "task"
|
|
181
|
+
? this.#store.getTaskRoleSessionSet(owner.taskId, owner.roleName)
|
|
182
|
+
: this.#store.getGlobalRoleSessionSet(owner.roleName);
|
|
181
183
|
const binding = set?.providerBinding;
|
|
182
184
|
const input = binding?.run;
|
|
183
185
|
const unsettled = input != null && ["submitting", "accepted", "delivery-unknown"].includes(input.status);
|
|
184
186
|
const hasGoal = binding?.goal != null && binding.goal.status !== "complete";
|
|
185
|
-
const selected =
|
|
187
|
+
const selected = roleSessionControlTarget(set);
|
|
186
188
|
if (set == null || (!unsettled && !hasGoal && selected?.adapterId !== "codex"))
|
|
187
189
|
return;
|
|
188
190
|
if (selected === undefined) {
|
|
189
191
|
throw new Error("Cannot identify the native execution to stop.");
|
|
190
192
|
}
|
|
191
|
-
const request = this.#store.listEvents(owner.taskId).filter(event => ["runtime.session-replacement-requested", "runtime.session-stop-requested"].includes(event.type)
|
|
193
|
+
const request = owner.scope === "task" ? this.#store.listEvents(owner.taskId).filter(event => ["runtime.session-replacement-requested", "runtime.session-stop-requested"].includes(event.type)
|
|
192
194
|
&& event.payload.roleName === owner.roleName
|
|
193
|
-
&& event.payload.nativeSessionId === selected.nativeSessionId).at(-1);
|
|
194
|
-
const operatorStop =
|
|
195
|
-
|
|
195
|
+
&& event.payload.nativeSessionId === selected.nativeSessionId).at(-1) : undefined;
|
|
196
|
+
const operatorStop = owner.scope === "global"
|
|
197
|
+
? disposition === "end-session" || disposition === "replace-session"
|
|
198
|
+
: this.#store.getTask(owner.taskId)?.executionGate.state === "stopped"
|
|
199
|
+
|| (request !== undefined && ["user", "operator"].includes(request.payload.requestedBy));
|
|
196
200
|
if (binding?.authority.owner === "human" && !operatorStop) {
|
|
197
201
|
throw new Error("This native Session is under human control; Operator/user must request its stop or replacement.");
|
|
198
202
|
}
|
|
199
203
|
let terminal;
|
|
200
204
|
try {
|
|
201
205
|
const result = unsettled && binding?.authority.owner === "controller" && binding.authority.holderId !== undefined ? await this.#cancelInput({
|
|
202
|
-
home: this.#home,
|
|
206
|
+
home: this.#home, ...owner,
|
|
203
207
|
control: {
|
|
204
208
|
protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "cancel",
|
|
205
209
|
nativeSessionId: selected.nativeSessionId,
|
|
@@ -220,7 +224,7 @@ export class SessionOwnerReconciliation {
|
|
|
220
224
|
}
|
|
221
225
|
if (terminal === undefined || hasGoal || selected.adapterId === "codex") {
|
|
222
226
|
if (selected.adapterId === "codex") {
|
|
223
|
-
await this.#stopNative(this.#nativeConnection(owner
|
|
227
|
+
await this.#stopNative(this.#nativeConnection(owner), {
|
|
224
228
|
conversationId: selected.nativeSessionId,
|
|
225
229
|
nativeTurnId: unsettled && terminal === undefined ? input?.nativeTurnId : undefined,
|
|
226
230
|
clearGoal: true
|
|
@@ -237,7 +241,7 @@ export class SessionOwnerReconciliation {
|
|
|
237
241
|
}
|
|
238
242
|
else if (selected.status !== "ended"
|
|
239
243
|
|| records.some(ownerRootIsLive)
|
|
240
|
-
|| this.#tmux?.probeRoleStatus(owner.taskId, owner.roleName) !== "exited") {
|
|
244
|
+
|| this.#tmux?.probeRoleStatus(owner.scope === "task" ? owner.taskId : "operator", owner.roleName) !== "exited") {
|
|
241
245
|
throw new Error("Native process custody is unavailable; inspect the exact local execution before reusing its workspace.");
|
|
242
246
|
}
|
|
243
247
|
}
|
|
@@ -246,9 +250,12 @@ export class SessionOwnerReconciliation {
|
|
|
246
250
|
}
|
|
247
251
|
}
|
|
248
252
|
this.#store.transaction(tx => {
|
|
249
|
-
const current =
|
|
253
|
+
const current = owner.scope === "task"
|
|
254
|
+
? tx.getTaskRoleSessionSet(owner.taskId, owner.roleName)
|
|
255
|
+
: tx.getGlobalRoleSessionSet(owner.roleName);
|
|
250
256
|
const provider = current?.providerBinding;
|
|
251
257
|
if (current == null || provider?.run?.attemptId !== input?.attemptId
|
|
258
|
+
|| provider?.run?.nativeTurnId !== input?.nativeTurnId
|
|
252
259
|
|| (selected.fromBinding
|
|
253
260
|
? current.providerBinding?.conversations.find(entry => entry.status === "current")?.conversationId
|
|
254
261
|
: current.sessions[current.activeAgentId]?.nativeSessionId) !== selected.nativeSessionId
|
|
@@ -268,15 +275,26 @@ export class SessionOwnerReconciliation {
|
|
|
268
275
|
}
|
|
269
276
|
if (hasGoal || selected.adapterId === "codex")
|
|
270
277
|
stopped = clearProviderGoal(stopped);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
+
if (owner.scope === "global") {
|
|
279
|
+
tx.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(current, stopped, now));
|
|
280
|
+
if (unsettled || hasGoal)
|
|
281
|
+
tx.saveGlobalRoleMessage(createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), owner.roleName, `Explicit Session stop confirmed: ${JSON.stringify({
|
|
282
|
+
nativeSessionId: selected.nativeSessionId, attemptId: input?.attemptId,
|
|
283
|
+
nativeTurnId: input?.nativeTurnId, status: stopped.run?.status ?? "quiescent",
|
|
284
|
+
evidence: terminal === undefined ? "native-resource-inspection" : "native-terminal"
|
|
285
|
+
})}`, "system", { type: "system" }, now));
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
tx.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(current, stopped, now));
|
|
289
|
+
tx.saveEvent(owner.taskId, createTaskEvent(tx.nextEventId(owner.taskId), owner.taskId, "runtime.input-stop-confirmed", {
|
|
290
|
+
roleName: owner.roleName, ...(input == null ? {} : { attemptId: input.attemptId }),
|
|
291
|
+
nativeSessionId: selected.nativeSessionId, status: stopped.run?.status ?? "quiescent",
|
|
292
|
+
evidence: terminal === undefined ? "native-resource-inspection" : "native-terminal",
|
|
293
|
+
...(input?.nativeTurnId === undefined ? {} : { nativeTurnId: input.nativeTurnId })
|
|
294
|
+
}, now));
|
|
295
|
+
}
|
|
278
296
|
}
|
|
279
|
-
else if (selected.adapterId === "codex") {
|
|
297
|
+
else if (selected.adapterId === "codex" && owner.scope === "task") {
|
|
280
298
|
tx.saveEvent(owner.taskId, createTaskEvent(tx.nextEventId(owner.taskId), owner.taskId, "runtime.input-stop-confirmed", {
|
|
281
299
|
roleName: owner.roleName, nativeSessionId: selected.nativeSessionId,
|
|
282
300
|
status: "quiescent", evidence: "native-resource-inspection"
|