conductor-remote 1.116.0 → 1.118.0

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 (200) hide show
  1. package/README.md +10 -8
  2. package/dist/assets/{PierrePatch-BPp5Cf-p.js → PierrePatch-D4YbKD8Q.js} +1 -1
  3. package/dist/assets/index-BRzsSW8g.css +1 -0
  4. package/dist/assets/index-BaS6qXUW.js +65 -0
  5. package/dist/index.html +2 -2
  6. package/dist/push-sw.js +2 -2
  7. package/dist/sw.js +1 -1
  8. package/dist-node/scripts/nosleep-setup.js +2 -2
  9. package/dist-node/scripts/nosleep.js +3 -3
  10. package/dist-node/scripts/service/commands.js +32 -0
  11. package/dist-node/scripts/service/configuration.js +205 -0
  12. package/dist-node/scripts/service/environment.js +59 -0
  13. package/dist-node/scripts/service/flags.js +45 -0
  14. package/dist-node/scripts/service/installation.js +91 -0
  15. package/dist-node/scripts/service/launch-agent.js +134 -0
  16. package/dist-node/scripts/service/network.js +244 -0
  17. package/dist-node/scripts/service/presentation.js +101 -0
  18. package/dist-node/scripts/service/voice.js +113 -0
  19. package/dist-node/scripts/service.js +5 -980
  20. package/dist-node/src/{agent-config.js → agents/agent-config.js} +1 -1
  21. package/dist-node/src/{model-cache.js → agents/model-cache.js} +1 -1
  22. package/dist-node/src/{roles.js → agents/roles.js} +3 -3
  23. package/dist-node/src/db.js +1 -1
  24. package/dist-node/src/{parked.js → delivery/parked.js} +1 -1
  25. package/dist-node/src/{dev-server.js → dev-server/controller.js} +8 -291
  26. package/dist-node/src/dev-server/ports.js +63 -0
  27. package/dist-node/src/dev-server/processes.js +73 -0
  28. package/dist-node/src/dev-server/proxy.js +158 -0
  29. package/dist-node/src/{run-activity.js → dev-server/run-activity.js} +2 -2
  30. package/dist-node/src/dev-server/types.js +1 -0
  31. package/dist-node/src/{file-preview.js → files/file-preview.js} +1 -1
  32. package/dist-node/src/{change-stats.js → git/change-stats.js} +1 -1
  33. package/dist-node/src/{git.js → git/diff.js} +1 -1
  34. package/dist-node/src/{autoupdate.js → host/autoupdate.js} +1 -1
  35. package/dist-node/src/{funnel-watchdog.js → host/funnel-watchdog.js} +4 -4
  36. package/dist-node/src/{logbuf.js → host/logbuf.js} +1 -1
  37. package/dist-node/src/{nosleep-helper.js → host/nosleep-helper.js} +1 -1
  38. package/dist-node/src/{tailscale.js → host/tailscale.js} +1 -1
  39. package/dist-node/src/http/router-types.js +2 -0
  40. package/dist-node/src/http/router.js +78 -0
  41. package/dist-node/src/http/routes/create-workspace.js +90 -0
  42. package/dist-node/src/http/routes/files.js +53 -0
  43. package/dist-node/src/http/routes/prompts.js +363 -0
  44. package/dist-node/src/http/routes/sessions.js +279 -0
  45. package/dist-node/src/http/routes/state.js +186 -0
  46. package/dist-node/src/http/routes/system.js +220 -0
  47. package/dist-node/src/http/routes/voice.js +117 -0
  48. package/dist-node/src/http/routes/workflows.js +248 -0
  49. package/dist-node/src/http/routes/workspaces.js +267 -0
  50. package/dist-node/src/http/services/base.js +252 -0
  51. package/dist-node/src/http/services/delegations.js +301 -0
  52. package/dist-node/src/http/services/delivery.js +433 -0
  53. package/dist-node/src/http/services/files.js +199 -0
  54. package/dist-node/src/http/services/mcp.js +94 -0
  55. package/dist-node/src/http/services/responses.js +120 -0
  56. package/dist-node/src/http/services/voice.js +147 -0
  57. package/dist-node/src/http/services/workflow-probes.js +215 -0
  58. package/dist-node/src/http/services/workflow-state.js +195 -0
  59. package/dist-node/src/http/services/workflow.js +366 -0
  60. package/dist-node/src/http/services.js +35 -0
  61. package/dist-node/src/mcp/arguments.js +14 -0
  62. package/dist-node/src/mcp/dispatcher.js +56 -0
  63. package/dist-node/src/mcp/formatters.js +110 -0
  64. package/dist-node/src/mcp/protocol.js +7 -0
  65. package/dist-node/src/mcp/registry.js +66 -0
  66. package/dist-node/src/mcp/tools/agent-options.js +122 -0
  67. package/dist-node/src/mcp/tools/chat-actions.js +194 -0
  68. package/dist-node/src/mcp/tools/chats.js +194 -0
  69. package/dist-node/src/mcp/tools/delegation.js +135 -0
  70. package/dist-node/src/mcp/tools/system.js +135 -0
  71. package/dist-node/src/mcp/tools/voice.js +136 -0
  72. package/dist-node/src/mcp/tools/workspaces.js +259 -0
  73. package/dist-node/src/mcp/types.js +1 -0
  74. package/dist-node/src/mcp.js +6 -4
  75. package/dist-node/src/{notify.js → notifications/notify.js} +5 -5
  76. package/dist-node/src/{webpush.js → notifications/webpush.js} +1 -1
  77. package/dist-node/src/orchestration/delegation/codec.js +201 -0
  78. package/dist-node/src/{delegation-intake.js → orchestration/delegation/intake.js} +2 -2
  79. package/dist-node/src/{delegation-prompt.js → orchestration/delegation/prompt.js} +1 -1
  80. package/dist-node/src/orchestration/delegation/queue.js +255 -0
  81. package/dist-node/src/orchestration/delegation/store.js +111 -0
  82. package/dist-node/src/orchestration/delegation/types.js +2 -0
  83. package/dist-node/src/orchestration/persistence/capabilities.js +109 -0
  84. package/dist-node/src/orchestration/persistence/codecs.js +148 -0
  85. package/dist-node/src/orchestration/persistence/connection.js +205 -0
  86. package/dist-node/src/orchestration/persistence/db.js +101 -0
  87. package/dist-node/src/orchestration/persistence/effect-recovery.js +227 -0
  88. package/dist-node/src/orchestration/persistence/effect-transitions.js +157 -0
  89. package/dist-node/src/orchestration/persistence/effects.js +109 -0
  90. package/dist-node/src/orchestration/persistence/errors.js +39 -0
  91. package/dist-node/src/orchestration/persistence/events.js +29 -0
  92. package/dist-node/src/orchestration/persistence/idempotency.js +82 -0
  93. package/dist-node/src/orchestration/persistence/job-attempts.js +114 -0
  94. package/dist-node/src/orchestration/persistence/jobs.js +159 -0
  95. package/dist-node/src/orchestration/persistence/projections.js +190 -0
  96. package/dist-node/src/orchestration/persistence/quarantine.js +46 -0
  97. package/dist-node/src/orchestration/persistence/records.js +115 -0
  98. package/dist-node/src/orchestration/persistence/relays.js +78 -0
  99. package/dist-node/src/orchestration/persistence/runs.js +156 -0
  100. package/dist-node/src/{orchestration-schema.js → orchestration/persistence/schema.js} +1 -1
  101. package/dist-node/src/orchestration/persistence/types.js +1 -0
  102. package/dist-node/src/orchestration/persistence/ui-lease.js +197 -0
  103. package/dist-node/src/orchestration/persistence/values.js +114 -0
  104. package/dist-node/src/orchestration/workflow/commands.js +309 -0
  105. package/dist-node/src/orchestration/workflow/coordinator.js +28 -0
  106. package/dist-node/src/orchestration/workflow/effect-recovery.js +125 -0
  107. package/dist-node/src/orchestration/workflow/effects.js +307 -0
  108. package/dist-node/src/orchestration/workflow/errors.js +13 -0
  109. package/dist-node/src/orchestration/workflow/helpers.js +154 -0
  110. package/dist-node/src/orchestration/workflow/job-dispatch.js +312 -0
  111. package/dist-node/src/orchestration/workflow/job-results.js +280 -0
  112. package/dist-node/src/{workflow.js → orchestration/workflow/prompts.js} +2 -2
  113. package/dist-node/src/orchestration/workflow/root.js +293 -0
  114. package/dist-node/src/orchestration/workflow/start.js +99 -0
  115. package/dist-node/src/orchestration/workflow/state.js +105 -0
  116. package/dist-node/src/orchestration/workflow/types.js +1 -0
  117. package/dist-node/src/orchestration/workflow/wake.js +164 -0
  118. package/dist-node/src/prefs.js +1 -1
  119. package/dist-node/src/{background-tasks.js → reads/background-tasks.js} +1 -1
  120. package/dist-node/src/reads/messages.js +362 -0
  121. package/dist-node/src/reads/repository.js +87 -0
  122. package/dist-node/src/reads/sessions.js +165 -0
  123. package/dist-node/src/reads/types.js +1 -0
  124. package/dist-node/src/reads/workspaces.js +230 -0
  125. package/dist-node/src/reads/worktrees.js +45 -0
  126. package/dist-node/src/routes.js +5 -5
  127. package/dist-node/src/{search.js → search/coordinator.js} +4 -4
  128. package/dist-node/src/{search-worker.js → search/worker.js} +3 -3
  129. package/dist-node/src/server.js +14 -3930
  130. package/dist-node/src/settings.js +1 -1
  131. package/dist-node/src/shared.js +130 -9
  132. package/dist-node/src/{transcript.js → transcript/parser.js} +4 -124
  133. package/dist-node/src/{tool-usage-worker.js → usage/tool-usage-worker.js} +1 -1
  134. package/dist-node/src/{tool-usage.js → usage/tool-usage.js} +1 -1
  135. package/dist-node/src/voice/brief.js +22 -50
  136. package/dist-node/src/voice/context.js +1 -1
  137. package/dist-node/src/voice/dates.js +38 -0
  138. package/dist-node/src/voice/history.js +14 -5
  139. package/dist-node/src/voice/prompt.js +10 -9
  140. package/dist-node/src/voice/recall.js +115 -0
  141. package/dist-node/src/{speech.js → voice/speech.js} +1 -1
  142. package/dist-node/src/voice/tools.js +92 -3
  143. package/dist-node/src/voice/webrtc.js +1 -1
  144. package/dist-node/src/wire.js +2 -2
  145. package/dist-node/src/writes/actuator.js +124 -0
  146. package/dist-node/src/writes/agent-options.js +151 -0
  147. package/dist-node/src/writes/applescript/agent-options.applescript +334 -0
  148. package/dist-node/src/writes/applescript/chat-targeting.applescript +149 -0
  149. package/dist-node/src/writes/applescript/composer.applescript +195 -0
  150. package/dist-node/src/writes/applescript/hotspot.applescript +155 -0
  151. package/dist-node/src/writes/applescript/lifecycle.applescript +266 -0
  152. package/dist-node/src/writes/applescript/run-tasks.applescript +208 -0
  153. package/dist-node/src/writes/applescript/source.js +52 -0
  154. package/dist-node/src/writes/applescript/window.applescript +436 -0
  155. package/dist-node/src/writes/applescript/workspace-status.applescript +177 -0
  156. package/dist-node/src/writes/applescript/workspace-targeting.applescript +359 -0
  157. package/dist-node/src/writes/chats.js +160 -0
  158. package/dist-node/src/writes/guards.js +77 -0
  159. package/dist-node/src/writes/runner.js +67 -0
  160. package/dist-node/src/{sidecar.js → writes/sidecar.js} +1 -1
  161. package/dist-node/src/writes/system.js +81 -0
  162. package/dist-node/src/writes/targeting.js +121 -0
  163. package/dist-node/src/writes/types.js +1 -0
  164. package/dist-node/src/writes/ui-lock.js +229 -0
  165. package/dist-node/src/writes/workspaces.js +233 -0
  166. package/docs/voice-setup.md +34 -12
  167. package/package.json +2 -2
  168. package/dist/assets/index-DNLdIfZ2.css +0 -1
  169. package/dist/assets/index-QJR7tn2I.js +0 -61
  170. package/dist-node/src/conductor.applescript +0 -2279
  171. package/dist-node/src/delegations.js +0 -557
  172. package/dist-node/src/mcp-tools.js +0 -1319
  173. package/dist-node/src/orchestration-db.js +0 -2186
  174. package/dist-node/src/reads.js +0 -787
  175. package/dist-node/src/workflow-coordinator.js +0 -2119
  176. package/dist-node/src/writes.js +0 -1223
  177. /package/dist-node/src/{conductor-settings.js → agents/conductor-settings.js} +0 -0
  178. /package/dist-node/src/{firstprompt.js → delivery/firstprompt.js} +0 -0
  179. /package/dist-node/src/{sendonce.js → delivery/sendonce.js} +0 -0
  180. /package/dist-node/src/{preview-urls.js → dev-server/preview-urls.js} +0 -0
  181. /package/dist-node/src/{run-configs.js → dev-server/run-configs.js} +0 -0
  182. /package/dist-node/src/{attachments.js → files/attachments.js} +0 -0
  183. /package/dist-node/src/{icons.js → files/icons.js} +0 -0
  184. /package/dist-node/src/{staged-attachments.js → files/staged-attachments.js} +0 -0
  185. /package/dist-node/src/{fork-workspace.js → git/fork-workspace.js} +0 -0
  186. /package/dist-node/src/{merge.js → git/merge.js} +0 -0
  187. /package/dist-node/src/{pr.js → git/pr.js} +0 -0
  188. /package/dist-node/src/{nosleep.js → host/nosleep.js} +0 -0
  189. /package/dist-node/src/{relay-processes.js → host/relay-processes.js} +0 -0
  190. /package/dist-node/src/{ui-lease-watchdog.js → host/ui-lease-watchdog.js} +0 -0
  191. /package/dist-node/src/{wifi.js → host/wifi.js} +0 -0
  192. /package/dist-node/src/{orchestration-schema.generated.js → orchestration/persistence/schema.generated.js} +0 -0
  193. /package/dist-node/src/{workflow-effect-runner.js → orchestration/workflow/effect-runner.js} +0 -0
  194. /package/dist-node/src/{workflow-http.js → orchestration/workflow/http.js} +0 -0
  195. /package/dist-node/src/{workflow-machine.js → orchestration/workflow/machine.js} +0 -0
  196. /package/dist-node/src/{session-poller.js → reads/session-poller.js} +0 -0
  197. /package/dist-node/src/{context-breakdown.js → transcript/context-breakdown.js} +0 -0
  198. /package/dist-node/src/{chat-cursor.js → transcript/cursor.js} +0 -0
  199. /package/dist-node/src/{plan-usage.js → usage/plan-usage.js} +0 -0
  200. /package/dist-node/src/{tool-usage-service.js → usage/tool-usage-service.js} +0 -0
