conductor-remote 1.107.0 → 1.108.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 +65 -44
- package/dist/assets/{PierrePatch-CfKXuj7p.js → PierrePatch-DF-STc4J.js} +1 -1
- package/dist/assets/index-DM7eCDXg.css +1 -0
- package/dist/assets/index-DWbWwEOp.js +59 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/src/logbuf.js +4 -3
- package/dist-node/src/mcp-tools.js +48 -138
- package/dist-node/src/model-cache.js +22 -7
- package/dist-node/src/orchestration-db.js +2150 -0
- package/dist-node/src/orchestration-schema.generated.js +306 -0
- package/dist-node/src/orchestration-schema.js +417 -0
- package/dist-node/src/preview-urls.js +1 -1
- package/dist-node/src/reads.js +135 -6
- package/dist-node/src/relay-processes.js +230 -0
- package/dist-node/src/roles.js +64 -12
- package/dist-node/src/routes.js +14 -1
- package/dist-node/src/search-worker.js +3 -1
- package/dist-node/src/server.js +1050 -211
- package/dist-node/src/shared.js +79 -2
- package/dist-node/src/transcript.js +20 -10
- package/dist-node/src/ui-lease-watchdog.js +94 -0
- package/dist-node/src/workflow-coordinator.js +2119 -0
- package/dist-node/src/workflow-effect-runner.js +336 -0
- package/dist-node/src/workflow-http.js +141 -0
- package/dist-node/src/workflow-machine.js +112 -0
- package/dist-node/src/workflow.js +111 -22
- package/dist-node/src/writes.js +157 -12
- package/package.json +9 -2
- package/dist/assets/index-BHV0KmAq.js +0 -58
- package/dist/assets/index-DwXH_w7X.css +0 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { execFile, execFileSync } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* Decide from argv only. Never ask `ps` for the environment: a Conductor child or
|
|
6
|
+
* relay process can carry bearer tokens there.
|
|
7
|
+
*/
|
|
8
|
+
export function isUiCapableRelayArgs(args) {
|
|
9
|
+
const normalized = args.replace(/\s+/g, ' ').trim();
|
|
10
|
+
if (!/(^|\/)node(?:\s|$)/.test(normalized))
|
|
11
|
+
return false;
|
|
12
|
+
if (/\bconductor-remote(?:\/bin\/cli\.js)?\s+(?:mcp|service|config|logs|nosleep)(?:\s|$)/.test(normalized)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
// `yarn start` and the documented checkout command can leave the script path
|
|
16
|
+
// relative in `ps` (for example `node bin/cli.js`). Treat a whitespace or path
|
|
17
|
+
// separator as the same token boundary. Utility subcommands are excluded first
|
|
18
|
+
// so a relative MCP proxy can never be mistaken for a UI-driving relay.
|
|
19
|
+
if (/(?:^|\s|\/)(?:conductor-remote\/)?bin\/cli\.js\s+(?:mcp|service|config|logs|nosleep)(?:\s|$)/.test(normalized)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (/(?:^|\s|\/)(?:src\/server\.ts|dist-node\/src\/server\.js)(?:\s|$)/.test(normalized))
|
|
23
|
+
return true;
|
|
24
|
+
if (/(?:^|\s|\/)(?:conductor-remote\/)?bin\/cli\.js(?:\s+(?:start))?(?:\s|$)/.test(normalized))
|
|
25
|
+
return true;
|
|
26
|
+
return /\bconductor-remote(?:\s+start)?(?:\s|$)/.test(normalized);
|
|
27
|
+
}
|
|
28
|
+
/** Parse macOS `ps -axo pid=,lstart=,args=` without depending on locale words. */
|
|
29
|
+
export function parseUiCapableRelayProcesses(output, ownPid = process.pid) {
|
|
30
|
+
const found = [];
|
|
31
|
+
for (const line of output.split('\n')) {
|
|
32
|
+
// macOS lstart is five fields: weekday month day hh:mm:ss year. Capture by
|
|
33
|
+
// fields rather than a byte width so single-digit days and locale spacing work.
|
|
34
|
+
const match = /^\s*(\d+)\s+(\S+\s+\S+\s+\d+\s+\d\d:\d\d:\d\d\s+\d{4})\s+(.+)$/.exec(line);
|
|
35
|
+
if (!match)
|
|
36
|
+
continue;
|
|
37
|
+
const pid = Number(match[1]);
|
|
38
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid === ownPid)
|
|
39
|
+
continue;
|
|
40
|
+
const args = match[3].trim();
|
|
41
|
+
if (!isUiCapableRelayArgs(args))
|
|
42
|
+
continue;
|
|
43
|
+
found.push({ pid, processStartIdentity: match[2].replace(/\s+/g, ' '), args });
|
|
44
|
+
}
|
|
45
|
+
return found.sort((a, b) => a.pid - b.pid);
|
|
46
|
+
}
|
|
47
|
+
export async function listUiCapableRelayProcesses(ownPid = process.pid) {
|
|
48
|
+
const { stdout } = await execFileAsync('/bin/ps', ['-axo', 'pid=,lstart=,args='], {
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
timeout: 6_000,
|
|
51
|
+
maxBuffer: 4 * 1024 * 1024
|
|
52
|
+
});
|
|
53
|
+
return parseUiCapableRelayProcesses(stdout, ownPid);
|
|
54
|
+
}
|
|
55
|
+
function normalizeStartIdentity(value) {
|
|
56
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
57
|
+
}
|
|
58
|
+
const PROCESS_TABLE_ARGS = ['-axo', 'pid=,pgid=,lstart='];
|
|
59
|
+
function parseProcessTable(output) {
|
|
60
|
+
const entries = [];
|
|
61
|
+
for (const line of output.split('\n')) {
|
|
62
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
|
|
63
|
+
if (!match)
|
|
64
|
+
continue;
|
|
65
|
+
const pid = Number(match[1]);
|
|
66
|
+
const processGroup = Number(match[2]);
|
|
67
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isSafeInteger(processGroup) || processGroup <= 0) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
entries.push({ pid, processGroup, processStartedAt: normalizeStartIdentity(match[3]) });
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
function readProcessTableSync() {
|
|
75
|
+
try {
|
|
76
|
+
const output = execFileSync('/bin/ps', [...PROCESS_TABLE_ARGS], {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
timeout: 2_000,
|
|
79
|
+
maxBuffer: 4 * 1024 * 1024
|
|
80
|
+
});
|
|
81
|
+
return parseProcessTable(output);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function readProcessTable() {
|
|
88
|
+
try {
|
|
89
|
+
const { stdout } = await execFileAsync('/bin/ps', [...PROCESS_TABLE_ARGS], {
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
timeout: 2_000,
|
|
92
|
+
maxBuffer: 4 * 1024 * 1024
|
|
93
|
+
});
|
|
94
|
+
return parseProcessTable(stdout);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function exactLeader(entries, identity) {
|
|
101
|
+
const expectedStart = normalizeStartIdentity(identity.processStartedAt);
|
|
102
|
+
return entries.find(entry => entry.pid === identity.pid && normalizeStartIdentity(entry.processStartedAt) === expectedStart);
|
|
103
|
+
}
|
|
104
|
+
function validProcessGroup(processGroup) {
|
|
105
|
+
return Number.isSafeInteger(processGroup) && processGroup > 1;
|
|
106
|
+
}
|
|
107
|
+
function groupAlive(entries, processGroup) {
|
|
108
|
+
return entries.some(entry => entry.processGroup === processGroup);
|
|
109
|
+
}
|
|
110
|
+
function sameProcess(left, right) {
|
|
111
|
+
return left.pid === right.pid && left.processStartedAt === right.processStartedAt;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Exact PID + process-start check, extended for a recorded detached wrapper.
|
|
115
|
+
* A dead/reused leader is still conservatively alive while any member retains
|
|
116
|
+
* its process group. Failure to read the process table also fails closed.
|
|
117
|
+
*/
|
|
118
|
+
export function processIdentityAlive(identity) {
|
|
119
|
+
const entries = readProcessTableSync();
|
|
120
|
+
if (!entries)
|
|
121
|
+
return true;
|
|
122
|
+
if (exactLeader(entries, identity))
|
|
123
|
+
return true;
|
|
124
|
+
if (identity.processGroup === undefined)
|
|
125
|
+
return false;
|
|
126
|
+
if (!validProcessGroup(identity.processGroup))
|
|
127
|
+
return true;
|
|
128
|
+
return groupAlive(entries, identity.processGroup);
|
|
129
|
+
}
|
|
130
|
+
function duration(value, fallback, minimum) {
|
|
131
|
+
return value === undefined || !Number.isFinite(value) ? fallback : Math.max(minimum, Math.floor(value));
|
|
132
|
+
}
|
|
133
|
+
async function waitForGroupDeath(processGroup, waitMs, pollIntervalMs) {
|
|
134
|
+
const deadline = Date.now() + waitMs;
|
|
135
|
+
for (;;) {
|
|
136
|
+
const entries = await readProcessTable();
|
|
137
|
+
if (!entries)
|
|
138
|
+
return false;
|
|
139
|
+
if (!groupAlive(entries, processGroup))
|
|
140
|
+
return true;
|
|
141
|
+
const remaining = deadline - Date.now();
|
|
142
|
+
if (remaining <= 0)
|
|
143
|
+
return false;
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function signalGroup(processGroup, signal) {
|
|
148
|
+
try {
|
|
149
|
+
process.kill(-processGroup, signal);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH')
|
|
154
|
+
return true;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Terminate a detached wrapper's process group without risking a reused PGID.
|
|
160
|
+
*
|
|
161
|
+
* The recorded leader must still have the exact PID/start identity, must still
|
|
162
|
+
* lead the recorded group, and must not be this relay. Only then is SIGTERM sent.
|
|
163
|
+
* SIGKILL follows when descendants survive the grace period, unless a reused
|
|
164
|
+
* leader is observed. `true` means a later process-table snapshot proved that no
|
|
165
|
+
* member of the recorded group remains; every inspection failure returns false.
|
|
166
|
+
*/
|
|
167
|
+
export async function terminateRecordedProcessGroup(identity, options = {}) {
|
|
168
|
+
if (!Number.isSafeInteger(identity.pid) ||
|
|
169
|
+
identity.pid <= 1 ||
|
|
170
|
+
identity.pid === process.pid ||
|
|
171
|
+
!validProcessGroup(identity.processGroup) ||
|
|
172
|
+
identity.processGroup !== identity.pid ||
|
|
173
|
+
!normalizeStartIdentity(identity.processStartedAt)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const termGraceMs = duration(options.termGraceMs, 500, 0);
|
|
177
|
+
const killWaitMs = duration(options.killWaitMs, 2_000, 0);
|
|
178
|
+
const pollIntervalMs = duration(options.pollIntervalMs, 25, 1);
|
|
179
|
+
const initial = await readProcessTable();
|
|
180
|
+
if (!initial)
|
|
181
|
+
return false;
|
|
182
|
+
const leader = exactLeader(initial, identity);
|
|
183
|
+
if (!leader)
|
|
184
|
+
return !groupAlive(initial, identity.processGroup);
|
|
185
|
+
if (leader.processGroup !== identity.processGroup)
|
|
186
|
+
return false;
|
|
187
|
+
const initialMembers = initial.filter(entry => entry.processGroup === identity.processGroup);
|
|
188
|
+
if (!signalGroup(identity.processGroup, 'SIGTERM'))
|
|
189
|
+
return false;
|
|
190
|
+
if (await waitForGroupDeath(identity.processGroup, termGraceMs, pollIntervalMs))
|
|
191
|
+
return true;
|
|
192
|
+
const beforeKill = await readProcessTable();
|
|
193
|
+
if (!beforeKill)
|
|
194
|
+
return false;
|
|
195
|
+
if (!groupAlive(beforeKill, identity.processGroup))
|
|
196
|
+
return true;
|
|
197
|
+
const currentLeader = beforeKill.find(entry => entry.pid === identity.pid);
|
|
198
|
+
if (currentLeader &&
|
|
199
|
+
(normalizeStartIdentity(currentLeader.processStartedAt) !== normalizeStartIdentity(identity.processStartedAt) ||
|
|
200
|
+
currentLeader.processGroup !== identity.processGroup)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const survivingMembers = beforeKill.filter(entry => entry.processGroup === identity.processGroup);
|
|
204
|
+
if (!survivingMembers.some(current => initialMembers.some(initialMember => sameProcess(current, initialMember)))) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (!signalGroup(identity.processGroup, 'SIGKILL'))
|
|
208
|
+
return false;
|
|
209
|
+
return waitForGroupDeath(identity.processGroup, killWaitMs, pollIntervalMs);
|
|
210
|
+
}
|
|
211
|
+
export function currentProcessStartIdentity(pid = process.pid) {
|
|
212
|
+
const output = execFileSync('/bin/ps', ['-p', String(pid), '-o', 'lstart='], {
|
|
213
|
+
encoding: 'utf8',
|
|
214
|
+
timeout: 2_000,
|
|
215
|
+
maxBuffer: 64 * 1024
|
|
216
|
+
});
|
|
217
|
+
const identity = normalizeStartIdentity(output);
|
|
218
|
+
if (!identity)
|
|
219
|
+
throw new Error(`could not read process start identity for PID ${pid}`);
|
|
220
|
+
return identity;
|
|
221
|
+
}
|
|
222
|
+
/** A live UI-capable process must have a matching registration at this protocol. */
|
|
223
|
+
export function incompatibleRelayProcesses(processes, registrations, protocolVersion) {
|
|
224
|
+
return processes.filter(candidate => {
|
|
225
|
+
const registration = registrations.find(entry => entry.canDriveUi &&
|
|
226
|
+
entry.pid === candidate.pid &&
|
|
227
|
+
normalizeStartIdentity(entry.processStartedAt) === normalizeStartIdentity(candidate.processStartIdentity));
|
|
228
|
+
return !registration || registration.protocolVersion !== protocolVersion;
|
|
229
|
+
});
|
|
230
|
+
}
|
package/dist-node/src/roles.js
CHANGED
|
@@ -9,13 +9,35 @@
|
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { stateDir } from "./config.js";
|
|
12
|
-
import {
|
|
12
|
+
import { modelAgentType, modelCatalogIncludes, modelPickerLabel, newestModelSnapshot } from "./shared.js";
|
|
13
|
+
export { newestModelSnapshot } from "./shared.js";
|
|
13
14
|
const EFFORTS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode']);
|
|
14
15
|
const ROLE_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
15
16
|
const MAX_ROLES = 32;
|
|
16
17
|
const MAX_MODEL_LENGTH = 256;
|
|
17
18
|
const MAX_PREAMBLE_LENGTH = 50_000;
|
|
18
19
|
const ROLE_FIELDS = new Set(['model', 'effort', 'fast', 'preamble']);
|
|
20
|
+
/**
|
|
21
|
+
* Controls verified against Conductor's provider-specific composer UI.
|
|
22
|
+
*
|
|
23
|
+
* This is intentionally versioned and deliberately contains only providers whose
|
|
24
|
+
* controls have been measured. A provider recognized from its model label but absent
|
|
25
|
+
* here can still be used with its defaults; role configuration must omit controls we
|
|
26
|
+
* cannot prove Conductor can apply and read back.
|
|
27
|
+
*/
|
|
28
|
+
export const ROLE_CONTROL_CAPABILITIES = {
|
|
29
|
+
version: 1,
|
|
30
|
+
providers: {
|
|
31
|
+
claude: {
|
|
32
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
|
|
33
|
+
fast: true
|
|
34
|
+
},
|
|
35
|
+
codex: {
|
|
36
|
+
efforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
|
|
37
|
+
fast: true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
19
41
|
const batonPreamble = (role) => `You are the ${role} agent for this workspace. End your final answer with a \`## Baton\` section: Decision, Evidence, Files changed, Risks, Suggested next role.`;
|
|
20
42
|
export const DEFAULT_ROLES = {
|
|
21
43
|
version: 1,
|
|
@@ -89,25 +111,27 @@ function issue(code, message) {
|
|
|
89
111
|
return { code, message, retryable: false };
|
|
90
112
|
}
|
|
91
113
|
function controlIssue(name, role, agentType) {
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
const controls = ROLE_CONTROL_CAPABILITIES.providers[agentType];
|
|
115
|
+
if (role.effort !== undefined && !controls) {
|
|
116
|
+
return issue('invalid_request', `Role ${name} must omit effort because this provider's Conductor reasoning control is not verified.`);
|
|
94
117
|
}
|
|
95
|
-
if (role.effort
|
|
96
|
-
return issue('invalid_request', `Role ${name}
|
|
118
|
+
if (role.effort !== undefined && controls && !controls.efforts.includes(role.effort)) {
|
|
119
|
+
return issue('invalid_request', `Role ${name} cannot use ${role.effort} effort with its configured provider.`);
|
|
97
120
|
}
|
|
98
|
-
if (role.fast !== undefined && !
|
|
99
|
-
return issue('invalid_request', `Role ${name}
|
|
121
|
+
if (role.fast !== undefined && !controls?.fast) {
|
|
122
|
+
return issue('invalid_request', `Role ${name} must omit Fast mode because this provider's Conductor Fast control is not verified.`);
|
|
100
123
|
}
|
|
101
124
|
return null;
|
|
102
125
|
}
|
|
103
126
|
/** Picker validation stays separate from shape decoding so a vanished model remains editable. */
|
|
104
127
|
export function roleModelIssues(config, groups) {
|
|
105
128
|
const issues = [];
|
|
129
|
+
const snapshot = newestModelSnapshot(groups);
|
|
106
130
|
for (const [name, role] of Object.entries(config.roles)) {
|
|
107
|
-
if (!modelCatalogIncludes(role.model,
|
|
131
|
+
if (!snapshot || !modelCatalogIncludes(role.model, [snapshot])) {
|
|
108
132
|
issues.push({
|
|
109
133
|
role: name,
|
|
110
|
-
error: issue('model_missing', `Role ${name} needs an exact model from Conductor's
|
|
134
|
+
error: issue('model_missing', `Role ${name} needs an exact model from Conductor's newest picker snapshot; update it in the role editor.`)
|
|
111
135
|
});
|
|
112
136
|
continue;
|
|
113
137
|
}
|
|
@@ -130,10 +154,11 @@ export function resolveRole(config, name, groups) {
|
|
|
130
154
|
const role = config.roles[name];
|
|
131
155
|
if (!role)
|
|
132
156
|
return { ok: false, error: issue('role_not_found', `Unknown delegated role ${name}.`) };
|
|
133
|
-
|
|
157
|
+
const snapshot = newestModelSnapshot(groups);
|
|
158
|
+
if (!snapshot || !modelCatalogIncludes(role.model, [snapshot])) {
|
|
134
159
|
return {
|
|
135
160
|
ok: false,
|
|
136
|
-
error: issue('model_missing', `Role ${name} needs an exact model from Conductor's
|
|
161
|
+
error: issue('model_missing', `Role ${name} needs an exact model from Conductor's newest picker snapshot; update it in the role editor.`)
|
|
137
162
|
};
|
|
138
163
|
}
|
|
139
164
|
const agentType = modelAgentType(role.model);
|
|
@@ -152,11 +177,36 @@ export function resolveRole(config, name, groups) {
|
|
|
152
177
|
export class RoleStore {
|
|
153
178
|
file;
|
|
154
179
|
cache = null;
|
|
180
|
+
cacheStamp;
|
|
155
181
|
constructor(file = path.join(stateDir(), 'roles.json')) {
|
|
156
182
|
this.file = file;
|
|
157
183
|
}
|
|
184
|
+
fileStamp() {
|
|
185
|
+
try {
|
|
186
|
+
const stat = fs.statSync(this.file);
|
|
187
|
+
return `${stat.mtimeMs}:${stat.size}:${stat.ino}`;
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
if (err.code === 'ENOENT')
|
|
191
|
+
return null;
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
158
195
|
read() {
|
|
159
|
-
|
|
196
|
+
let stamp;
|
|
197
|
+
try {
|
|
198
|
+
stamp = this.fileStamp();
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
stamp = null;
|
|
202
|
+
this.cache = {
|
|
203
|
+
config: cloneConfig(DEFAULT_ROLES),
|
|
204
|
+
warning: `Could not inspect roles.json: ${err instanceof Error ? err.message : String(err)}`
|
|
205
|
+
};
|
|
206
|
+
this.cacheStamp = undefined;
|
|
207
|
+
return { ...this.cache, config: cloneConfig(this.cache.config) };
|
|
208
|
+
}
|
|
209
|
+
if (this.cache && this.cacheStamp === stamp)
|
|
160
210
|
return { ...this.cache, config: cloneConfig(this.cache.config) };
|
|
161
211
|
try {
|
|
162
212
|
this.cache = { config: decodeRoles(JSON.parse(fs.readFileSync(this.file, 'utf8'))) };
|
|
@@ -171,6 +221,7 @@ export class RoleStore {
|
|
|
171
221
|
};
|
|
172
222
|
}
|
|
173
223
|
}
|
|
224
|
+
this.cacheStamp = stamp;
|
|
174
225
|
return { ...this.cache, config: cloneConfig(this.cache.config) };
|
|
175
226
|
}
|
|
176
227
|
write(raw) {
|
|
@@ -197,6 +248,7 @@ export class RoleStore {
|
|
|
197
248
|
return { ok: false, error: `could not persist roles: ${err instanceof Error ? err.message : String(err)}` };
|
|
198
249
|
}
|
|
199
250
|
this.cache = { config };
|
|
251
|
+
this.cacheStamp = this.fileStamp();
|
|
200
252
|
return { ok: true, config: cloneConfig(config) };
|
|
201
253
|
}
|
|
202
254
|
}
|
package/dist-node/src/routes.js
CHANGED
|
@@ -101,6 +101,19 @@ export const routes = {
|
|
|
101
101
|
delegations: flat('GET', '/api/delegations'),
|
|
102
102
|
/** Dismiss one failed job without touching its chats or session-role identity. */
|
|
103
103
|
dismissDelegation: param('DELETE', '/api/delegations/:delegationId'),
|
|
104
|
+
/** UI-only intake for a durable Workflow; MCP deliberately exposes no start tool. */
|
|
105
|
+
workflows: flat('POST', '/api/workflows'),
|
|
106
|
+
/** Cancel without destructive Conductor UI actions; `clientId` travels in the query. */
|
|
107
|
+
workflow: param('DELETE', '/api/workflows/:workflowId'),
|
|
108
|
+
/** The only agent-facing Workflow mutation, authorized by the current phase capability. */
|
|
109
|
+
workflowDelegation: param('POST', '/api/workflows/:workflowId/delegations'),
|
|
110
|
+
/** Phone-only, idempotent recovery actions for a blocked Workflow. */
|
|
111
|
+
workflowRetry: param('POST', '/api/workflows/:workflowId/retry'),
|
|
112
|
+
workflowAdopt: param('POST', '/api/workflows/:workflowId/adopt'),
|
|
113
|
+
workflowReplay: param('POST', '/api/workflows/:workflowId/replay'),
|
|
114
|
+
workflowComplete: param('POST', '/api/workflows/:workflowId/complete'),
|
|
115
|
+
/** Phone-only acknowledgement that the user inspected Conductor after an ambiguous UI dispatch. */
|
|
116
|
+
confirmUiStable: flat('POST', '/api/ui-quarantine/confirm'),
|
|
104
117
|
// ── workspaces ──
|
|
105
118
|
createWorkspace: flat('POST', '/api/workspaces'),
|
|
106
119
|
/** One workspace by id, archived included — what `/api/state` deliberately leaves out. */
|
|
@@ -140,7 +153,7 @@ export const routes = {
|
|
|
140
153
|
uploadAttachment: param('POST', '/api/sessions/:sessionId/attachments'),
|
|
141
154
|
/** Copy a chat into a fresh tab beside it, as a Conductor attachment (src/attachments.ts). */
|
|
142
155
|
splitChat: param('POST', '/api/sessions/:sessionId/split'),
|
|
143
|
-
/**
|
|
156
|
+
/** Legacy intake retained only to reject new ordinary-chat delegation after migration. */
|
|
144
157
|
delegateTask: param('POST', '/api/sessions/:sessionId/delegate'),
|
|
145
158
|
/** Dismiss a prompt parked behind the lock screen (src/parked.ts). */
|
|
146
159
|
dismissParkedPrompt: param('DELETE', '/api/sessions/:sessionId/prompt')
|
|
@@ -6,7 +6,9 @@ import { ConductorDb } from "./db.js";
|
|
|
6
6
|
import { HIT_CLOSE, HIT_OPEN, matchQuery } from "./search.js";
|
|
7
7
|
import { parseMessage } from "./transcript.js";
|
|
8
8
|
/** Bump when extraction or an invariant such as one-chunk-per-source changes stored rows. */
|
|
9
|
-
|
|
9
|
+
// v3 rebuilds every chunk through WorkflowSecretScrubber so a capability indexed by
|
|
10
|
+
// an older relay cannot survive after the private-envelope boundary is introduced.
|
|
11
|
+
const SCHEMA_VERSION = 3;
|
|
10
12
|
/**
|
|
11
13
|
* Source rows advanced per tick. The cursor moves by *scanned* rowid rather than
|
|
12
14
|
* matched rowid, so a caught-up index re-scans nothing.
|