@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.
Files changed (58) hide show
  1. package/README.md +10 -3
  2. package/dist/cli/commandCatalog.js +1 -1
  3. package/dist/commands/taskCommands.js +63 -28
  4. package/dist/commands/taskContextCommand.js +27 -1
  5. package/dist/commands/taskRoleRuntimeStatus.js +17 -6
  6. package/dist/controller/agentRuntimeObserver.js +6 -3
  7. package/dist/controller/controller.js +29 -47
  8. package/dist/controller/fileSchedulerStoreAdapter.js +572 -258
  9. package/dist/controller/runtime.js +8 -1
  10. package/dist/controller/runtimeEventInbox.js +16 -5
  11. package/dist/controller/runtimeHookRunFence.js +51 -5
  12. package/dist/controller/runtimeObservationHook.js +8 -2
  13. package/dist/coordination/workMailbox.js +408 -28
  14. package/dist/coordination/workMailboxQueue.js +12 -10
  15. package/dist/executor/agentExecutor.js +101 -94
  16. package/dist/executor/executorRegistry.js +47 -2
  17. package/dist/executor/fileRoleLaunchPlanner.js +4 -2
  18. package/dist/lifecycle/exactRunTerminalization.js +1 -7
  19. package/dist/repository/taskWorkspaceCoordinator.js +9 -4
  20. package/dist/runtime/agentDriver.js +83 -4
  21. package/dist/runtime/agentDriverObservation.js +25 -10
  22. package/dist/runtime/builtinAgentDrivers.js +168 -18
  23. package/dist/runtime/codexAppServerRuntime.js +355 -0
  24. package/dist/runtime/continuationManager.js +117 -0
  25. package/dist/runtime/index.js +2 -0
  26. package/dist/runtime/lifecycleReservation.js +4 -3
  27. package/dist/runtime/promptEnvelope.js +14 -3
  28. package/dist/runtime/providerContinuation.js +225 -0
  29. package/dist/runtime/providerContinuationReconciliationService.js +172 -0
  30. package/dist/runtime/providerRuntimeIdentity.js +232 -0
  31. package/dist/runtime/providerRuntimeReconciler.js +166 -0
  32. package/dist/runtime/runtimeContinuationProjection.js +34 -0
  33. package/dist/runtime/runtimeObservation.js +217 -6
  34. package/dist/runtime/runtimeProjection.js +172 -11
  35. package/dist/scheduler/activeRoleRunDelivery.js +314 -1
  36. package/dist/scheduler/leaderWakeupProcessor.js +2 -1
  37. package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
  38. package/dist/scheduler/roleRunLiveness.js +8 -7
  39. package/dist/scheduler/roleRunStall.js +4 -2
  40. package/dist/scheduler/taskExecutionProjection.js +2 -2
  41. package/dist/storage/migration/productionRegistry.js +474 -1
  42. package/dist/storage/sqliteSchema.js +102 -21
  43. package/dist/storage/sqliteStore.js +52 -110
  44. package/dist/storage/storageVersions.js +1 -1
  45. package/dist/storage/storeRpc.js +0 -1
  46. package/dist/storage/taskStore.js +40 -53
  47. package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
  48. package/dist/task/nextAction.js +1 -1
  49. package/dist/web/assets/client/app.js +1 -1
  50. package/dist/web/assets/client/components.js +233 -4
  51. package/dist/web/assets/client/i18n.js +166 -2
  52. package/dist/web/assets/client/view.js +30 -13
  53. package/dist/web/assets/styles/cards.js +62 -0
  54. package/dist/web/assets/styles/widgets.js +1 -0
  55. package/dist/web/webSnapshot.js +11 -2
  56. package/package.json +1 -1
  57. package/skills/yui-leader/SKILL.md +33 -24
  58. package/skills/yui-operator/SKILL.md +7 -5