@@ -0,0 +1,309 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { hashCapabilityToken, WorkflowTransitionError } from "../persistence/db.js";
3
+ import { clearMatchingQuarantine, reconcileBlockedEffect, requireSameAmbiguousEffect } from "./effect-recovery.js";
4
+ import { WorkflowCoordinatorError } from "./errors.js";
5
+ import { actionOrdinal, cleanUnknown, guardJobs, isDeliveryReceipt, privateCorrelationBlock, promptRole, workflowEffectCorrelationMarker } from "./helpers.js";
6
+ import { assertWorkflowDelegation, isTerminalWorkflowJobState, workflowDelegationTransition } from "./machine.js";
7
+ import { workflowChildPrompt } from "./prompts.js";
8
+ import { assertCompatible, heartbeat, projection, requireBlockedAction, requireBlockedRun, requireEffect, requireRun } from "./state.js";
9
+ export async function delegate(context, input) {
10
+ const tokenHash = hashCapabilityToken(input.phaseCapability);
11
+ const request = {
12
+ workflowId: input.workflowId,
13
+ sessionId: input.sessionId,
14
+ tokenHash,
15
+ role: input.role,
16
+ task: input.task,
17
+ planningInterpretation: input.planningInterpretation
18
+ };
19
+ const replay = context.db.getIdempotentMutation('workflow_delegate', input.clientId, request);
20
+ if (replay) {
21
+ const job = context.db.getWorkflowJob(replay.result.jobId);
22
+ if (!job)
23
+ throw new WorkflowTransitionError('accepted Workflow job disappeared');
24
+ return { replayed: true, job, workflow: projection(context, replay.result.runId) };
25
+ }
26
+ heartbeat(context);
27
+ if (!input.task.trim())
28
+ throw new WorkflowCoordinatorError('invalid_request', 'Delegation needs a focused task.', { status: 400 });
29
+ const initial = context.db.getWorkflowRun(input.workflowId);
30
+ if (!initial)
31
+ throw new WorkflowCoordinatorError('workflow_not_found', 'Workflow does not exist.', { status: 404 });
32
+ const jobId = randomUUID();
33
+ const transcriptCursor = await context.deps.captureTranscriptCursor?.(input.sessionId);
34
+ const deliveryCursor = await context.deps.captureDeliveryCursor(input.sessionId);
35
+ const role = initial.roles[input.role];
36
+ const prompt = [
37
+ workflowChildPrompt({
38
+ roleName: input.role,
39
+ objective: initial.objective,
40
+ role: promptRole(role),
41
+ task: input.task
42
+ }),
43
+ privateCorrelationBlock(initial.id, `job:${jobId}`)
44
+ ].join('\n\n');
45
+ const mutation = context.db.idempotentMutation('workflow_delegate', input.clientId, request, () => {
46
+ const run = requireRun(context, input.workflowId);
47
+ const jobs = context.db.listWorkflowJobs(run.id);
48
+ const capability = context.db.getWorkflowCapability(tokenHash);
49
+ const claims = capability &&
50
+ (capability.phase === 'exploring' || capability.phase === 'planning' || capability.phase === 'reviewing')
51
+ ? {
52
+ runId: capability.runId,
53
+ rootSessionId: capability.rootSessionId,
54
+ cycle: capability.cycle,
55
+ revision: capability.revision,
56
+ phase: capability.phase,
57
+ allowedRoles: capability.allowedRoles,
58
+ consumed: capability.consumedAt !== undefined,
59
+ revoked: capability.revokedAt !== undefined
60
+ }
61
+ : null;
62
+ const guardRun = {
63
+ id: run.id,
64
+ rootSessionId: run.rootSessionId ?? null,
65
+ phase: run.phase,
66
+ cycle: run.cycle,
67
+ revision: run.revision
68
+ };
69
+ assertWorkflowDelegation(guardRun, claims, { sessionId: input.sessionId, role: input.role }, guardJobs(jobs));
70
+ const transition = workflowDelegationTransition(guardRun, input.role);
71
+ const ordinal = actionOrdinal(jobs, input.role, transition.cycle);
72
+ const logicalKey = `${transition.logicalPrefix}:${transition.cycle}:${ordinal}`;
73
+ context.db.consumeWorkflowCapability({
74
+ tokenHash,
75
+ runId: run.id,
76
+ rootSessionId: input.sessionId,
77
+ role: input.role,
78
+ expectedPhase: run.phase,
79
+ expectedCycle: run.cycle,
80
+ expectedRevision: run.revision,
81
+ eventKey: `capability-consumed:${logicalKey}`
82
+ });
83
+ const created = context.db.createWorkflowJob({
84
+ id: jobId,
85
+ runId: run.id,
86
+ logicalKey,
87
+ role: input.role,
88
+ cycle: transition.cycle,
89
+ revision: transition.revision,
90
+ resolvedRole: role,
91
+ prompt,
92
+ state: 'queued',
93
+ transcriptCursor,
94
+ expectedCancellationGeneration: run.cancellationGeneration,
95
+ eventKey: `delegated:${logicalKey}`
96
+ });
97
+ const advanced = context.db.transitionWorkflowRun({
98
+ runId: run.id,
99
+ expectedPhase: run.phase,
100
+ expectedCancellationGeneration: run.cancellationGeneration,
101
+ phase: transition.phase,
102
+ cycle: transition.cycle,
103
+ revision: transition.revision,
104
+ ...(input.role === 'implementation'
105
+ ? { planningInterpretation: input.planningInterpretation ?? input.task }
106
+ : {}),
107
+ eventKey: `delegation-transition:${logicalKey}`,
108
+ eventType: 'workflow_delegation_accepted',
109
+ eventData: { jobId: created.job.id, logicalKey, role: input.role }
110
+ });
111
+ context.db.revokeWorkflowCapabilities(run.id, `revoke-after-delegation:${logicalKey}`, run.phase);
112
+ if (input.role === 'exploration') {
113
+ const actionId = `authorize:exploring:${advanced.cycle}:${advanced.revision}`;
114
+ context.db.prepareWorkflowEffect({
115
+ runId: run.id,
116
+ actionId,
117
+ kind: 'authorize_phase',
118
+ target: { sessionId: input.sessionId },
119
+ inputs: {
120
+ grant: {
121
+ phase: 'exploring',
122
+ cycle: advanced.cycle,
123
+ revision: advanced.revision,
124
+ allowedRoles: ['exploration']
125
+ },
126
+ correlationMarker: workflowEffectCorrelationMarker(run.id, actionId)
127
+ },
128
+ cursor: deliveryCursor,
129
+ expectedCancellationGeneration: run.cancellationGeneration,
130
+ eventKey: `prepare:${actionId}`
131
+ });
132
+ }
133
+ return { runId: run.id, jobId: created.job.id };
134
+ }, { runId: input.workflowId });
135
+ const job = context.db.getWorkflowJob(mutation.result.jobId);
136
+ if (!job)
137
+ throw new WorkflowTransitionError('accepted Workflow job disappeared');
138
+ return { replayed: mutation.replayed, job, workflow: projection(context, input.workflowId) };
139
+ }
140
+ export async function retry(context, input) {
141
+ const replay = context.db.getIdempotentMutation('workflow_retry', input.clientId, input);
142
+ if (replay)
143
+ return { replayed: true, workflow: projection(context, replay.result.runId) };
144
+ heartbeat(context);
145
+ await assertCompatible(context);
146
+ const mutation = context.db.idempotentMutation('workflow_retry', input.clientId, input, () => {
147
+ const run = requireBlockedRun(context, input.workflowId);
148
+ const actionId = run.blocked?.actionId;
149
+ if (run.blocked?.retryClass !== 'deterministic') {
150
+ throw new WorkflowCoordinatorError('workflow_recovery_invalid', 'This blocked action is not safely retryable.');
151
+ }
152
+ if (actionId.startsWith('job:')) {
153
+ const jobId = actionId.slice(4);
154
+ context.db.updateWorkflowJob({
155
+ jobId,
156
+ expectedStates: ['failed'],
157
+ expectedCancellationGeneration: run.cancellationGeneration,
158
+ state: 'queued',
159
+ childSessionId: null,
160
+ outcome: null,
161
+ taskReceipt: null,
162
+ batonReceipt: null,
163
+ clearOwner: true,
164
+ eventKey: `job-retry:${jobId}:${input.clientId}`,
165
+ eventType: 'workflow_job_retry_queued'
166
+ });
167
+ }
168
+ else if (actionId.startsWith('compatibility:') || actionId === 'bind-root') {
169
+ // No external intent exists for these read-only gates; unblocking is the retry.
170
+ }
171
+ else {
172
+ context.db.retryWorkflowEffect(run.id, actionId, `effect-retry:${actionId}:${input.clientId}`);
173
+ }
174
+ context.db.transitionWorkflowRun({
175
+ runId: run.id,
176
+ expectedPhase: 'blocked',
177
+ expectedCancellationGeneration: run.cancellationGeneration,
178
+ phase: run.blocked?.resumePhase,
179
+ blocked: null,
180
+ eventKey: `workflow-retry:${actionId}:${input.clientId}`,
181
+ eventType: 'workflow_retry_accepted'
182
+ });
183
+ return { runId: run.id };
184
+ }, { runId: input.workflowId });
185
+ return { replayed: mutation.replayed, workflow: projection(context, input.workflowId) };
186
+ }
187
+ export async function adopt(context, input) {
188
+ const replay = context.db.getIdempotentMutation('workflow_adopt', input.clientId, input);
189
+ if (replay)
190
+ return { replayed: true, workflow: projection(context, replay.result.runId) };
191
+ heartbeat(context);
192
+ const run = requireBlockedAction(context, input.workflowId, input.actionId);
193
+ const candidate = run.blocked?.candidates?.find(item => item.id === input.candidateId);
194
+ if (!candidate)
195
+ throw new WorkflowCoordinatorError('workflow_recovery_invalid', 'That saved adoption candidate is unavailable.');
196
+ const effect = requireEffect(context, run.id, input.actionId);
197
+ if (effect.state !== 'ambiguous' || !context.deps.validateAdoption) {
198
+ throw new WorkflowCoordinatorError('workflow_recovery_invalid', 'This action cannot be adopted.');
199
+ }
200
+ const receipt = await context.deps.validateAdoption({ run, effect, candidate });
201
+ if (receipt === null)
202
+ throw new WorkflowCoordinatorError('workflow_recovery_invalid', 'The candidate no longer validates.');
203
+ const mutation = context.db.idempotentMutation('workflow_adopt', input.clientId, input, () => {
204
+ const current = requireBlockedAction(context, input.workflowId, input.actionId);
205
+ const currentEffect = requireSameAmbiguousEffect(context, current.id, input.actionId, effect);
206
+ context.db.markWorkflowEffectCommitted({
207
+ runId: current.id,
208
+ actionId: input.actionId,
209
+ receipt: cleanUnknown(receipt),
210
+ eventKey: `effect-adopted:${input.actionId}:${input.clientId}`
211
+ });
212
+ context.db.transitionWorkflowRun({
213
+ runId: current.id,
214
+ expectedPhase: 'blocked',
215
+ expectedCancellationGeneration: current.cancellationGeneration,
216
+ phase: current.blocked?.resumePhase,
217
+ blocked: null,
218
+ eventKey: `workflow-adopted:${input.actionId}:${input.clientId}`,
219
+ eventType: 'workflow_effect_adopted',
220
+ eventData: { candidateId: candidate.id }
221
+ });
222
+ clearMatchingQuarantine(context, currentEffect, `adopt:${input.clientId}`);
223
+ return { runId: current.id };
224
+ }, { runId: input.workflowId, actionId: input.actionId });
225
+ return { replayed: mutation.replayed, workflow: projection(context, input.workflowId) };
226
+ }
227
+ export async function replay(context, input) {
228
+ const prior = context.db.getIdempotentMutation('workflow_replay_ambiguous', input.clientId, input);
229
+ if (prior)
230
+ return { replayed: true, workflow: projection(context, prior.result.runId) };
231
+ heartbeat(context);
232
+ if (input.confirmDuplicateRisk !== true) {
233
+ throw new WorkflowCoordinatorError('workflow_recovery_invalid', 'Risky replay requires explicit duplicate-risk confirmation.');
234
+ }
235
+ const blocked = requireBlockedAction(context, input.workflowId, input.actionId);
236
+ const effect = requireEffect(context, blocked.id, input.actionId);
237
+ await reconcileBlockedEffect(context, blocked);
238
+ if (requireRun(context, input.workflowId).phase !== 'blocked') {
239
+ const reconciled = context.db.idempotentMutation('workflow_replay_ambiguous', input.clientId, input, () => ({ runId: input.workflowId }), { runId: input.workflowId, actionId: input.actionId });
240
+ return { replayed: reconciled.replayed, workflow: projection(context, input.workflowId) };
241
+ }
242
+ const mutation = context.db.idempotentMutation('workflow_replay_ambiguous', input.clientId, input, () => {
243
+ const run = requireBlockedAction(context, input.workflowId, input.actionId);
244
+ if (run.blocked?.retryClass !== 'ambiguous') {
245
+ throw new WorkflowCoordinatorError('workflow_recovery_invalid', 'This action is not an ambiguous effect.');
246
+ }
247
+ if (run.blocked.candidates?.length) {
248
+ throw new WorkflowCoordinatorError('workflow_recovery_invalid', 'Validate and adopt a saved candidate instead of risking a duplicate replay.');
249
+ }
250
+ const currentEffect = requireSameAmbiguousEffect(context, run.id, input.actionId, effect);
251
+ context.db.replayAmbiguousWorkflowEffect(run.id, input.actionId, `effect-replay:${input.actionId}:${input.clientId}`);
252
+ context.db.transitionWorkflowRun({
253
+ runId: run.id,
254
+ expectedPhase: 'blocked',
255
+ expectedCancellationGeneration: run.cancellationGeneration,
256
+ phase: run.blocked?.resumePhase,
257
+ blocked: null,
258
+ eventKey: `workflow-replay:${input.actionId}:${input.clientId}`,
259
+ eventType: 'workflow_ambiguous_replay_confirmed'
260
+ });
261
+ clearMatchingQuarantine(context, currentEffect, `replay:${input.clientId}`);
262
+ return { runId: run.id };
263
+ }, { runId: input.workflowId, actionId: input.actionId });
264
+ return { replayed: mutation.replayed, workflow: projection(context, input.workflowId) };
265
+ }
266
+ export async function complete(context, input) {
267
+ const replay = context.db.getIdempotentMutation('workflow_complete', input.clientId, input);
268
+ if (replay)
269
+ return { replayed: true, workflow: projection(context, replay.result.runId) };
270
+ heartbeat(context);
271
+ const mutation = context.db.idempotentMutation('workflow_complete', input.clientId, input, () => {
272
+ const run = requireRun(context, input.workflowId);
273
+ const jobs = context.db.listWorkflowJobs(run.id);
274
+ const outstanding = jobs.some(job => !isTerminalWorkflowJobState(job.state));
275
+ const implementationDelivered = jobs.some(job => job.role === 'implementation' &&
276
+ job.state === 'returned' &&
277
+ isDeliveryReceipt(job.batonReceipt) &&
278
+ job.batonReceipt.kind === 'message');
279
+ if (run.phase !== 'reviewing' ||
280
+ outstanding ||
281
+ !implementationDelivered ||
282
+ run.implementationBatonsDelivered < 1) {
283
+ throw new WorkflowCoordinatorError('workflow_recovery_invalid', 'Workflow can complete only from reviewing after a delivered implementation Baton and no outstanding job.');
284
+ }
285
+ context.db.transitionWorkflowRun({
286
+ runId: run.id,
287
+ expectedPhase: 'reviewing',
288
+ expectedCancellationGeneration: run.cancellationGeneration,
289
+ phase: 'completed',
290
+ eventKey: `workflow-completed:${input.clientId}`,
291
+ eventType: 'workflow_completed'
292
+ });
293
+ context.db.revokeWorkflowCapabilities(run.id, `revoke-on-complete:${input.clientId}`);
294
+ return { runId: run.id };
295
+ }, { runId: input.workflowId });
296
+ return { replayed: mutation.replayed, workflow: projection(context, input.workflowId) };
297
+ }
298
+ export async function cancel(context, input) {
299
+ const replay = context.db.getIdempotentMutation('workflow_cancel', input.clientId, input);
300
+ if (replay)
301
+ return { replayed: true, workflow: projection(context, replay.result.runId) };
302
+ heartbeat(context);
303
+ const mutation = context.db.idempotentMutation('workflow_cancel', input.clientId, input, () => {
304
+ context.db.cancelWorkflowRun(input.workflowId, `workflow-cancelled:${input.clientId}`);
305
+ context.db.revokeWorkflowCapabilities(input.workflowId, `revoke-on-cancel:${input.clientId}`);
306
+ return { runId: input.workflowId };
307
+ }, { runId: input.workflowId });
308
+ return { replayed: mutation.replayed, workflow: projection(context, input.workflowId) };
309
+ }
@@ -0,0 +1,28 @@
1
+ import { adopt, cancel, complete, delegate, replay, retry } from "./commands.js";
2
+ import { start } from "./start.js";
3
+ import { ownsSession, projection, projections, runIdsNeedingWake, workflowForSession } from "./state.js";
4
+ import { wake } from "./wake.js";
5
+ export { WorkflowCoordinatorError } from "./errors.js";
6
+ export { workflowEffectCorrelationMarker } from "./helpers.js";
7
+ export * from "./types.js";
8
+ /** Owns one wake map and the shared dependencies for every run lifecycle. */
9
+ export class WorkflowCoordinator {
10
+ context;
11
+ constructor(db, relay, deps) {
12
+ this.context = { db, relay, deps, waking: new Map() };
13
+ db.registerRelayInstance({ ...relay, canDriveUi: true });
14
+ }
15
+ start = (...args) => start(this.context, ...args);
16
+ wake = (...args) => wake(this.context, ...args);
17
+ delegate = (...args) => delegate(this.context, ...args);
18
+ retry = (...args) => retry(this.context, ...args);
19
+ adopt = (...args) => adopt(this.context, ...args);
20
+ replay = (...args) => replay(this.context, ...args);
21
+ complete = (...args) => complete(this.context, ...args);
22
+ cancel = (...args) => cancel(this.context, ...args);
23
+ projection = (...args) => projection(this.context, ...args);
24
+ projections = (...args) => projections(this.context, ...args);
25
+ runIdsNeedingWake = (...args) => runIdsNeedingWake(this.context, ...args);
26
+ workflowForSession = (...args) => workflowForSession(this.context, ...args);
27
+ ownsSession = (...args) => ownsSession(this.context, ...args);
28
+ }
@@ -0,0 +1,125 @@
1
+ import { WorkflowCoordinatorError } from "./errors.js";
2
+ import { cleanUnknown, isDeliveryReceipt, receiptEventSuffix } from "./helpers.js";
3
+ import { blockRun, effectReadCall, requireBlockedAction, requireEffect, requireRun } from "./state.js";
4
+ /** A later positive receipt resolves ambiguity safely; a newly visible candidate only refreshes the phone choice. */
5
+ export async function reconcileBlockedEffect(context, run) {
6
+ if (run.phase !== 'blocked' || run.blocked?.retryClass !== 'ambiguous')
7
+ return false;
8
+ const effect = context.db.getWorkflowEffect(run.id, run.blocked.actionId);
9
+ if (effect?.state !== 'ambiguous')
10
+ return false;
11
+ const reconciliation = await context.deps.reconcileEffect?.(effectReadCall(run, effect));
12
+ if (!reconciliation || reconciliation.status === 'pending')
13
+ return false;
14
+ if (reconciliation.status === 'committed') {
15
+ const receipt = cleanUnknown(reconciliation.receipt);
16
+ context.db.idempotentMutation('reconcile_blocked_workflow_effect', `${run.id}:${effect.actionId}:${receiptEventSuffix(receipt, `attempt:${effect.attemptCount}`)}`, { actionId: effect.actionId, receipt }, () => {
17
+ const current = requireBlockedAction(context, run.id, effect.actionId);
18
+ const currentEffect = requireSameAmbiguousEffect(context, current.id, effect.actionId, effect);
19
+ context.db.markWorkflowEffectCommitted({
20
+ runId: current.id,
21
+ actionId: effect.actionId,
22
+ receipt,
23
+ eventKey: `effect-positive-while-blocked:${effect.actionId}:${effect.attemptCount}`
24
+ });
25
+ context.db.transitionWorkflowRun({
26
+ runId: current.id,
27
+ expectedPhase: 'blocked',
28
+ expectedCancellationGeneration: current.cancellationGeneration,
29
+ phase: current.blocked?.resumePhase,
30
+ blocked: null,
31
+ eventKey: `workflow-positive-while-blocked:${effect.actionId}:${effect.attemptCount}`,
32
+ eventType: 'workflow_ambiguous_effect_reconciled'
33
+ });
34
+ clearMatchingQuarantine(context, currentEffect, `positive-receipt:${effect.id}`);
35
+ return { runId: current.id };
36
+ }, { runId: run.id, actionId: effect.actionId });
37
+ return true;
38
+ }
39
+ const candidates = (reconciliation.candidates ?? [])
40
+ .slice(0, 20)
41
+ .map(candidate => cleanUnknown(candidate));
42
+ const prior = run.blocked.candidates ?? [];
43
+ if (JSON.stringify(prior) === JSON.stringify(candidates))
44
+ return false;
45
+ context.db.transitionWorkflowRun({
46
+ runId: run.id,
47
+ expectedPhase: 'blocked',
48
+ expectedCancellationGeneration: run.cancellationGeneration,
49
+ phase: 'blocked',
50
+ blocked: { ...run.blocked, candidates },
51
+ eventKey: `workflow-candidates-refreshed:${effect.actionId}:${effect.attemptCount}:${candidates.map(item => item.id).join(',') || 'none'}`,
52
+ eventType: 'workflow_adoption_candidates_refreshed',
53
+ eventData: { actionId: effect.actionId, candidateCount: candidates.length }
54
+ });
55
+ return true;
56
+ }
57
+ export function markReceiptLost(context, run, effect, evidence) {
58
+ if (!isDeliveryReceipt(effect.receipt) || effect.receipt.kind !== 'outbox') {
59
+ throw new WorkflowCoordinatorError('workflow_adapter_invalid', `Delivery resolver reported a non-outbox receipt lost for ${effect.actionId}.`);
60
+ }
61
+ const errorMessage = 'Conductor accepted the queued message, but it disappeared before a durable message row was found.';
62
+ const ambiguous = effect.state === 'dispatched'
63
+ ? context.db.markWorkflowEffectAmbiguous({
64
+ runId: run.id,
65
+ actionId: effect.actionId,
66
+ errorCode: 'outbox_receipt_lost',
67
+ errorMessage,
68
+ ...(evidence === undefined ? {} : { evidence: cleanUnknown(evidence) }),
69
+ eventKey: `effect-receipt-lost:${effect.actionId}:${effect.receipt.id}`
70
+ })
71
+ : context.db.markWorkflowEffectReceiptLost({
72
+ runId: run.id,
73
+ actionId: effect.actionId,
74
+ expectedReceipt: effect.receipt,
75
+ errorCode: 'outbox_receipt_lost',
76
+ errorMessage,
77
+ ...(evidence === undefined ? {} : { evidence: cleanUnknown(evidence) }),
78
+ eventKey: `effect-receipt-lost:${effect.actionId}:${effect.receipt.id}`
79
+ });
80
+ const current = requireRun(context, run.id);
81
+ if (current.phase === 'cancelled') {
82
+ context.db.activateUiQuarantine({
83
+ actionId: ambiguous.actionId,
84
+ effectId: ambiguous.id,
85
+ reason: ambiguous.errorMessage ?? 'A cancelled Workflow has a lost accepted outbox receipt.',
86
+ owner: ambiguous.owner,
87
+ externalProcess: ambiguous.externalProcess
88
+ });
89
+ return;
90
+ }
91
+ quarantineAndBlock(context, current, ambiguous);
92
+ }
93
+ export function quarantineAndBlock(context, run, effect, candidates) {
94
+ context.db.activateUiQuarantine({
95
+ actionId: effect.actionId,
96
+ effectId: effect.id,
97
+ reason: effect.errorMessage ?? 'Workflow UI effect has no positive receipt.',
98
+ owner: effect.owner,
99
+ externalProcess: effect.externalProcess
100
+ });
101
+ blockRun(context, requireRun(context, run.id), {
102
+ actionId: effect.actionId,
103
+ errorCode: effect.errorCode ?? 'workflow_effect_ambiguous',
104
+ message: effect.errorMessage ?? 'The UI action may have executed; automatic replay is disabled.',
105
+ retryClass: 'ambiguous',
106
+ candidates
107
+ });
108
+ }
109
+ /** Call only inside the surrounding durable mutation so a newer hold cannot be cleared between read and write. */
110
+ export function clearMatchingQuarantine(context, effect, clearedBy) {
111
+ const quarantine = context.db.getUiQuarantine();
112
+ const matches = quarantine.effectId
113
+ ? quarantine.effectId === effect.id
114
+ : quarantine.actionId === effect.id || quarantine.actionId === effect.actionId;
115
+ if (quarantine.active && matches) {
116
+ context.db.clearUiQuarantine(clearedBy);
117
+ }
118
+ }
119
+ export function requireSameAmbiguousEffect(context, runId, actionId, expected) {
120
+ const current = requireEffect(context, runId, actionId);
121
+ if (current.state !== 'ambiguous' || current.attemptCount !== expected.attemptCount) {
122
+ throw new WorkflowCoordinatorError('workflow_recovery_invalid', 'The ambiguous action changed while it was being validated; inspect its current state and try again.');
123
+ }
124
+ return current;
125
+ }