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,159 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { and, asc, eq, notInArray } from 'drizzle-orm';
3
+ import { isTerminalWorkflowJobState, isTerminalWorkflowPhase } from "../workflow/machine.js";
4
+ import { decodeJob } from "./codecs.js";
5
+ import { WorkflowTransitionError } from "./errors.js";
6
+ import { appendEvent, touchRun } from "./events.js";
7
+ import { canonicalRequestJson } from "./idempotency.js";
8
+ import { checkRunGuard, requireJob, requireRun } from "./records.js";
9
+ import { workflowJobs, workflowRuns } from "./schema.js";
10
+ import { asObject, json, optionalJson, ownerAuditKey, probeAlive, RUNNABLE_PHASES, sameOwner, validateRelayIdentity } from "./values.js";
11
+ export function insertJob(context, input) {
12
+ context.db
13
+ .prepare(`INSERT INTO workflow_jobs (
14
+ id, run_id, logical_key, role, cycle, revision, resolved_role_json, prompt, state, cancellation_generation,
15
+ transcript_cursor_json, created_at, updated_at
16
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
17
+ .run(input.id, input.runId, input.logicalKey, input.role, input.cycle, input.revision, json(input.resolvedRole), input.prompt, input.state, input.cancellationGeneration, optionalJson(input.transcriptCursor), input.at, input.at);
18
+ }
19
+ export function createWorkflowJob(context, input) {
20
+ return context.immediate(() => {
21
+ const run = requireRun(context, input.runId);
22
+ checkRunGuard(run, RUNNABLE_PHASES, input.expectedCancellationGeneration);
23
+ const existing = context.orm
24
+ .select()
25
+ .from(workflowJobs)
26
+ .where(and(eq(workflowJobs.runId, input.runId), eq(workflowJobs.logicalKey, input.logicalKey)))
27
+ .get();
28
+ if (existing) {
29
+ const decoded = decodeJob(existing);
30
+ if (decoded.role !== input.role ||
31
+ decoded.cycle !== (input.cycle ?? run.cycle) ||
32
+ decoded.revision !== (input.revision ?? run.revision) ||
33
+ canonicalRequestJson(decoded.resolvedRole) !== canonicalRequestJson(input.resolvedRole) ||
34
+ decoded.prompt !== input.prompt) {
35
+ throw new WorkflowTransitionError(`logical job ${input.logicalKey} already has different frozen inputs`);
36
+ }
37
+ return { created: false, job: decoded };
38
+ }
39
+ const id = input.id ?? randomUUID();
40
+ const at = context.now();
41
+ insertJob(context, {
42
+ ...input,
43
+ id,
44
+ cycle: input.cycle ?? run.cycle,
45
+ revision: input.revision ?? run.revision,
46
+ state: input.state ?? 'queued',
47
+ cancellationGeneration: run.cancellationGeneration,
48
+ at
49
+ });
50
+ touchRun(context, input.runId, at);
51
+ appendEvent(context, input.runId, input.eventKey, 'workflow_job_created', {
52
+ jobId: id,
53
+ logicalKey: input.logicalKey,
54
+ role: input.role,
55
+ cycle: input.cycle ?? run.cycle,
56
+ revision: input.revision ?? run.revision,
57
+ state: input.state ?? 'queued'
58
+ });
59
+ return { created: true, job: requireJob(context, id) };
60
+ });
61
+ }
62
+ export function activateWorkflowJob(context, jobId, expectedCancellationGeneration, eventKey, transcriptCursor) {
63
+ return updateWorkflowJob(context, {
64
+ jobId,
65
+ expectedStates: ['dormant'],
66
+ expectedCancellationGeneration,
67
+ state: 'queued',
68
+ ...(transcriptCursor === undefined ? {} : { transcriptCursor }),
69
+ eventKey,
70
+ eventType: 'workflow_job_activated'
71
+ });
72
+ }
73
+ export function claimNextWorkflowJob(context, owner, runId) {
74
+ validateRelayIdentity(owner);
75
+ return context.immediate(() => {
76
+ const row = context.orm
77
+ .select({ job: workflowJobs })
78
+ .from(workflowJobs)
79
+ .innerJoin(workflowRuns, eq(workflowRuns.id, workflowJobs.runId))
80
+ .where(and(eq(workflowJobs.state, 'queued'), eq(workflowJobs.cancellationGeneration, workflowRuns.cancellationGeneration), notInArray(workflowRuns.phase, ['blocked', 'completed', 'cancelled']), runId === undefined ? undefined : eq(workflowJobs.runId, runId)))
81
+ .orderBy(asc(workflowJobs.createdAt), asc(workflowJobs.id))
82
+ .limit(1)
83
+ .get()?.job;
84
+ if (!row)
85
+ return undefined;
86
+ const at = context.now();
87
+ const result = context.db
88
+ .prepare(`UPDATE workflow_jobs SET state = 'owned', owner_instance_id = ?, owner_pid = ?,
89
+ owner_process_started_at = ?, owner_protocol_version = ?, updated_at = ?
90
+ WHERE id = ? AND state = 'queued'`)
91
+ .run(owner.instanceId, owner.pid, owner.processStartedAt, owner.protocolVersion, at, row.id);
92
+ if (Number(result.changes) !== 1)
93
+ return undefined;
94
+ touchRun(context, row.runId, at);
95
+ appendEvent(context, row.runId, `job_claimed:${row.id}:${row.attemptCount + 1}:${ownerAuditKey(owner)}`, 'workflow_job_claimed', {
96
+ jobId: row.id,
97
+ ownerInstanceId: owner.instanceId
98
+ });
99
+ return requireJob(context, row.id);
100
+ });
101
+ }
102
+ /** Requeue only the pre-effect `owned` state after exact process death is proven. */
103
+ export function reconcileAbandonedWorkflowJobClaim(context, input) {
104
+ const observed = requireJob(context, input.jobId);
105
+ if (!observed.owner || observed.state !== 'owned')
106
+ return { status: 'unsafe', job: observed };
107
+ const observedOwner = observed.owner;
108
+ const probe = input.processProbe ?? context.processProbe;
109
+ if (probeAlive(observedOwner, probe))
110
+ return { status: 'owner_alive', job: observed };
111
+ return context.immediate(() => {
112
+ const current = requireJob(context, input.jobId);
113
+ if (current.state !== 'owned' ||
114
+ !current.owner ||
115
+ !sameOwner(current.owner, observedOwner) ||
116
+ current.attemptCount !== observed.attemptCount) {
117
+ return { status: 'changed', job: current };
118
+ }
119
+ const at = context.now();
120
+ context.db
121
+ .prepare(`UPDATE workflow_jobs SET state = 'queued', owner_instance_id = NULL, owner_pid = NULL,
122
+ owner_process_started_at = NULL, owner_protocol_version = NULL, updated_at = ? WHERE id = ?`)
123
+ .run(at, current.id);
124
+ touchRun(context, current.runId, at);
125
+ appendEvent(context, current.runId, `${input.eventKey}:${ownerAuditKey(observedOwner)}`, 'workflow_job_claim_recovered', {
126
+ jobId: current.id,
127
+ abandonedInstanceId: observedOwner.instanceId
128
+ });
129
+ return { status: 'requeued', job: requireJob(context, current.id) };
130
+ });
131
+ }
132
+ export function updateWorkflowJob(context, input) {
133
+ return context.immediate(() => {
134
+ const job = requireJob(context, input.jobId);
135
+ const run = requireRun(context, job.runId);
136
+ if (isTerminalWorkflowPhase(run.phase) || job.state === 'cancelled') {
137
+ throw new WorkflowTransitionError(`cannot advance terminal job ${job.id}`);
138
+ }
139
+ checkRunGuard(run, undefined, input.expectedCancellationGeneration);
140
+ if (!input.expectedStates.includes(job.state)) {
141
+ throw new WorkflowTransitionError(`job ${job.id} is ${job.state}, expected ${input.expectedStates.join(' or ')}`);
142
+ }
143
+ const at = context.now();
144
+ const terminalAt = isTerminalWorkflowJobState(input.state) ? (job.terminalAt ?? at) : null;
145
+ const result = context.db
146
+ .prepare(`UPDATE workflow_jobs SET state = ?, transcript_cursor_json = ?, child_session_id = ?, outcome_json = ?, task_receipt_json = ?,
147
+ baton_receipt_json = ?, owner_instance_id = ?, owner_pid = ?, owner_process_started_at = ?,
148
+ owner_protocol_version = ?, updated_at = ?, terminal_at = ?
149
+ WHERE id = ? AND state = ? AND cancellation_generation = ?`)
150
+ .run(input.state, input.transcriptCursor === undefined
151
+ ? optionalJson(job.transcriptCursor)
152
+ : optionalJson(input.transcriptCursor), input.childSessionId === undefined ? (job.childSessionId ?? null) : input.childSessionId, input.outcome === undefined ? optionalJson(job.outcome) : optionalJson(input.outcome), input.taskReceipt === undefined ? optionalJson(job.taskReceipt) : optionalJson(input.taskReceipt), input.batonReceipt === undefined ? optionalJson(job.batonReceipt) : optionalJson(input.batonReceipt), input.clearOwner ? null : (job.owner?.instanceId ?? null), input.clearOwner ? null : (job.owner?.pid ?? null), input.clearOwner ? null : (job.owner?.processStartedAt ?? null), input.clearOwner ? null : (job.owner?.protocolVersion ?? null), at, terminalAt, job.id, job.state, input.expectedCancellationGeneration);
153
+ if (Number(result.changes) !== 1)
154
+ throw new WorkflowTransitionError(`job ${job.id} changed concurrently`);
155
+ touchRun(context, job.runId, at);
156
+ appendEvent(context, job.runId, input.eventKey, input.eventType, { jobId: job.id, ...asObject(input.eventData) });
157
+ return requireJob(context, job.id);
158
+ });
159
+ }
@@ -0,0 +1,190 @@
1
+ import { desc, inArray, notInArray } from 'drizzle-orm';
2
+ import { isTerminalWorkflowJobState, isTerminalWorkflowPhase } from "../workflow/machine.js";
3
+ import { decodeRun } from "./codecs.js";
4
+ import { appendEvent } from "./events.js";
5
+ import { getWorkflowEffect, getWorkflowRun, listWorkflowJobs } from "./records.js";
6
+ import { workflowRuns } from "./schema.js";
7
+ import { ALL_JOB_STATES } from "./values.js";
8
+ export function getWorkflowProjection(context, runId) {
9
+ const run = getWorkflowRun(context, runId);
10
+ return run ? projectRun(context, run) : undefined;
11
+ }
12
+ export function listWorkflowProjections(context, options = {}) {
13
+ return context.orm
14
+ .select()
15
+ .from(workflowRuns)
16
+ .where(options.includeTerminal ? undefined : notInArray(workflowRuns.phase, ['completed', 'cancelled']))
17
+ .orderBy(desc(workflowRuns.updatedAt))
18
+ .all()
19
+ .map(row => projectRun(context, decodeRun(row)));
20
+ }
21
+ /**
22
+ * The sidebar keeps the identity of the newest Workflow that touched each live
23
+ * workspace after that run reaches a terminal phase. Limit the query to the
24
+ * workspaces on screen and project only one run per workspace: terminal history
25
+ * is unbounded, while `/api/state` is polled every few seconds.
26
+ */
27
+ export function listLatestWorkflowProjectionsForWorkspaces(context, workspaceIds) {
28
+ const ids = [...new Set(workspaceIds.filter(Boolean))];
29
+ if (!ids.length)
30
+ return [];
31
+ const placeholders = ids.map(() => '?').join(', ');
32
+ const newest = context.db
33
+ .prepare(`SELECT id FROM (
34
+ SELECT id, created_at, updated_at,
35
+ ROW_NUMBER() OVER (
36
+ PARTITION BY workspace_id
37
+ ORDER BY created_at DESC, updated_at DESC, id DESC
38
+ ) AS ordinal
39
+ FROM workflow_runs
40
+ WHERE workspace_id IN (${placeholders})
41
+ ) WHERE ordinal = 1
42
+ ORDER BY created_at DESC, updated_at DESC, id DESC`)
43
+ .all(...ids);
44
+ if (!newest.length)
45
+ return [];
46
+ const rows = context.orm
47
+ .select()
48
+ .from(workflowRuns)
49
+ .where(inArray(workflowRuns.id, newest.map(row => row.id)))
50
+ .all();
51
+ const byId = new Map(rows.map(row => [row.id, row]));
52
+ return newest.flatMap(({ id }) => {
53
+ const row = byId.get(id);
54
+ return row ? [projectRun(context, decodeRun(row))] : [];
55
+ });
56
+ }
57
+ /**
58
+ * Active runs always need driving. A cancelled run remains wakeable only while
59
+ * it has durable evidence that can still settle: an external effect whose
60
+ * outcome is unknown, an accepted outbox receipt awaiting promotion, or a
61
+ * delivered child turn whose final outcome has not appeared yet.
62
+ */
63
+ export function listWorkflowRunIdsNeedingWake(context) {
64
+ return context.db
65
+ .prepare(`SELECT r.id FROM workflow_runs r
66
+ WHERE r.phase NOT IN ('completed', 'cancelled')
67
+ OR (
68
+ r.phase = 'cancelled' AND (
69
+ EXISTS (
70
+ SELECT 1 FROM workflow_effects e
71
+ WHERE e.run_id = r.id AND (
72
+ e.state IN ('dispatched', 'ambiguous') OR
73
+ (e.state = 'committed' AND json_extract(e.receipt_json, '$.kind') = 'outbox')
74
+ )
75
+ ) OR EXISTS (
76
+ SELECT 1 FROM workflow_jobs j
77
+ WHERE j.run_id = r.id AND j.state = 'cancelled'
78
+ AND j.child_session_id IS NOT NULL AND j.outcome_json IS NULL
79
+ AND (
80
+ json_extract(j.task_receipt_json, '$.kind') = 'message' OR EXISTS (
81
+ SELECT 1 FROM workflow_effects task
82
+ WHERE task.job_id = j.id AND task.kind = 'send_task'
83
+ AND task.state = 'committed'
84
+ AND json_extract(task.receipt_json, '$.kind') = 'message'
85
+ )
86
+ )
87
+ )
88
+ )
89
+ )
90
+ ORDER BY r.updated_at DESC, r.id`)
91
+ .all().map(row => row.id);
92
+ }
93
+ export function projectRun(context, run) {
94
+ const jobs = listWorkflowJobs(context, run.id);
95
+ const summary = (role) => {
96
+ const selected = jobs.filter(job => job.role === role && job.state !== 'cancelled');
97
+ return {
98
+ requested: selected.length,
99
+ running: selected.filter(job => !isTerminalWorkflowJobState(job.state) && job.state !== 'dormant').length,
100
+ returned: selected.filter(job => job.state === 'returned').length,
101
+ failed: selected.filter(job => job.state === 'failed').length
102
+ };
103
+ };
104
+ const publicRole = (role) => ({
105
+ agentType: role.agentType,
106
+ model: role.model,
107
+ ...(role.effort === undefined ? {} : { effort: role.effort }),
108
+ ...(role.fast === undefined ? {} : { fast: role.fast })
109
+ });
110
+ const blocked = run.blocked;
111
+ const candidates = (blocked?.candidates ?? []).slice(0, 20).map(candidate => ({
112
+ id: candidate.id,
113
+ title: context.scrubPublicText(candidate.title).slice(0, 200),
114
+ repo: context.scrubPublicText(candidate.repo).slice(0, 200),
115
+ createdAt: candidate.createdAt
116
+ }));
117
+ const blockedEffect = blocked ? getWorkflowEffect(context, run.id, blocked.actionId) : undefined;
118
+ const adoptionKind = blocked?.candidates?.[0]?.kind ?? (blockedEffect?.kind === 'create_workspace' ? 'workspace' : 'session');
119
+ // Risky replay still needs the stable action id when reconciliation found no
120
+ // candidate. The fixed wire deliberately has no second public effect-id field,
121
+ // so retain the recovery envelope with an empty candidates array.
122
+ const exposeRecoveryAction = !!blocked &&
123
+ (candidates.length > 0 || blocked.retryClass === 'ambiguous') &&
124
+ (adoptionKind === 'workspace' || adoptionKind === 'session');
125
+ const outstanding = jobs.some(job => !isTerminalWorkflowJobState(job.state));
126
+ const implementationReturned = jobs.some(job => job.role === 'implementation' && job.state === 'returned');
127
+ return {
128
+ id: run.id,
129
+ ...(run.workspaceId ? { workspaceId: run.workspaceId } : {}),
130
+ ...(run.rootSessionId ? { rootSessionId: run.rootSessionId } : {}),
131
+ phase: run.phase,
132
+ objectiveExcerpt: context.scrubPublicText(run.objective).slice(0, 240),
133
+ roles: {
134
+ planning: publicRole(run.roles.planning),
135
+ exploration: publicRole(run.roles.exploration),
136
+ implementation: publicRole(run.roles.implementation)
137
+ },
138
+ jobs: { exploration: summary('exploration'), implementation: summary('implementation') },
139
+ ...(blocked
140
+ ? {
141
+ error: {
142
+ code: blocked.errorCode,
143
+ message: context.scrubPublicText(blocked.message).slice(0, 500),
144
+ retryable: blocked.retryClass === 'deterministic'
145
+ }
146
+ }
147
+ : {}),
148
+ ...(exposeRecoveryAction ? { adoption: { actionId: blocked.actionId, kind: adoptionKind, candidates } } : {}),
149
+ actions: {
150
+ canRetry: run.phase === 'blocked' && run.blocked?.retryClass === 'deterministic',
151
+ canAdopt: run.phase === 'blocked' && candidates.length > 0,
152
+ canReplayAmbiguous: run.phase === 'blocked' && run.blocked?.retryClass === 'ambiguous',
153
+ canCancel: !isTerminalWorkflowPhase(run.phase),
154
+ canComplete: run.phase === 'reviewing' && implementationReturned && !outstanding
155
+ },
156
+ createdAt: run.createdAt,
157
+ updatedAt: run.updatedAt
158
+ };
159
+ }
160
+ export function compactTerminalRuns(context, options) {
161
+ return context.immediate(() => {
162
+ const rows = context.db
163
+ .prepare(`SELECT id FROM workflow_runs
164
+ WHERE phase IN ('completed', 'cancelled') AND terminal_at IS NOT NULL AND terminal_at < ?
165
+ AND NOT EXISTS (
166
+ SELECT 1 FROM workflow_events e
167
+ WHERE e.run_id = workflow_runs.id AND e.type = 'terminal_compacted'
168
+ )
169
+ ORDER BY terminal_at LIMIT ?`)
170
+ .all(options.olderThan, Math.max(1, Math.min(options.limit ?? 20, 100)));
171
+ for (const row of rows) {
172
+ const jobs = listWorkflowJobs(context, row.id);
173
+ const effects = context.db
174
+ .prepare('SELECT state, COUNT(*) count FROM workflow_effects WHERE run_id = ? GROUP BY state')
175
+ .all(row.id);
176
+ context.db.prepare('DELETE FROM workflow_capabilities WHERE run_id = ?').run(row.id);
177
+ context.db.prepare('DELETE FROM workflow_jobs WHERE run_id = ?').run(row.id);
178
+ context.db.prepare('DELETE FROM workflow_effects WHERE run_id = ?').run(row.id);
179
+ context.db.prepare('DELETE FROM workflow_events WHERE run_id = ?').run(row.id);
180
+ appendEvent(context, row.id, 'terminal_compacted', 'terminal_compacted', {
181
+ jobs: Object.fromEntries(['exploration', 'implementation'].map(role => [
182
+ role,
183
+ Object.fromEntries(ALL_JOB_STATES.map(state => [state, jobs.filter(job => job.role === role && job.state === state).length]))
184
+ ])),
185
+ effects: Object.fromEntries(effects.map(effect => [effect.state, effect.count]))
186
+ });
187
+ }
188
+ return rows.length;
189
+ });
190
+ }
@@ -0,0 +1,46 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { OrchestrationError } from "./errors.js";
3
+ import { uiQuarantine, uiQuarantineSelectSchema } from "./schema.js";
4
+ import { externalFromColumns, nonEmpty, ownerFromColumns } from "./values.js";
5
+ export function activateUiQuarantine(context, input) {
6
+ return context.immediate(() => {
7
+ const current = getUiQuarantine(context);
8
+ if (current.active)
9
+ return current;
10
+ const at = context.now();
11
+ context.db
12
+ .prepare(`UPDATE ui_quarantine SET active = 1, action_id = ?, effect_id = ?, reason = ?,
13
+ owner_instance_id = ?, owner_pid = ?, owner_process_started_at = ?, owner_protocol_version = ?,
14
+ external_pid = ?, external_process_started_at = ?, external_process_group = ?,
15
+ created_at = ?, cleared_at = NULL, cleared_by = NULL WHERE id = 1`)
16
+ .run(input.actionId, input.effectId ?? null, nonEmpty(input.reason, 'quarantine reason'), input.owner?.instanceId ?? null, input.owner?.pid ?? null, input.owner?.processStartedAt ?? null, input.owner?.protocolVersion ?? null, input.externalProcess?.pid ?? null, input.externalProcess?.processStartedAt ?? null, input.externalProcess?.processGroup ?? null, at);
17
+ return getUiQuarantine(context);
18
+ });
19
+ }
20
+ export function clearUiQuarantine(context, clearedBy) {
21
+ return context.immediate(() => {
22
+ const result = context.db
23
+ .prepare('UPDATE ui_quarantine SET active = 0, cleared_at = ?, cleared_by = ? WHERE id = 1 AND active = 1')
24
+ .run(context.now(), nonEmpty(clearedBy, 'quarantine clearer'));
25
+ return Number(result.changes) === 1;
26
+ });
27
+ }
28
+ export function getUiQuarantine(context) {
29
+ const candidate = context.orm.select().from(uiQuarantine).where(eq(uiQuarantine.id, 1)).get();
30
+ if (!candidate)
31
+ throw new OrchestrationError('orchestration UI quarantine singleton is missing');
32
+ const row = uiQuarantineSelectSchema.parse(candidate);
33
+ const owner = ownerFromColumns(row);
34
+ const externalProcess = externalFromColumns(row);
35
+ return {
36
+ active: row.active,
37
+ ...(row.actionId === null ? {} : { actionId: row.actionId }),
38
+ ...(row.effectId === null ? {} : { effectId: row.effectId }),
39
+ ...(row.reason === null ? {} : { reason: row.reason }),
40
+ ...(owner ? { owner } : {}),
41
+ ...(externalProcess ? { externalProcess } : {}),
42
+ ...(row.createdAt === null ? {} : { createdAt: row.createdAt }),
43
+ ...(row.clearedAt === null ? {} : { clearedAt: row.clearedAt }),
44
+ ...(row.clearedBy === null ? {} : { clearedBy: row.clearedBy })
45
+ };
46
+ }
@@ -0,0 +1,115 @@
1
+ import { and, asc, eq, notInArray } from 'drizzle-orm';
2
+ import { decodeEffect, decodeEffectAttempt, decodeJob, decodeJobAttempt, decodeRun } from "./codecs.js";
3
+ import { WorkflowTransitionError } from "./errors.js";
4
+ import { workflowEffectAttempts, workflowEffects, workflowJobAttempts, workflowJobs, workflowRuns } from "./schema.js";
5
+ import { sameOwner } from "./values.js";
6
+ export function getWorkflowRun(context, id) {
7
+ const row = context.orm.select().from(workflowRuns).where(eq(workflowRuns.id, id)).get();
8
+ return row ? decodeRun(row) : undefined;
9
+ }
10
+ export function findActiveWorkflowByRoot(context, rootSessionId) {
11
+ const row = context.orm
12
+ .select()
13
+ .from(workflowRuns)
14
+ .where(and(eq(workflowRuns.rootSessionId, rootSessionId), notInArray(workflowRuns.phase, ['completed', 'cancelled'])))
15
+ .get();
16
+ return row ? decodeRun(row) : undefined;
17
+ }
18
+ export function requireRun(context, id) {
19
+ const run = getWorkflowRun(context, id);
20
+ if (!run)
21
+ throw new WorkflowTransitionError(`Workflow ${id} does not exist`);
22
+ return run;
23
+ }
24
+ export function checkRunGuard(run, expectedPhase, expectedCancellationGeneration) {
25
+ const phases = Array.isArray(expectedPhase) ? expectedPhase : expectedPhase ? [expectedPhase] : undefined;
26
+ if (phases && !phases.includes(run.phase)) {
27
+ throw new WorkflowTransitionError(`Workflow ${run.id} is ${run.phase}, expected ${phases.join(' or ')}`);
28
+ }
29
+ if (run.cancellationGeneration !== expectedCancellationGeneration) {
30
+ throw new WorkflowTransitionError(`Workflow ${run.id} cancellation generation changed`);
31
+ }
32
+ }
33
+ export function getWorkflowJob(context, id) {
34
+ const row = context.orm.select().from(workflowJobs).where(eq(workflowJobs.id, id)).get();
35
+ return row ? decodeJob(row) : undefined;
36
+ }
37
+ export function listWorkflowJobs(context, runId) {
38
+ return context.orm
39
+ .select()
40
+ .from(workflowJobs)
41
+ .where(eq(workflowJobs.runId, runId))
42
+ .orderBy(asc(workflowJobs.createdAt), asc(workflowJobs.id))
43
+ .all()
44
+ .map(row => decodeJob(row));
45
+ }
46
+ export function listWorkflowJobAttempts(context, jobId) {
47
+ return context.orm
48
+ .select()
49
+ .from(workflowJobAttempts)
50
+ .where(eq(workflowJobAttempts.jobId, jobId))
51
+ .orderBy(asc(workflowJobAttempts.attemptNumber))
52
+ .all()
53
+ .map(row => decodeJobAttempt(row));
54
+ }
55
+ export function requireJob(context, id) {
56
+ const job = getWorkflowJob(context, id);
57
+ if (!job)
58
+ throw new WorkflowTransitionError(`Workflow job ${id} does not exist`);
59
+ return job;
60
+ }
61
+ export function requireJobAttempt(context, id) {
62
+ const row = context.orm.select().from(workflowJobAttempts).where(eq(workflowJobAttempts.id, id)).get();
63
+ if (!row)
64
+ throw new WorkflowTransitionError(`Workflow job attempt ${id} does not exist`);
65
+ return decodeJobAttempt(row);
66
+ }
67
+ export function getWorkflowEffect(context, runId, actionId) {
68
+ const row = context.orm
69
+ .select()
70
+ .from(workflowEffects)
71
+ .where(and(eq(workflowEffects.runId, runId), eq(workflowEffects.actionId, actionId)))
72
+ .get();
73
+ return row ? decodeEffect(row) : undefined;
74
+ }
75
+ export function listWorkflowEffects(context, runId) {
76
+ return context.orm
77
+ .select()
78
+ .from(workflowEffects)
79
+ .where(eq(workflowEffects.runId, runId))
80
+ .orderBy(asc(workflowEffects.createdAt), asc(workflowEffects.id))
81
+ .all()
82
+ .map(row => decodeEffect(row));
83
+ }
84
+ export function requireEffectByAction(context, runId, actionId) {
85
+ const effect = getWorkflowEffect(context, runId, actionId);
86
+ if (!effect)
87
+ throw new WorkflowTransitionError(`Workflow effect ${actionId} does not exist`);
88
+ return effect;
89
+ }
90
+ export function requireEffect(context, id) {
91
+ const row = context.orm.select().from(workflowEffects).where(eq(workflowEffects.id, id)).get();
92
+ if (!row)
93
+ throw new WorkflowTransitionError(`Workflow effect ${id} does not exist`);
94
+ return decodeEffect(row);
95
+ }
96
+ export function requireEffectAttempt(context, id) {
97
+ const row = context.orm.select().from(workflowEffectAttempts).where(eq(workflowEffectAttempts.id, id)).get();
98
+ if (!row)
99
+ throw new WorkflowTransitionError(`Workflow effect attempt ${id} does not exist`);
100
+ return decodeEffectAttempt(row);
101
+ }
102
+ export function listWorkflowEffectAttempts(context, effectId) {
103
+ return context.orm
104
+ .select()
105
+ .from(workflowEffectAttempts)
106
+ .where(eq(workflowEffectAttempts.effectId, effectId))
107
+ .orderBy(asc(workflowEffectAttempts.attemptNumber))
108
+ .all()
109
+ .map(row => decodeEffectAttempt(row));
110
+ }
111
+ export function requireEffectOwner(effect, owner, attemptNumber) {
112
+ if (!effect.owner || !sameOwner(effect.owner, owner) || effect.attemptCount !== attemptNumber) {
113
+ throw new WorkflowTransitionError(`effect ${effect.actionId} is not owned by this relay attempt`);
114
+ }
115
+ }
@@ -0,0 +1,78 @@
1
+ import { asc } from 'drizzle-orm';
2
+ import { OrchestrationError } from "./errors.js";
3
+ import { relayInstanceSelectSchema, relayInstances } from "./schema.js";
4
+ import { jsonProperty, optionalJson, probeAlive, sameOwner, validateRelayIdentity } from "./values.js";
5
+ export function registerRelayInstance(context, input) {
6
+ validateRelayIdentity(input);
7
+ return context.immediate(() => {
8
+ const at = context.now();
9
+ context.db
10
+ .prepare(`INSERT INTO relay_instances (
11
+ instance_id, pid, process_started_at, protocol_version, can_drive_ui,
12
+ heartbeat_at, registered_at, metadata_json
13
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
14
+ ON CONFLICT(instance_id) DO UPDATE SET
15
+ pid = excluded.pid,
16
+ process_started_at = excluded.process_started_at,
17
+ protocol_version = excluded.protocol_version,
18
+ can_drive_ui = excluded.can_drive_ui,
19
+ heartbeat_at = excluded.heartbeat_at,
20
+ registered_at = CASE
21
+ WHEN relay_instances.pid = excluded.pid
22
+ AND relay_instances.process_started_at = excluded.process_started_at
23
+ THEN relay_instances.registered_at ELSE excluded.registered_at END,
24
+ metadata_json = excluded.metadata_json`)
25
+ .run(input.instanceId, input.pid, input.processStartedAt, input.protocolVersion, input.canDriveUi === false ? 0 : 1, at, at, optionalJson(input.metadata));
26
+ return requireRelayInstance(context, input.instanceId);
27
+ });
28
+ }
29
+ export function heartbeatRelayInstance(context, identity) {
30
+ return context.immediate(() => {
31
+ const result = context.db
32
+ .prepare(`UPDATE relay_instances SET heartbeat_at = ?
33
+ WHERE instance_id = ? AND pid = ? AND process_started_at = ? AND protocol_version = ?`)
34
+ .run(context.now(), identity.instanceId, identity.pid, identity.processStartedAt, identity.protocolVersion);
35
+ return Number(result.changes) === 1;
36
+ });
37
+ }
38
+ export function listRelayInstances(context) {
39
+ return context.orm
40
+ .select()
41
+ .from(relayInstances)
42
+ .orderBy(asc(relayInstances.registeredAt), asc(relayInstances.instanceId))
43
+ .all()
44
+ .map(candidate => {
45
+ const row = relayInstanceSelectSchema.parse(candidate);
46
+ return {
47
+ instanceId: row.instanceId,
48
+ pid: row.pid,
49
+ processStartedAt: row.processStartedAt,
50
+ protocolVersion: row.protocolVersion,
51
+ canDriveUi: row.canDriveUi,
52
+ heartbeatAt: row.heartbeatAt,
53
+ registeredAt: row.registeredAt,
54
+ ...jsonProperty('metadata', row.metadata)
55
+ };
56
+ });
57
+ }
58
+ export function findIncompatibleRelayInstances(context, current, probe = context.processProbe) {
59
+ return listRelayInstances(context).filter(instance => {
60
+ if (!instance.canDriveUi || sameOwner(instance, current) || instance.protocolVersion === current.protocolVersion)
61
+ return false;
62
+ return probeAlive(instance, probe);
63
+ });
64
+ }
65
+ export function requireRelayInstance(context, instanceId) {
66
+ const instance = listRelayInstances(context).find(entry => entry.instanceId === instanceId);
67
+ if (!instance)
68
+ throw new OrchestrationError(`relay instance ${instanceId} does not exist`);
69
+ return instance;
70
+ }
71
+ export function assertRelayRegistered(context, owner) {
72
+ const row = context.db
73
+ .prepare(`SELECT 1 present FROM relay_instances
74
+ WHERE instance_id = ? AND pid = ? AND process_started_at = ? AND protocol_version = ? AND can_drive_ui = 1`)
75
+ .get(owner.instanceId, owner.pid, owner.processStartedAt, owner.protocolVersion);
76
+ if (!row)
77
+ throw new OrchestrationError(`relay ${owner.instanceId} is not registered as this live UI-driving process`);
78
+ }