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,156 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { isTerminalWorkflowPhase } from "../workflow/machine.js";
3
+ import { insertEffect } from "./effects.js";
4
+ import { OrchestrationError, WorkflowTransitionError } from "./errors.js";
5
+ import { appendEvent } from "./events.js";
6
+ import { idempotentMutation } from "./idempotency.js";
7
+ import { insertJob } from "./jobs.js";
8
+ import { checkRunGuard, getWorkflowRun, requireRun } from "./records.js";
9
+ import { isConstraintError, json, nonEmpty, optionalJson } from "./values.js";
10
+ export function createWorkflowRun(context, input) {
11
+ const initialEffects = input.initialEffects ?? (input.initialEffect ? [input.initialEffect] : []);
12
+ if (new Set(initialEffects.map(effect => effect.actionId)).size !== initialEffects.length) {
13
+ throw new OrchestrationError('initial Workflow effects must have unique action IDs');
14
+ }
15
+ // Idempotency belongs to the normalized UI request, not to derived preflight
16
+ // evidence. Re-reading roles or the now-non-pristine root on a transport retry
17
+ // must not turn the same tap into a conflict.
18
+ const request = { objective: input.objective, target: input.target };
19
+ let createdRunId;
20
+ const answer = idempotentMutation(context, 'start_workflow', input.clientId, request, () => {
21
+ const runId = input.id ?? randomUUID();
22
+ createdRunId = runId;
23
+ const at = context.now();
24
+ const phase = input.target.kind === 'new_workspace' ? 'creating_workspace' : 'pending_root';
25
+ if (input.target.kind === 'existing_session') {
26
+ if (input.workspaceId && input.workspaceId !== input.target.workspaceId) {
27
+ throw new OrchestrationError('workspace binding conflicts with the existing-session target');
28
+ }
29
+ if (input.rootSessionId && input.rootSessionId !== input.target.sessionId) {
30
+ throw new OrchestrationError('root binding conflicts with the existing-session target');
31
+ }
32
+ }
33
+ try {
34
+ context.db
35
+ .prepare(`INSERT INTO workflow_runs (
36
+ id, objective, target_json, roles_json, phase, workspace_id, root_session_id,
37
+ pristine_evidence_json, delivery_baseline_json, created_at, updated_at
38
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
39
+ .run(runId, nonEmpty(input.objective, 'objective'), json(input.target), json(input.roles), phase, input.target.kind === 'existing_session' ? input.target.workspaceId : (input.workspaceId ?? null), input.target.kind === 'existing_session' ? input.target.sessionId : (input.rootSessionId ?? null), optionalJson(input.pristineEvidence), optionalJson(input.deliveryBaseline), at, at);
40
+ }
41
+ catch (error) {
42
+ if (isConstraintError(error)) {
43
+ throw new WorkflowTransitionError('the root already belongs to an active Workflow');
44
+ }
45
+ throw error;
46
+ }
47
+ insertJob(context, {
48
+ id: `${runId}:explore:0`,
49
+ runId,
50
+ logicalKey: 'explore:0',
51
+ role: 'exploration',
52
+ cycle: 0,
53
+ revision: 0,
54
+ resolvedRole: input.roles.exploration,
55
+ prompt: input.bootstrapPrompt ?? input.objective,
56
+ state: 'dormant',
57
+ cancellationGeneration: 0,
58
+ at
59
+ });
60
+ for (const effect of initialEffects) {
61
+ insertEffect(context, {
62
+ ...effect,
63
+ id: effect.id ?? `${runId}:${effect.actionId}`,
64
+ runId,
65
+ at
66
+ });
67
+ }
68
+ appendEvent(context, runId, 'workflow_started', 'workflow_started', {
69
+ target: input.target,
70
+ bootstrapJob: 'explore:0',
71
+ initialActionIds: initialEffects.map(effect => effect.actionId)
72
+ });
73
+ return { runId };
74
+ }, { runId: input.id, actionId: initialEffects[0]?.actionId });
75
+ const runId = answer.result.runId ?? createdRunId;
76
+ if (!runId)
77
+ throw new OrchestrationError('idempotent Workflow result did not contain a run ID');
78
+ const run = getWorkflowRun(context, runId);
79
+ if (!run)
80
+ throw new OrchestrationError(`Workflow ${runId} disappeared after creation`);
81
+ return { replayed: answer.replayed, run };
82
+ }
83
+ export function transitionWorkflowRun(context, input) {
84
+ return context.immediate(() => {
85
+ const current = requireRun(context, input.runId);
86
+ if (isTerminalWorkflowPhase(current.phase)) {
87
+ throw new WorkflowTransitionError(`terminal Workflow ${current.id} cannot transition from ${current.phase}`);
88
+ }
89
+ checkRunGuard(current, input.expectedPhase, input.expectedCancellationGeneration);
90
+ const phase = input.phase ?? current.phase;
91
+ const terminalAt = isTerminalWorkflowPhase(phase) ? (current.terminalAt ?? context.now()) : undefined;
92
+ const blocked = input.blocked === undefined ? current.blocked : (input.blocked ?? undefined);
93
+ const at = context.now();
94
+ try {
95
+ context.db
96
+ .prepare(`UPDATE workflow_runs SET
97
+ phase = ?, cycle = ?, revision = ?, workspace_id = ?, root_session_id = ?,
98
+ pristine_evidence_json = ?, delivery_baseline_json = ?, planning_interpretation = ?,
99
+ blocked_action_id = ?, blocked_error_code = ?, blocked_message = ?, resume_phase = ?,
100
+ retry_class = ?, blocked_candidates_json = ?, blocked_at = ?,
101
+ implementation_batons_delivered = ?, updated_at = ?, terminal_at = ?
102
+ WHERE id = ? AND cancellation_generation = ?`)
103
+ .run(phase, input.cycle ?? current.cycle, input.revision ?? current.revision, input.workspaceId === undefined ? (current.workspaceId ?? null) : input.workspaceId, input.rootSessionId === undefined ? (current.rootSessionId ?? null) : input.rootSessionId, input.pristineEvidence === undefined
104
+ ? optionalJson(current.pristineEvidence)
105
+ : optionalJson(input.pristineEvidence), input.deliveryBaseline === undefined
106
+ ? optionalJson(current.deliveryBaseline)
107
+ : optionalJson(input.deliveryBaseline), input.planningInterpretation === undefined
108
+ ? (current.planningInterpretation ?? null)
109
+ : input.planningInterpretation, blocked?.actionId ?? null, blocked?.errorCode ?? null, blocked?.message ?? null, blocked?.resumePhase ?? null, blocked?.retryClass ?? null, optionalJson(blocked?.candidates), blocked ? (blocked.blockedAt ?? at) : null, input.implementationBatonsDelivered ?? current.implementationBatonsDelivered, at, terminalAt ?? null, input.runId, input.expectedCancellationGeneration);
110
+ }
111
+ catch (error) {
112
+ if (isConstraintError(error)) {
113
+ throw new WorkflowTransitionError('the requested root is already bound to another active Workflow');
114
+ }
115
+ throw error;
116
+ }
117
+ appendEvent(context, input.runId, input.eventKey, input.eventType, input.eventData);
118
+ return requireRun(context, input.runId);
119
+ });
120
+ }
121
+ export function cancelWorkflowRun(context, runId, eventKey, eventData) {
122
+ return context.immediate(() => {
123
+ const run = requireRun(context, runId);
124
+ if (run.phase === 'cancelled')
125
+ return run;
126
+ if (run.phase === 'completed')
127
+ throw new WorkflowTransitionError('a completed Workflow cannot be cancelled');
128
+ const at = context.now();
129
+ const generation = run.cancellationGeneration + 1;
130
+ context.db
131
+ .prepare(`UPDATE workflow_runs SET phase = 'cancelled', cancellation_generation = ?, updated_at = ?, terminal_at = ?,
132
+ blocked_action_id = NULL, blocked_error_code = NULL, blocked_message = NULL, resume_phase = NULL,
133
+ retry_class = NULL, blocked_candidates_json = NULL, blocked_at = NULL
134
+ WHERE id = ? AND cancellation_generation = ?`)
135
+ .run(generation, at, at, runId, run.cancellationGeneration);
136
+ context.db
137
+ .prepare(`UPDATE workflow_jobs SET state = 'cancelled', cancellation_generation = ?, updated_at = ?, terminal_at = ?
138
+ WHERE run_id = ? AND state NOT IN ('returned', 'failed', 'cancelled')`)
139
+ .run(generation, at, at, runId);
140
+ context.db
141
+ .prepare(`UPDATE workflow_job_attempts SET state = 'cancelled', updated_at = ?, terminal_at = ?
142
+ WHERE job_id IN (SELECT id FROM workflow_jobs WHERE run_id = ?)
143
+ AND state NOT IN ('returned', 'failed', 'cancelled')`)
144
+ .run(at, at, runId);
145
+ context.db
146
+ .prepare(`UPDATE workflow_effects SET state = 'cancelled', updated_at = ?, terminal_at = ?
147
+ WHERE run_id = ? AND state IN ('prepared', 'failed')`)
148
+ .run(at, at, runId);
149
+ context.db
150
+ .prepare(`UPDATE workflow_effect_attempts SET state = 'cancelled', updated_at = ?, terminal_at = ?
151
+ WHERE effect_id IN (SELECT id FROM workflow_effects WHERE run_id = ?) AND state = 'prepared'`)
152
+ .run(at, at, runId);
153
+ appendEvent(context, runId, eventKey, 'workflow_cancelled', eventData);
154
+ return requireRun(context, runId);
155
+ });
156
+ }
@@ -2,7 +2,7 @@ import { getTableName, sql } from 'drizzle-orm';
2
2
  import { check, customType, index, integer, primaryKey, sqliteTable, text, unique, uniqueIndex } from 'drizzle-orm/sqlite-core';
3
3
  import { createSelectSchema, jsonSchema } from 'drizzle-orm/zod';
4
4
  import { z } from 'zod';
5
- export { ORCHESTRATION_BOOTSTRAP_SQL } from "./orchestration-schema.generated.js";
5
+ export { ORCHESTRATION_BOOTSTRAP_SQL } from "./schema.generated.js";
6
6
  export const ORCHESTRATION_SCHEMA_VERSION = 1;
7
7
  export const WORKFLOW_PHASES = [
8
8
  'creating_workspace',
@@ -0,0 +1 @@
1
+ export const ORCHESTRATION_PROTOCOL_VERSION = 1;
@@ -0,0 +1,197 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { eq } from 'drizzle-orm';
3
+ import { OrchestrationError, UiLeaseUnavailableError } from "./errors.js";
4
+ import { activateUiQuarantine, getUiQuarantine } from "./quarantine.js";
5
+ import { assertRelayRegistered } from "./relays.js";
6
+ import { uiMutex, uiMutexSelectSchema } from "./schema.js";
7
+ import { externalFromColumns, nonEmpty, ownerFromColumns, probeAlive, sameLeaseSnapshot, validateProcessIdentity, validateRelayIdentity } from "./values.js";
8
+ export function acquireUiLease(context, input) {
9
+ validateRelayIdentity(input.owner);
10
+ const probe = input.processProbe ?? context.processProbe;
11
+ const observed = readUiLeaseOwner(context);
12
+ const ownerAlive = observed ? probeAlive(observed, probe) : false;
13
+ const externalAlive = observed?.externalProcess ? probeAlive(observed.externalProcess, probe) : false;
14
+ return context.immediate(() => {
15
+ assertRelayRegistered(context, input.owner);
16
+ const quarantine = getUiQuarantine(context);
17
+ if (quarantine.active && input.priority === 'background')
18
+ return { status: 'quarantined', quarantine };
19
+ const current = readUiLeaseOwner(context);
20
+ if (observed && (!current || !sameLeaseSnapshot(observed, current))) {
21
+ return current ? { status: 'busy', owner: current, reason: 'changed' } : assignUiLease(context, input);
22
+ }
23
+ if (!observed && current)
24
+ return { status: 'busy', owner: current, reason: 'changed' };
25
+ if (current) {
26
+ if (ownerAlive)
27
+ return { status: 'busy', owner: current, reason: 'owner_alive' };
28
+ if (current.externalProcess && externalAlive) {
29
+ return { status: 'busy', owner: current, reason: 'external_process_alive' };
30
+ }
31
+ if (current.mayExecute) {
32
+ clearUiMutex(context, current);
33
+ const hold = activateUiQuarantine(context, {
34
+ actionId: current.actionId,
35
+ effectId: current.effectId,
36
+ reason: 'a dead UI lease owner may have emitted an external action',
37
+ owner: current,
38
+ externalProcess: current.externalProcess
39
+ });
40
+ if (input.priority === 'background')
41
+ return { status: 'quarantined', quarantine: hold };
42
+ return assignUiLease(context, input, current);
43
+ }
44
+ clearUiMutex(context, current);
45
+ return assignUiLease(context, input, current);
46
+ }
47
+ return assignUiLease(context, input);
48
+ });
49
+ }
50
+ export function assignUiLease(context, input, reclaimed) {
51
+ const nonce = input.nonce ?? randomUUID();
52
+ const at = context.now();
53
+ const result = context.db
54
+ .prepare(`UPDATE ui_mutex SET owner_instance_id = ?, owner_pid = ?, owner_process_started_at = ?,
55
+ owner_protocol_version = ?, nonce = ?, action_id = ?, effect_id = ?, external_pid = NULL,
56
+ external_process_started_at = NULL, external_process_group = NULL, may_execute = 0,
57
+ deadline_at = ?, acquired_at = ?, updated_at = ?
58
+ WHERE id = 1 AND owner_instance_id IS NULL`)
59
+ .run(input.owner.instanceId, input.owner.pid, input.owner.processStartedAt, input.owner.protocolVersion, nonce, nonEmpty(input.actionId, 'UI action ID'), input.effectId ?? null, input.deadlineAt, at, at);
60
+ if (Number(result.changes) !== 1) {
61
+ const owner = readUiLeaseOwner(context);
62
+ if (!owner)
63
+ throw new OrchestrationError('UI mutex changed while being acquired');
64
+ return { status: 'busy', owner, reason: 'changed' };
65
+ }
66
+ return {
67
+ status: 'acquired',
68
+ lease: {
69
+ instanceId: input.owner.instanceId,
70
+ pid: input.owner.pid,
71
+ processStartedAt: input.owner.processStartedAt,
72
+ nonce,
73
+ actionId: input.actionId,
74
+ ...(input.effectId ? { effectId: input.effectId } : {})
75
+ },
76
+ ...(reclaimed ? { reclaimed } : {})
77
+ };
78
+ }
79
+ export function markUiLeaseMayExecute(context, lease, externalProcess, deadlineAt) {
80
+ if (externalProcess)
81
+ validateProcessIdentity(externalProcess, 'external UI process');
82
+ return context.immediate(() => {
83
+ const result = context.db
84
+ .prepare(`UPDATE ui_mutex SET may_execute = 1, external_pid = COALESCE(?, external_pid),
85
+ external_process_started_at = COALESCE(?, external_process_started_at),
86
+ external_process_group = CASE WHEN ? IS NULL THEN external_process_group ELSE ? END,
87
+ deadline_at = COALESCE(?, deadline_at), updated_at = ?
88
+ WHERE id = 1 AND owner_instance_id = ? AND owner_pid = ? AND owner_process_started_at = ?
89
+ AND nonce = ? AND action_id = ?`)
90
+ .run(externalProcess?.pid ?? null, externalProcess?.processStartedAt ?? null, externalProcess?.pid ?? null, externalProcess?.processGroup ?? null, deadlineAt ?? null, context.now(), lease.instanceId, lease.pid, lease.processStartedAt, lease.nonce, lease.actionId);
91
+ return Number(result.changes) === 1;
92
+ });
93
+ }
94
+ export function renewUiLease(context, lease, deadlineAt) {
95
+ return context.immediate(() => {
96
+ const result = context.db
97
+ .prepare(`UPDATE ui_mutex SET deadline_at = ?, updated_at = ?
98
+ WHERE id = 1 AND owner_instance_id = ? AND owner_pid = ? AND owner_process_started_at = ?
99
+ AND nonce = ? AND action_id = ?`)
100
+ .run(deadlineAt, context.now(), lease.instanceId, lease.pid, lease.processStartedAt, lease.nonce, lease.actionId);
101
+ return Number(result.changes) === 1;
102
+ });
103
+ }
104
+ export function releaseUiLease(context, lease) {
105
+ const observed = readUiLeaseOwner(context);
106
+ if (observed &&
107
+ observed.instanceId === lease.instanceId &&
108
+ observed.pid === lease.pid &&
109
+ observed.processStartedAt === lease.processStartedAt &&
110
+ observed.nonce === lease.nonce &&
111
+ observed.actionId === lease.actionId &&
112
+ observed.externalProcess &&
113
+ probeAlive(observed.externalProcess, context.processProbe)) {
114
+ // A returned/failed caller is not proof that a detached helper (or one of
115
+ // its process-group children) is gone. Retain the durable owner so the
116
+ // watchdog can terminate and prove the exact group dead before reclaim.
117
+ return false;
118
+ }
119
+ return context.immediate(() => {
120
+ const result = context.db
121
+ .prepare(`UPDATE ui_mutex SET owner_instance_id = NULL, owner_pid = NULL, owner_process_started_at = NULL,
122
+ owner_protocol_version = NULL, nonce = NULL, action_id = NULL, effect_id = NULL,
123
+ external_pid = NULL, external_process_started_at = NULL, external_process_group = NULL,
124
+ may_execute = 0, deadline_at = NULL, acquired_at = NULL, updated_at = ?
125
+ WHERE id = 1 AND owner_instance_id = ? AND owner_pid = ? AND owner_process_started_at = ?
126
+ AND nonce = ? AND action_id = ?`)
127
+ .run(context.now(), lease.instanceId, lease.pid, lease.processStartedAt, lease.nonce, lease.actionId);
128
+ return Number(result.changes) === 1;
129
+ });
130
+ }
131
+ export function getUiLeaseOwner(context) {
132
+ return readUiLeaseOwner(context);
133
+ }
134
+ /** Adapter installed with `configureSharedUiLeaseProvider` in `src/writes/ui-lock.ts`. */
135
+ export function createSharedUiLeaseProvider(context, owner, options = {}) {
136
+ const leaseMs = Math.max(1_000, options.leaseMs ?? 60_000);
137
+ return {
138
+ acquire: async (request) => {
139
+ const result = acquireUiLease(context, {
140
+ owner,
141
+ actionId: request.actionId ?? `ui:${owner.instanceId}:${randomUUID()}`,
142
+ deadlineAt: context.now() + leaseMs,
143
+ priority: request.priority
144
+ });
145
+ if (result.status !== 'acquired')
146
+ throw new UiLeaseUnavailableError(result);
147
+ return {
148
+ markMayExecute: externalProcess => {
149
+ if (!markUiLeaseMayExecute(context, result.lease, externalProcess)) {
150
+ throw new OrchestrationError('lost the shared UI lease before dispatch');
151
+ }
152
+ },
153
+ release: () => {
154
+ if (!releaseUiLease(context, result.lease)) {
155
+ throw new OrchestrationError('shared UI lease release did not match its durable owner or its external process is still live');
156
+ }
157
+ }
158
+ };
159
+ }
160
+ };
161
+ }
162
+ export function readUiLeaseOwner(context) {
163
+ const candidate = context.orm.select().from(uiMutex).where(eq(uiMutex.id, 1)).get();
164
+ if (!candidate)
165
+ throw new OrchestrationError('orchestration UI mutex singleton is missing');
166
+ const row = uiMutexSelectSchema.parse(candidate);
167
+ const owner = ownerFromColumns(row);
168
+ if (!owner ||
169
+ row.nonce === null ||
170
+ row.actionId === null ||
171
+ row.deadlineAt === null ||
172
+ row.acquiredAt === null ||
173
+ row.updatedAt === null) {
174
+ return undefined;
175
+ }
176
+ const externalProcess = externalFromColumns(row);
177
+ return {
178
+ ...owner,
179
+ nonce: row.nonce,
180
+ actionId: row.actionId,
181
+ ...(row.effectId === null ? {} : { effectId: row.effectId }),
182
+ ...(externalProcess ? { externalProcess } : {}),
183
+ mayExecute: row.mayExecute,
184
+ deadlineAt: row.deadlineAt,
185
+ acquiredAt: row.acquiredAt,
186
+ updatedAt: row.updatedAt
187
+ };
188
+ }
189
+ export function clearUiMutex(context, expected) {
190
+ context.db
191
+ .prepare(`UPDATE ui_mutex SET owner_instance_id = NULL, owner_pid = NULL, owner_process_started_at = NULL,
192
+ owner_protocol_version = NULL, nonce = NULL, action_id = NULL, effect_id = NULL,
193
+ external_pid = NULL, external_process_started_at = NULL, external_process_group = NULL,
194
+ may_execute = 0, deadline_at = NULL, acquired_at = NULL, updated_at = ?
195
+ WHERE id = 1 AND owner_instance_id = ? AND owner_pid = ? AND owner_process_started_at = ? AND nonce = ?`)
196
+ .run(context.now(), expected.instanceId, expected.pid, expected.processStartedAt, expected.nonce);
197
+ }
@@ -0,0 +1,114 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { scrubWorkflowSecrets, withoutWindowEvidence } from "../../shared.js";
3
+ import { OrchestrationError } from "./errors.js";
4
+ export const ACTIVE_PHASES = [
5
+ 'creating_workspace',
6
+ 'binding_root',
7
+ 'pending_root',
8
+ 'exploring',
9
+ 'planning',
10
+ 'implementing',
11
+ 'reviewing',
12
+ 'blocked'
13
+ ];
14
+ export const RUNNABLE_PHASES = ACTIVE_PHASES.filter(phase => phase !== 'blocked');
15
+ export const ALL_JOB_STATES = [
16
+ 'dormant',
17
+ 'queued',
18
+ 'owned',
19
+ 'opening',
20
+ 'configuring',
21
+ 'sending',
22
+ 'running',
23
+ 'returning',
24
+ 'returned',
25
+ 'failed',
26
+ 'cancelled'
27
+ ];
28
+ export const TERMINAL_EFFECT_STATES = ['committed', 'failed', 'ambiguous', 'cancelled'];
29
+ export const nonEmpty = (value, name) => {
30
+ if (!value.trim())
31
+ throw new OrchestrationError(`${name} must not be empty`);
32
+ return value;
33
+ };
34
+ export const validateProcessIdentity = (identity, name) => {
35
+ if (!Number.isSafeInteger(identity.pid) || identity.pid <= 0)
36
+ throw new OrchestrationError(`${name} PID is invalid`);
37
+ nonEmpty(identity.processStartedAt, `${name} process-start identity`);
38
+ };
39
+ export const validateRelayIdentity = (identity) => {
40
+ nonEmpty(identity.instanceId, 'relay instance ID');
41
+ validateProcessIdentity(identity, 'relay');
42
+ if (!Number.isSafeInteger(identity.protocolVersion) || identity.protocolVersion < 0) {
43
+ throw new OrchestrationError('relay orchestration protocol is invalid');
44
+ }
45
+ };
46
+ export const json = (value) => {
47
+ const encoded = JSON.stringify(value);
48
+ if (encoded === undefined)
49
+ throw new OrchestrationError('value is not JSON serializable');
50
+ return encoded;
51
+ };
52
+ export const optionalJson = (value) => (value === undefined ? null : json(value));
53
+ export const jsonProperty = (key, stored) => stored === null ? {} : { [key]: stored.value };
54
+ export const isPromiseLike = (value) => value !== null &&
55
+ (typeof value === 'object' || typeof value === 'function') &&
56
+ typeof value.then === 'function';
57
+ export const isConstraintError = (error) => error instanceof Error && /constraint failed|unique constraint/i.test(error.message);
58
+ export const ownerFromColumns = (row) => {
59
+ if (row.ownerInstanceId === null ||
60
+ row.ownerPid === null ||
61
+ row.ownerProcessStartedAt === null ||
62
+ row.ownerProtocolVersion === null) {
63
+ return undefined;
64
+ }
65
+ return {
66
+ instanceId: row.ownerInstanceId,
67
+ pid: row.ownerPid,
68
+ processStartedAt: row.ownerProcessStartedAt,
69
+ protocolVersion: row.ownerProtocolVersion
70
+ };
71
+ };
72
+ export const externalFromColumns = (row) => {
73
+ if (row.externalPid === null || row.externalProcessStartedAt === null)
74
+ return undefined;
75
+ return {
76
+ pid: row.externalPid,
77
+ processStartedAt: row.externalProcessStartedAt,
78
+ ...(row.externalProcessGroup === null ? {} : { processGroup: row.externalProcessGroup })
79
+ };
80
+ };
81
+ const sameProcess = (left, right) => left.pid === right.pid && left.processStartedAt === right.processStartedAt;
82
+ export const sameOwner = (left, right) => left.instanceId === right.instanceId && left.protocolVersion === right.protocolVersion && sameProcess(left, right);
83
+ export const processAuditKey = (identity) => `${identity.pid}:${createHash('sha256')
84
+ .update(`${identity.processStartedAt}\0${identity.processGroup ?? ''}`)
85
+ .digest('hex')
86
+ .slice(0, 16)}`;
87
+ export const ownerAuditKey = (owner) => createHash('sha256')
88
+ .update(`${owner.instanceId}\0${owner.pid}\0${owner.processStartedAt}\0${owner.protocolVersion}`)
89
+ .digest('hex')
90
+ .slice(0, 16);
91
+ export const defaultScrub = (text) => withoutWindowEvidence(scrubWorkflowSecrets(text))
92
+ .replace(/[\r\n\t]+/g, ' ')
93
+ .replace(/\s{2,}/g, ' ')
94
+ .trim();
95
+ export function probeAlive(identity, probe) {
96
+ try {
97
+ return probe(identity);
98
+ }
99
+ catch {
100
+ // Failure to prove death must never authorize overlap.
101
+ return true;
102
+ }
103
+ }
104
+ export const asObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
105
+ export const sameOptionalProcess = (left, right) => left === undefined
106
+ ? right === undefined
107
+ : right !== undefined && sameProcess(left, right) && left.processGroup === right.processGroup;
108
+ export const sameLeaseSnapshot = (left, right) => sameOwner(left, right) &&
109
+ left.nonce === right.nonce &&
110
+ left.actionId === right.actionId &&
111
+ left.effectId === right.effectId &&
112
+ left.mayExecute === right.mayExecute &&
113
+ left.deadlineAt === right.deadlineAt &&
114
+ sameOptionalProcess(left.externalProcess, right.externalProcess);