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.
- package/README.md +10 -8
- package/dist/assets/{PierrePatch-BPp5Cf-p.js → PierrePatch-D4YbKD8Q.js} +1 -1
- package/dist/assets/index-BRzsSW8g.css +1 -0
- package/dist/assets/index-BaS6qXUW.js +65 -0
- package/dist/index.html +2 -2
- package/dist/push-sw.js +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/scripts/nosleep-setup.js +2 -2
- package/dist-node/scripts/nosleep.js +3 -3
- package/dist-node/scripts/service/commands.js +32 -0
- package/dist-node/scripts/service/configuration.js +205 -0
- package/dist-node/scripts/service/environment.js +59 -0
- package/dist-node/scripts/service/flags.js +45 -0
- package/dist-node/scripts/service/installation.js +91 -0
- package/dist-node/scripts/service/launch-agent.js +134 -0
- package/dist-node/scripts/service/network.js +244 -0
- package/dist-node/scripts/service/presentation.js +101 -0
- package/dist-node/scripts/service/voice.js +113 -0
- package/dist-node/scripts/service.js +5 -980
- package/dist-node/src/{agent-config.js → agents/agent-config.js} +1 -1
- package/dist-node/src/{model-cache.js → agents/model-cache.js} +1 -1
- package/dist-node/src/{roles.js → agents/roles.js} +3 -3
- package/dist-node/src/db.js +1 -1
- package/dist-node/src/{parked.js → delivery/parked.js} +1 -1
- package/dist-node/src/{dev-server.js → dev-server/controller.js} +8 -291
- package/dist-node/src/dev-server/ports.js +63 -0
- package/dist-node/src/dev-server/processes.js +73 -0
- package/dist-node/src/dev-server/proxy.js +158 -0
- package/dist-node/src/{run-activity.js → dev-server/run-activity.js} +2 -2
- package/dist-node/src/dev-server/types.js +1 -0
- package/dist-node/src/{file-preview.js → files/file-preview.js} +1 -1
- package/dist-node/src/{change-stats.js → git/change-stats.js} +1 -1
- package/dist-node/src/{git.js → git/diff.js} +1 -1
- package/dist-node/src/{autoupdate.js → host/autoupdate.js} +1 -1
- package/dist-node/src/{funnel-watchdog.js → host/funnel-watchdog.js} +4 -4
- package/dist-node/src/{logbuf.js → host/logbuf.js} +1 -1
- package/dist-node/src/{nosleep-helper.js → host/nosleep-helper.js} +1 -1
- package/dist-node/src/{tailscale.js → host/tailscale.js} +1 -1
- package/dist-node/src/http/router-types.js +2 -0
- package/dist-node/src/http/router.js +78 -0
- package/dist-node/src/http/routes/create-workspace.js +90 -0
- package/dist-node/src/http/routes/files.js +53 -0
- package/dist-node/src/http/routes/prompts.js +363 -0
- package/dist-node/src/http/routes/sessions.js +279 -0
- package/dist-node/src/http/routes/state.js +186 -0
- package/dist-node/src/http/routes/system.js +220 -0
- package/dist-node/src/http/routes/voice.js +117 -0
- package/dist-node/src/http/routes/workflows.js +248 -0
- package/dist-node/src/http/routes/workspaces.js +267 -0
- package/dist-node/src/http/services/base.js +252 -0
- package/dist-node/src/http/services/delegations.js +301 -0
- package/dist-node/src/http/services/delivery.js +433 -0
- package/dist-node/src/http/services/files.js +199 -0
- package/dist-node/src/http/services/mcp.js +94 -0
- package/dist-node/src/http/services/responses.js +120 -0
- package/dist-node/src/http/services/voice.js +147 -0
- package/dist-node/src/http/services/workflow-probes.js +215 -0
- package/dist-node/src/http/services/workflow-state.js +195 -0
- package/dist-node/src/http/services/workflow.js +366 -0
- package/dist-node/src/http/services.js +35 -0
- package/dist-node/src/mcp/arguments.js +14 -0
- package/dist-node/src/mcp/dispatcher.js +56 -0
- package/dist-node/src/mcp/formatters.js +110 -0
- package/dist-node/src/mcp/protocol.js +7 -0
- package/dist-node/src/mcp/registry.js +66 -0
- package/dist-node/src/mcp/tools/agent-options.js +122 -0
- package/dist-node/src/mcp/tools/chat-actions.js +194 -0
- package/dist-node/src/mcp/tools/chats.js +194 -0
- package/dist-node/src/mcp/tools/delegation.js +135 -0
- package/dist-node/src/mcp/tools/system.js +135 -0
- package/dist-node/src/mcp/tools/voice.js +136 -0
- package/dist-node/src/mcp/tools/workspaces.js +259 -0
- package/dist-node/src/mcp/types.js +1 -0
- package/dist-node/src/mcp.js +6 -4
- package/dist-node/src/{notify.js → notifications/notify.js} +5 -5
- package/dist-node/src/{webpush.js → notifications/webpush.js} +1 -1
- package/dist-node/src/orchestration/delegation/codec.js +201 -0
- package/dist-node/src/{delegation-intake.js → orchestration/delegation/intake.js} +2 -2
- package/dist-node/src/{delegation-prompt.js → orchestration/delegation/prompt.js} +1 -1
- package/dist-node/src/orchestration/delegation/queue.js +255 -0
- package/dist-node/src/orchestration/delegation/store.js +111 -0
- package/dist-node/src/orchestration/delegation/types.js +2 -0
- package/dist-node/src/orchestration/persistence/capabilities.js +109 -0
- package/dist-node/src/orchestration/persistence/codecs.js +148 -0
- package/dist-node/src/orchestration/persistence/connection.js +205 -0
- package/dist-node/src/orchestration/persistence/db.js +101 -0
- package/dist-node/src/orchestration/persistence/effect-recovery.js +227 -0
- package/dist-node/src/orchestration/persistence/effect-transitions.js +157 -0
- package/dist-node/src/orchestration/persistence/effects.js +109 -0
- package/dist-node/src/orchestration/persistence/errors.js +39 -0
- package/dist-node/src/orchestration/persistence/events.js +29 -0
- package/dist-node/src/orchestration/persistence/idempotency.js +82 -0
- package/dist-node/src/orchestration/persistence/job-attempts.js +114 -0
- package/dist-node/src/orchestration/persistence/jobs.js +159 -0
- package/dist-node/src/orchestration/persistence/projections.js +190 -0
- package/dist-node/src/orchestration/persistence/quarantine.js +46 -0
- package/dist-node/src/orchestration/persistence/records.js +115 -0
- package/dist-node/src/orchestration/persistence/relays.js +78 -0
- package/dist-node/src/orchestration/persistence/runs.js +156 -0
- package/dist-node/src/{orchestration-schema.js → orchestration/persistence/schema.js} +1 -1
- package/dist-node/src/orchestration/persistence/types.js +1 -0
- package/dist-node/src/orchestration/persistence/ui-lease.js +197 -0
- package/dist-node/src/orchestration/persistence/values.js +114 -0
- package/dist-node/src/orchestration/workflow/commands.js +309 -0
- package/dist-node/src/orchestration/workflow/coordinator.js +28 -0
- package/dist-node/src/orchestration/workflow/effect-recovery.js +125 -0
- package/dist-node/src/orchestration/workflow/effects.js +307 -0
- package/dist-node/src/orchestration/workflow/errors.js +13 -0
- package/dist-node/src/orchestration/workflow/helpers.js +154 -0
- package/dist-node/src/orchestration/workflow/job-dispatch.js +312 -0
- package/dist-node/src/orchestration/workflow/job-results.js +280 -0
- package/dist-node/src/{workflow.js → orchestration/workflow/prompts.js} +2 -2
- package/dist-node/src/orchestration/workflow/root.js +293 -0
- package/dist-node/src/orchestration/workflow/start.js +99 -0
- package/dist-node/src/orchestration/workflow/state.js +105 -0
- package/dist-node/src/orchestration/workflow/types.js +1 -0
- package/dist-node/src/orchestration/workflow/wake.js +164 -0
- package/dist-node/src/prefs.js +1 -1
- package/dist-node/src/{background-tasks.js → reads/background-tasks.js} +1 -1
- package/dist-node/src/reads/messages.js +362 -0
- package/dist-node/src/reads/repository.js +87 -0
- package/dist-node/src/reads/sessions.js +165 -0
- package/dist-node/src/reads/types.js +1 -0
- package/dist-node/src/reads/workspaces.js +230 -0
- package/dist-node/src/reads/worktrees.js +45 -0
- package/dist-node/src/routes.js +5 -5
- package/dist-node/src/{search.js → search/coordinator.js} +4 -4
- package/dist-node/src/{search-worker.js → search/worker.js} +3 -3
- package/dist-node/src/server.js +14 -3930
- package/dist-node/src/settings.js +1 -1
- package/dist-node/src/shared.js +130 -9
- package/dist-node/src/{transcript.js → transcript/parser.js} +4 -124
- package/dist-node/src/{tool-usage-worker.js → usage/tool-usage-worker.js} +1 -1
- package/dist-node/src/{tool-usage.js → usage/tool-usage.js} +1 -1
- package/dist-node/src/voice/brief.js +22 -50
- package/dist-node/src/voice/context.js +1 -1
- package/dist-node/src/voice/dates.js +38 -0
- package/dist-node/src/voice/history.js +14 -5
- package/dist-node/src/voice/prompt.js +10 -9
- package/dist-node/src/voice/recall.js +115 -0
- package/dist-node/src/{speech.js → voice/speech.js} +1 -1
- package/dist-node/src/voice/tools.js +92 -3
- package/dist-node/src/voice/webrtc.js +1 -1
- package/dist-node/src/wire.js +2 -2
- package/dist-node/src/writes/actuator.js +124 -0
- package/dist-node/src/writes/agent-options.js +151 -0
- package/dist-node/src/writes/applescript/agent-options.applescript +334 -0
- package/dist-node/src/writes/applescript/chat-targeting.applescript +149 -0
- package/dist-node/src/writes/applescript/composer.applescript +195 -0
- package/dist-node/src/writes/applescript/hotspot.applescript +155 -0
- package/dist-node/src/writes/applescript/lifecycle.applescript +266 -0
- package/dist-node/src/writes/applescript/run-tasks.applescript +208 -0
- package/dist-node/src/writes/applescript/source.js +52 -0
- package/dist-node/src/writes/applescript/window.applescript +436 -0
- package/dist-node/src/writes/applescript/workspace-status.applescript +177 -0
- package/dist-node/src/writes/applescript/workspace-targeting.applescript +359 -0
- package/dist-node/src/writes/chats.js +160 -0
- package/dist-node/src/writes/guards.js +77 -0
- package/dist-node/src/writes/runner.js +67 -0
- package/dist-node/src/{sidecar.js → writes/sidecar.js} +1 -1
- package/dist-node/src/writes/system.js +81 -0
- package/dist-node/src/writes/targeting.js +121 -0
- package/dist-node/src/writes/types.js +1 -0
- package/dist-node/src/writes/ui-lock.js +229 -0
- package/dist-node/src/writes/workspaces.js +233 -0
- package/docs/voice-setup.md +34 -12
- package/package.json +2 -2
- package/dist/assets/index-DNLdIfZ2.css +0 -1
- package/dist/assets/index-QJR7tn2I.js +0 -61
- package/dist-node/src/conductor.applescript +0 -2279
- package/dist-node/src/delegations.js +0 -557
- package/dist-node/src/mcp-tools.js +0 -1319
- package/dist-node/src/orchestration-db.js +0 -2186
- package/dist-node/src/reads.js +0 -787
- package/dist-node/src/workflow-coordinator.js +0 -2119
- package/dist-node/src/writes.js +0 -1223
- /package/dist-node/src/{conductor-settings.js → agents/conductor-settings.js} +0 -0
- /package/dist-node/src/{firstprompt.js → delivery/firstprompt.js} +0 -0
- /package/dist-node/src/{sendonce.js → delivery/sendonce.js} +0 -0
- /package/dist-node/src/{preview-urls.js → dev-server/preview-urls.js} +0 -0
- /package/dist-node/src/{run-configs.js → dev-server/run-configs.js} +0 -0
- /package/dist-node/src/{attachments.js → files/attachments.js} +0 -0
- /package/dist-node/src/{icons.js → files/icons.js} +0 -0
- /package/dist-node/src/{staged-attachments.js → files/staged-attachments.js} +0 -0
- /package/dist-node/src/{fork-workspace.js → git/fork-workspace.js} +0 -0
- /package/dist-node/src/{merge.js → git/merge.js} +0 -0
- /package/dist-node/src/{pr.js → git/pr.js} +0 -0
- /package/dist-node/src/{nosleep.js → host/nosleep.js} +0 -0
- /package/dist-node/src/{relay-processes.js → host/relay-processes.js} +0 -0
- /package/dist-node/src/{ui-lease-watchdog.js → host/ui-lease-watchdog.js} +0 -0
- /package/dist-node/src/{wifi.js → host/wifi.js} +0 -0
- /package/dist-node/src/{orchestration-schema.generated.js → orchestration/persistence/schema.generated.js} +0 -0
- /package/dist-node/src/{workflow-effect-runner.js → orchestration/workflow/effect-runner.js} +0 -0
- /package/dist-node/src/{workflow-http.js → orchestration/workflow/http.js} +0 -0
- /package/dist-node/src/{workflow-machine.js → orchestration/workflow/machine.js} +0 -0
- /package/dist-node/src/{session-poller.js → reads/session-poller.js} +0 -0
- /package/dist-node/src/{context-breakdown.js → transcript/context-breakdown.js} +0 -0
- /package/dist-node/src/{chat-cursor.js → transcript/cursor.js} +0 -0
- /package/dist-node/src/{plan-usage.js → usage/plan-usage.js} +0 -0
- /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 "./
|
|
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);
|