@@ -0,0 +1,117 @@
1
+ import { createProviderContinuation, observeProviderContinuation, providerContinuationKey, recordProviderReport } from "./providerContinuation.js";
2
+ import { createRuntimeObservation } from "./runtimeObservation.js";
3
+ export function foldContinuationObservation(existing, raw) {
4
+ const observation = createRuntimeObservation(raw);
5
+ if (!observation.kind.startsWith("continuation.")) {
6
+ throw new Error("ContinuationManager requires a continuation observation.");
7
+ }
8
+ const identity = continuationIdentity(observation);
9
+ const payload = observation.payload;
10
+ const base = existing ?? createProviderContinuation({
11
+ taskId: observation.fence.taskId,
12
+ roleName: observation.fence.roleName,
13
+ runId: observation.fence.runId,
14
+ identity,
15
+ ...(observation.fence.parentContinuationId === undefined
16
+ ? {}
17
+ : { parentContinuationId: observation.fence.parentContinuationId }),
18
+ attachment: payload.attachment,
19
+ observation: payload.observationQuality,
20
+ // A report is model-visible information, not settlement evidence. When
21
+ // it arrives before start, keep a conservative writer umbrella until an
22
+ // exact continuation.settled or complete native-work snapshot releases it.
23
+ mayWriteWorkspace: observation.kind === "continuation.reported"
24
+ ? true
25
+ : payload.mayWriteWorkspace,
26
+ observedAt: observation.observedAt ?? observation.receivedAt,
27
+ ...(observation.sequence === undefined ? {} : { providerSequence: observation.sequence })
28
+ });
29
+ if (existing !== null && providerContinuationKey(existing.identity) !== providerContinuationKey(identity)) {
30
+ throw new Error("Continuation observation identity does not match the existing projection.");
31
+ }
32
+ if (observation.kind === "continuation.reported") {
33
+ const next = recordProviderReport(base, {
34
+ reportId: payload.reportId,
35
+ ...(payload.resultRef === undefined ? {} : { resultRef: payload.resultRef }),
36
+ ...(payload.providerDeliveryRef === undefined
37
+ ? {}
38
+ : { providerDeliveryRef: payload.providerDeliveryRef }),
39
+ observedAt: observation.observedAt ?? observation.receivedAt
40
+ }, observation.sequence);
41
+ return {
42
+ disposition: next === base ? "duplicate" : "reported",
43
+ continuation: next
44
+ };
45
+ }
46
+ const next = observeProviderContinuation(base, {
47
+ execution: payload.execution,
48
+ outcome: payload.outcome,
49
+ attachment: payload.attachment,
50
+ observation: payload.observationQuality,
51
+ mayWriteWorkspace: payload.mayWriteWorkspace,
52
+ observedAt: observation.observedAt ?? observation.receivedAt,
53
+ ...(payload.resultRef === undefined ? {} : { resultRef: payload.resultRef }),
54
+ ...(observation.sequence === undefined ? {} : { providerSequence: observation.sequence })
55
+ });
56
+ return {
57
+ disposition: next === base
58
+ ? existing === null ? "created" : "duplicate"
59
+ : next.identityConflict ? "conflict"
60
+ : observation.kind === "continuation.settled" ? "settled"
61
+ : existing === null ? "created" : "updated",
62
+ continuation: next
63
+ };
64
+ }
65
+ /**
66
+ * Commits the semantic continuation fact and its Leader notification before
67
+ * releasing child ownership. The surrounding TaskStore transaction is the
68
+ * crash boundary; a replay with the same semanticKey becomes a no-op.
69
+ */
70
+ export function applyContinuationObservationAtomically(store, raw, factRef) {
71
+ const observation = createRuntimeObservation(raw);
72
+ const identity = continuationIdentity(observation);
73
+ return store.transaction((tx) => {
74
+ const result = foldContinuationObservation(tx.getProviderContinuation(providerContinuationKey(identity)), observation);
75
+ if (!tx.appendContinuationFactOnce(observation.semanticKey, observation, result.continuation)) {
76
+ return { disposition: "duplicate", continuation: result.continuation };
77
+ }
78
+ tx.saveProviderContinuation(result.continuation);
79
+ if (result.disposition === "reported" || result.disposition === "settled"
80
+ || result.disposition === "conflict") {
81
+ tx.enqueueContinuationSignal({
82
+ taskId: result.continuation.taskId,
83
+ roleName: "leader",
84
+ signal: {
85
+ reason: result.disposition === "reported"
86
+ ? "provider-continuation-report"
87
+ : result.disposition === "settled"
88
+ ? "provider-continuation-settled"
89
+ : "provider-continuation-identity-conflict",
90
+ refs: [factRef],
91
+ occurredAt: observation.observedAt ?? observation.receivedAt,
92
+ source: "continuation-manager",
93
+ dedupeKey: observation.semanticKey,
94
+ deliveryMode: "followup",
95
+ lane: "normal"
96
+ }
97
+ });
98
+ }
99
+ return result;
100
+ });
101
+ }
102
+ function continuationIdentity(observation) {
103
+ const fence = observation.fence;
104
+ if (fence.taskId === undefined || fence.runId === undefined
105
+ || fence.conversationId === undefined || fence.activationId === undefined
106
+ || fence.continuationId === undefined) {
107
+ throw new Error("Continuation observation fence is incomplete.");
108
+ }
109
+ return {
110
+ providerNamespace: fence.driverId,
111
+ accountScope: fence.agentId,
112
+ conversationId: fence.conversationId,
113
+ activationId: fence.activationId,
114
+ continuationId: fence.continuationId,
115
+ generation: fence.continuationGeneration
116
+ };
117
+ }
@@ -9,4 +9,6 @@ export { FileTaskRuntimeIsolation, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR, YUI_TA
9
9
  export { createSessionOwnerIdentity, discoverProviderRootByLaunchEnv, isLinuxProcessLive, listLaunchFencedProcesses, listOwnedProcessTree, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
10
10
  export { FileSessionOwnerRegistry } from "./sessionOwnerRegistry.js";
11
11
  export { DEFAULT_FORCED_GRACE_MS, DEFAULT_GRACEFUL_GRACE_MS, terminateSessionOwners } from "./sessionTerminationGuard.js";
12
+ export { ProviderContinuationReconciliationService } from "./providerContinuationReconciliationService.js";
13
+ export { codexNotificationBoundary, CodexAppServerRequestError, CodexAppServerRuntime } from "./codexAppServerRuntime.js";
12
14
  export { reconcileSessionOwners } from "./sessionReconciliation.js";
@@ -1,3 +1,4 @@
1
+ import { mailboxHasWork } from "../coordination/workMailbox.js";
1
2
  export const RUNTIME_LIFECYCLE_OWNER = "runtime-lifecycle";
2
3
  export const RUNTIME_LAUNCH_RESERVED_REASON = "runtime-launch-reserved";
3
4
  export const RUNTIME_CLEANUP_REQUIRED_REASON = "runtime-cleanup-required";
@@ -28,11 +29,11 @@ export function hasRuntimeLaunchReservation(mailbox) {
28
29
  return isRuntimeLaunchReservation(mailbox?.processing);
29
30
  }
30
31
  export function hasRuntimeCleanupObligation(mailbox) {
31
- return mailbox?.pending?.reasons.includes(RUNTIME_CLEANUP_REQUIRED_REASON) === true
32
+ const pending = mailbox?.pending.normal;
33
+ return pending?.reasons.includes(RUNTIME_CLEANUP_REQUIRED_REASON) === true
32
34
  || (!isRuntimeLaunchReservation(mailbox?.processing)
33
35
  && mailbox?.processing?.batch.reasons.includes(RUNTIME_CLEANUP_REQUIRED_REASON) === true);
34
36
  }
35
37
  export function hasRuntimeLifecycleWork(mailbox) {
36
- return mailbox !== null
37
- && (mailbox.processing !== null || mailbox.pending !== null);
38
+ return mailbox !== null && mailboxHasWork(mailbox);
38
39
  }
@@ -1,13 +1,15 @@
1
1
  import { requireText, requireTimestamp } from "./validation.js";
2
2
  import { formatAgentRunReceiptId, formatInputRequestReceiptId, validateTaskRecordReference } from "../task/taskRecordReference.js";
3
3
  export function createPromptEnvelope(input) {
4
- if (input.source.kind !== "agent-run" && input.source.kind !== "input-request") {
4
+ if (input.source.kind !== "agent-run"
5
+ && input.source.kind !== "run-input"
6
+ && input.source.kind !== "input-request") {
5
7
  throw new Error("Prompt source kind is invalid.");
6
8
  }
7
9
  const source = validateTaskRecordReference({
8
10
  taskId: input.source.taskId,
9
11
  localId: input.source.localId
10
- }, input.source.kind === "agent-run" ? "agentRun" : "inputRequest");
12
+ }, input.source.kind === "input-request" ? "inputRequest" : "agentRun");
11
13
  const id = requireQualifiedReceiptId(input.id, input.source.kind, source.taskId, source.localId);
12
14
  return {
13
15
  id,
@@ -23,7 +25,16 @@ export function createPromptEnvelope(input) {
23
25
  function requireQualifiedReceiptId(value, kind, taskId, localId) {
24
26
  const expected = kind === "agent-run"
25
27
  ? formatAgentRunReceiptId(taskId, localId)
26
- : formatInputRequestReceiptId(taskId, localId);
28
+ : kind === "input-request"
29
+ ? formatInputRequestReceiptId(taskId, localId)
30
+ : `agent-input:${taskId}/${localId}/`;
31
+ if (kind === "run-input") {
32
+ if (!value.startsWith(expected)
33
+ || !/^(normal|user-correction):[1-9]\d*-[1-9]\d*$/.test(value.slice(expected.length))) {
34
+ throw new Error("Prompt envelope id does not match its source.");
35
+ }
36
+ return value;
37
+ }
27
38
  if (value !== expected)
28
39
  throw new Error("Prompt envelope id does not match its source.");
29
40
  return expected;
@@ -0,0 +1,225 @@
1
+ export function providerContinuationKey(identity) {
2
+ const normalized = validateProviderContinuationIdentity(identity);
3
+ return [
4
+ normalized.providerNamespace,
5
+ normalized.accountScope,
6
+ normalized.conversationId,
7
+ normalized.activationId,
8
+ normalized.continuationId,
9
+ normalized.generation
10
+ ].join("\u0000");
11
+ }
12
+ export function createProviderContinuation(input) {
13
+ return validateProviderContinuation({
14
+ schemaVersion: 1,
15
+ taskId: text(input.taskId, "Task id"),
16
+ roleName: text(input.roleName, "Role name"),
17
+ runId: text(input.runId, "Run id"),
18
+ identity: validateProviderContinuationIdentity(input.identity),
19
+ ...(input.parentContinuationId === undefined
20
+ ? {}
21
+ : { parentContinuationId: text(input.parentContinuationId, "Parent Continuation id") }),
22
+ execution: "active",
23
+ outcome: "pending",
24
+ attachment: input.attachment,
25
+ observation: input.observation,
26
+ mayWriteWorkspace: input.mayWriteWorkspace,
27
+ reports: [],
28
+ ...(input.providerSequence === undefined ? {} : { lastProviderSequence: input.providerSequence }),
29
+ identityConflict: false,
30
+ createdAt: timestamp(input.observedAt, "Continuation observedAt"),
31
+ updatedAt: input.observedAt
32
+ });
33
+ }
34
+ export function recordProviderReport(raw, report, providerSequence) {
35
+ const current = validateProviderContinuation(raw);
36
+ if (current.identityConflict)
37
+ return current;
38
+ if (providerSequenceRegresses(current, providerSequence))
39
+ return current;
40
+ const normalized = validateProviderReport(report);
41
+ if (current.reports.some((entry) => entry.reportId === normalized.reportId))
42
+ return current;
43
+ return validateProviderContinuation({
44
+ ...current,
45
+ reports: [...current.reports, normalized],
46
+ ...(providerSequence === undefined ? {} : { lastProviderSequence: providerSequence }),
47
+ updatedAt: normalized.observedAt
48
+ });
49
+ }
50
+ export function observeProviderContinuation(raw, input) {
51
+ const current = validateProviderContinuation(raw);
52
+ if (current.identityConflict || providerSequenceRegresses(current, input.providerSequence)) {
53
+ return current;
54
+ }
55
+ const observedAt = timestamp(input.observedAt, "Continuation observedAt");
56
+ if (current.settledAt !== undefined) {
57
+ if (input.execution === "quiescent"
58
+ && input.outcome === current.outcome
59
+ && (input.resultRef === undefined || input.resultRef === current.resultRef))
60
+ return current;
61
+ // Exact terminal evidence may fill a result reference that was indexed
62
+ // after settlement, but it can never replace an existing reference.
63
+ if (input.execution === "quiescent"
64
+ && input.observation === "exact"
65
+ && input.outcome === current.outcome
66
+ && current.resultRef === undefined
67
+ && input.resultRef !== undefined) {
68
+ return validateProviderContinuation({
69
+ ...current,
70
+ resultRef: text(input.resultRef, "Result ref"),
71
+ ...(input.providerSequence === undefined
72
+ ? {}
73
+ : { lastProviderSequence: input.providerSequence }),
74
+ updatedAt: observedAt
75
+ });
76
+ }
77
+ // Exact absence/completeness can close writer ownership before the
78
+ // provider's terminal result record arrives. A later exact terminal fact
79
+ // may refine only the unknown outcome; it cannot reopen execution.
80
+ if (current.outcome === "unknown"
81
+ && input.execution === "quiescent"
82
+ && input.observation === "exact"
83
+ && input.outcome !== "pending") {
84
+ return validateProviderContinuation({
85
+ ...current,
86
+ outcome: input.outcome,
87
+ ...(input.resultRef === undefined ? {} : { resultRef: text(input.resultRef, "Result ref") }),
88
+ ...(input.providerSequence === undefined
89
+ ? {}
90
+ : { lastProviderSequence: input.providerSequence }),
91
+ updatedAt: observedAt
92
+ });
93
+ }
94
+ return validateProviderContinuation({ ...current, identityConflict: true, updatedAt: observedAt });
95
+ }
96
+ if (input.execution === "quiescent" && input.observation !== "exact") {
97
+ return validateProviderContinuation({
98
+ ...current,
99
+ execution: "unknown",
100
+ observation: input.observation,
101
+ attachment: input.attachment,
102
+ // Partial/unavailable evidence cannot release an already-established
103
+ // writer umbrella. Only exact quiescence may prove workspace ownership
104
+ // ended; otherwise cleanup could race a detached provider process.
105
+ mayWriteWorkspace: current.mayWriteWorkspace || input.mayWriteWorkspace,
106
+ ...(input.providerSequence === undefined ? {} : { lastProviderSequence: input.providerSequence }),
107
+ updatedAt: observedAt
108
+ });
109
+ }
110
+ return validateProviderContinuation({
111
+ ...current,
112
+ execution: input.execution,
113
+ outcome: input.outcome,
114
+ attachment: input.attachment,
115
+ observation: input.observation,
116
+ mayWriteWorkspace: input.mayWriteWorkspace,
117
+ ...(input.resultRef === undefined ? {} : { resultRef: text(input.resultRef, "Result ref") }),
118
+ ...(input.execution === "quiescent" && input.observation === "exact"
119
+ ? { settledAt: observedAt }
120
+ : {}),
121
+ ...(input.providerSequence === undefined ? {} : { lastProviderSequence: input.providerSequence }),
122
+ updatedAt: observedAt
123
+ });
124
+ }
125
+ export function detachProviderContinuation(raw, observedAt) {
126
+ const current = validateProviderContinuation(raw);
127
+ if (current.attachment === "detached")
128
+ return current;
129
+ return validateProviderContinuation({
130
+ ...current,
131
+ attachment: "detached",
132
+ updatedAt: timestamp(observedAt, "Continuation detachedAt")
133
+ });
134
+ }
135
+ export function continuationOwnsWriterUmbrella(value) {
136
+ const continuation = validateProviderContinuation(value);
137
+ return continuation.mayWriteWorkspace
138
+ && continuation.identityConflict === false
139
+ && (continuation.execution === "active" || continuation.execution === "unknown");
140
+ }
141
+ export function validateProviderContinuation(value) {
142
+ if (value.schemaVersion !== 1)
143
+ throw new Error("Provider Continuation schemaVersion must be 1.");
144
+ text(value.taskId, "Task id");
145
+ text(value.roleName, "Role name");
146
+ text(value.runId, "Run id");
147
+ validateProviderContinuationIdentity(value.identity);
148
+ if (!["active", "quiescent", "unknown"].includes(value.execution)) {
149
+ throw new Error("Provider Continuation execution is invalid.");
150
+ }
151
+ if (!["pending", "succeeded", "failed", "cancelled", "unknown"].includes(value.outcome)) {
152
+ throw new Error("Provider Continuation outcome is invalid.");
153
+ }
154
+ if (value.attachment !== "attached" && value.attachment !== "detached") {
155
+ throw new Error("Provider Continuation attachment is invalid.");
156
+ }
157
+ if (!["exact", "partial", "unavailable"].includes(value.observation)) {
158
+ throw new Error("Provider Continuation observation quality is invalid.");
159
+ }
160
+ if (typeof value.mayWriteWorkspace !== "boolean" || typeof value.identityConflict !== "boolean") {
161
+ throw new Error("Provider Continuation flags are invalid.");
162
+ }
163
+ const reports = value.reports.map(validateProviderReport);
164
+ if (new Set(reports.map((entry) => entry.reportId)).size !== reports.length) {
165
+ throw new Error("Provider Continuation reports contain duplicate identity.");
166
+ }
167
+ timestamp(value.createdAt, "Provider Continuation createdAt");
168
+ timestamp(value.updatedAt, "Provider Continuation updatedAt");
169
+ if (value.settledAt !== undefined) {
170
+ timestamp(value.settledAt, "Provider Continuation settledAt");
171
+ if (value.execution !== "quiescent" || value.observation !== "exact") {
172
+ throw new Error("Settled Provider Continuation requires exact quiescence.");
173
+ }
174
+ }
175
+ if (value.execution === "active" && value.outcome !== "pending") {
176
+ throw new Error("Active Provider Continuation outcome must remain pending.");
177
+ }
178
+ if (value.lastProviderSequence !== undefined)
179
+ sequence(value.lastProviderSequence);
180
+ return value;
181
+ }
182
+ export function validateProviderContinuationIdentity(value) {
183
+ text(value.providerNamespace, "Provider namespace");
184
+ text(value.accountScope, "Provider account scope");
185
+ text(value.conversationId, "Provider Conversation id");
186
+ text(value.activationId, "Provider Activation id");
187
+ text(value.continuationId, "Provider Continuation id");
188
+ if (!Number.isSafeInteger(value.generation) || value.generation < 1) {
189
+ throw new Error("Provider Continuation generation is invalid.");
190
+ }
191
+ return value;
192
+ }
193
+ function validateProviderReport(value) {
194
+ return {
195
+ reportId: text(value.reportId, "Provider report id"),
196
+ ...(value.resultRef === undefined ? {} : { resultRef: text(value.resultRef, "Result ref") }),
197
+ ...(value.providerDeliveryRef === undefined
198
+ ? {}
199
+ : { providerDeliveryRef: text(value.providerDeliveryRef, "Provider delivery ref") }),
200
+ observedAt: timestamp(value.observedAt, "Provider report observedAt")
201
+ };
202
+ }
203
+ function providerSequenceRegresses(current, incoming) {
204
+ if (incoming === undefined)
205
+ return false;
206
+ sequence(incoming);
207
+ return current.lastProviderSequence !== undefined && incoming < current.lastProviderSequence;
208
+ }
209
+ function sequence(value) {
210
+ if (!Number.isSafeInteger(value) || value < 0)
211
+ throw new Error("Provider sequence is invalid.");
212
+ return value;
213
+ }
214
+ function text(value, label) {
215
+ if (typeof value !== "string" || value.includes("\0") || value.trim().length === 0) {
216
+ throw new Error(`${label} is invalid.`);
217
+ }
218
+ return value.trim();
219
+ }
220
+ function timestamp(value, label) {
221
+ const normalized = text(value, label);
222
+ if (!Number.isFinite(Date.parse(normalized)))
223
+ throw new Error(`${label} must be a timestamp.`);
224
+ return normalized;
225
+ }
@@ -0,0 +1,172 @@
1
+ import { createHash } from "node:crypto";
2
+ import { providerContinuationKey } from "./providerContinuation.js";
3
+ import { projectProviderContinuations } from "./runtimeContinuationProjection.js";
4
+ import { reconcileKnownDetachedContinuations } from "./providerRuntimeReconciler.js";
5
+ import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
6
+ /**
7
+ * Low-frequency, metadata-only recovery for already-known detached children.
8
+ * No launch/model method is reachable through this object. The backoff is an
9
+ * advisory cache; committed observations remain the only durable authority.
10
+ */
11
+ export class ProviderContinuationReconciliationService {
12
+ store;
13
+ sink;
14
+ metadata;
15
+ #schedules = new Map();
16
+ constructor(store, sink, metadata) {
17
+ this.store = store;
18
+ this.sink = sink;
19
+ this.metadata = metadata;
20
+ }
21
+ async reconcile(now) {
22
+ const changedTaskIds = new Set();
23
+ for (const task of this.store.listTasks().filter((entry) => entry.status === "active")) {
24
+ const events = this.store.listEvents(task.id);
25
+ const groups = groupDetachedContinuations(projectProviderContinuations(events), events.map(runtimeObservationFromTaskEvent)
26
+ .filter((entry) => entry !== null));
27
+ for (const [groupKey, candidates] of groups) {
28
+ const group = candidates.map(({ continuation }) => continuation);
29
+ const previous = this.#schedules.get(groupKey);
30
+ if (previous !== undefined && Date.parse(previous.nextReconcileAt) > now.getTime()) {
31
+ continue;
32
+ }
33
+ let result;
34
+ try {
35
+ result = await reconcileKnownDetachedContinuations({
36
+ port: this.metadata,
37
+ continuations: group,
38
+ ...(previous === undefined ? {} : { previous }),
39
+ now
40
+ });
41
+ }
42
+ catch {
43
+ // One malformed identity group must not restart or block the
44
+ // Controller. Keep it writer-owned and retry through the same
45
+ // bounded, metadata-only schedule.
46
+ this.#schedules.set(groupKey, failureSchedule(groupKey, previous, now));
47
+ continue;
48
+ }
49
+ if (result.schedule === null)
50
+ this.#schedules.delete(groupKey);
51
+ else
52
+ this.#schedules.set(groupKey, result.schedule);
53
+ for (let index = 0; index < group.length; index += 1) {
54
+ const before = group[index];
55
+ const after = result.continuations[index];
56
+ if (sameContinuationState(before, after))
57
+ continue;
58
+ const disposition = this.sink.observeRuntimeObservation(reconciliationObservation(after, candidates[index].fence, now), now);
59
+ if (disposition === "applied")
60
+ changedTaskIds.add(after.taskId);
61
+ }
62
+ }
63
+ }
64
+ return Object.freeze([...changedTaskIds].sort());
65
+ }
66
+ }
67
+ function failureSchedule(key, previous, now) {
68
+ const attempts = (previous?.attempts ?? 0) + 1;
69
+ const errors = (previous?.consecutiveErrors ?? 0) + 1;
70
+ const delay = Math.min(5 * 60_000, 2_000 * (2 ** Math.min(attempts - 1, 8)));
71
+ return Object.freeze({
72
+ key,
73
+ attempts,
74
+ consecutiveErrors: errors,
75
+ nextReconcileAt: new Date(now.getTime() + delay).toISOString(),
76
+ ...(errors < 5
77
+ ? {}
78
+ : { circuitOpenUntil: new Date(now.getTime() + 5 * 60_000).toISOString() })
79
+ });
80
+ }
81
+ function groupDetachedContinuations(continuations, observations) {
82
+ const groups = new Map();
83
+ for (const continuation of continuations) {
84
+ if (continuation.attachment !== "detached"
85
+ || continuation.identityConflict
86
+ || (continuation.execution !== "active" && continuation.execution !== "unknown")) {
87
+ continue;
88
+ }
89
+ const key = [
90
+ continuation.identity.providerNamespace,
91
+ continuation.identity.accountScope,
92
+ continuation.identity.conversationId,
93
+ continuation.identity.activationId
94
+ ].join("\u0000");
95
+ const source = [...observations].reverse().find((observation) => (observation.kind.startsWith("continuation.")
96
+ && observation.fence.driverId === continuation.identity.providerNamespace
97
+ && observation.fence.agentId === continuation.identity.accountScope
98
+ && observation.fence.conversationId === continuation.identity.conversationId
99
+ && observation.fence.activationId === continuation.identity.activationId
100
+ && observation.fence.continuationId === continuation.identity.continuationId
101
+ && observation.fence.continuationGeneration === continuation.identity.generation));
102
+ // A projected continuation without its original durable fence cannot be
103
+ // safely attached to a live Run. Keep ownership conservative and let the
104
+ // malformed identity remain visible instead of synthesizing a receipt.
105
+ if (source === undefined)
106
+ continue;
107
+ const group = groups.get(key) ?? [];
108
+ group.push({ continuation, fence: source.fence });
109
+ groups.set(key, group);
110
+ }
111
+ return groups;
112
+ }
113
+ function sameContinuationState(left, right) {
114
+ return left.execution === right.execution
115
+ && left.outcome === right.outcome
116
+ && left.attachment === right.attachment
117
+ && left.observation === right.observation
118
+ && left.mayWriteWorkspace === right.mayWriteWorkspace
119
+ && left.resultRef === right.resultRef
120
+ && left.lastProviderSequence === right.lastProviderSequence
121
+ && left.identityConflict === right.identityConflict;
122
+ }
123
+ function reconciliationObservation(continuation, sourceFence, now) {
124
+ const key = providerContinuationKey(continuation.identity);
125
+ const state = [
126
+ continuation.execution,
127
+ continuation.outcome,
128
+ continuation.observation,
129
+ continuation.mayWriteWorkspace ? "writer" : "read-only",
130
+ continuation.resultRef ?? "none",
131
+ continuation.lastProviderSequence ?? "none"
132
+ ].join(":");
133
+ const digest = createHash("sha256").update(`${key}\u0000${state}`).digest("hex");
134
+ const settled = continuation.execution === "quiescent"
135
+ && continuation.observation === "exact";
136
+ return createRuntimeObservation({
137
+ schemaVersion: 2,
138
+ eventId: `continuation-reconcile:${digest}`,
139
+ semanticKey: `continuation-reconcile:${digest}`,
140
+ kind: settled ? "continuation.settled" : "continuation.started",
141
+ authority: "provider-structured",
142
+ receivedAt: now.toISOString(),
143
+ observedAt: now.toISOString(),
144
+ ...(continuation.lastProviderSequence === undefined
145
+ ? {}
146
+ : { sequence: continuation.lastProviderSequence }),
147
+ fence: {
148
+ ...sourceFence,
149
+ taskId: continuation.taskId,
150
+ roleName: continuation.roleName,
151
+ runId: continuation.runId,
152
+ agentId: continuation.identity.accountScope,
153
+ driverId: continuation.identity.providerNamespace,
154
+ conversationId: continuation.identity.conversationId,
155
+ activationId: continuation.identity.activationId,
156
+ nativeSessionId: continuation.identity.conversationId,
157
+ continuationId: continuation.identity.continuationId,
158
+ continuationGeneration: continuation.identity.generation,
159
+ ...(continuation.parentContinuationId === undefined
160
+ ? {}
161
+ : { parentContinuationId: continuation.parentContinuationId })
162
+ },
163
+ payload: {
164
+ execution: continuation.execution,
165
+ outcome: continuation.outcome,
166
+ attachment: "detached",
167
+ observationQuality: continuation.observation,
168
+ mayWriteWorkspace: continuation.mayWriteWorkspace,
169
+ ...(continuation.resultRef === undefined ? {} : { resultRef: continuation.resultRef })
170
+ }
171
+ });
172
+ }