conductor-remote 1.106.3 → 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-CEuLSHL_.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/licenses/vscode-seti.txt +48 -0
- 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-BfTQtpd5.css +0 -1
- package/dist/assets/index-BiWQQKon.js +0 -58
package/dist-node/src/shared.js
CHANGED
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
* Each of these was a second copy before, and each copy was a way for two screens to
|
|
14
14
|
* disagree about the same workspace.
|
|
15
15
|
*/
|
|
16
|
-
/** Stable delimiter around the user-authored part of a server-expanded Workflow prompt. */
|
|
17
|
-
export const WORKFLOW_OBJECTIVE_HEADING = '## Workflow objective';
|
|
18
16
|
/**
|
|
19
17
|
* Parse the duration grammar accepted by `conductor-remote nosleep`: `90m`,
|
|
20
18
|
* `2h`, `30s`, or bare seconds. Kept here so the CLI and phone cannot drift.
|
|
@@ -28,6 +26,59 @@ export function parseDurationSeconds(raw) {
|
|
|
28
26
|
const seconds = amount * (unit === 'h' ? 3600 : unit === 'm' ? 60 : 1);
|
|
29
27
|
return Number.isSafeInteger(seconds) ? seconds : null;
|
|
30
28
|
}
|
|
29
|
+
/** Stable delimiter around the user-authored part of a server-expanded Workflow prompt. */
|
|
30
|
+
export const WORKFLOW_OBJECTIVE_HEADING = '## Workflow objective';
|
|
31
|
+
/**
|
|
32
|
+
* Private orchestration metadata is deliberately recognizable at every relay read
|
|
33
|
+
* boundary. The model needs this block in its original Conductor context, while the
|
|
34
|
+
* phone, search index, transcript attachments, logs, and MCP reads must never repeat
|
|
35
|
+
* it. Keep one versioned marker rather than teaching each consumer about individual
|
|
36
|
+
* fields as the state machine grows.
|
|
37
|
+
*/
|
|
38
|
+
export const WORKFLOW_PRIVATE_ENVELOPE_VERSION = 1;
|
|
39
|
+
export const WORKFLOW_PRIVATE_ENVELOPE_OPEN = `<conductor-remote-workflow-private version="${WORKFLOW_PRIVATE_ENVELOPE_VERSION}">`;
|
|
40
|
+
export const WORKFLOW_PRIVATE_ENVELOPE_CLOSE = '</conductor-remote-workflow-private>';
|
|
41
|
+
export const WORKFLOW_PRIVATE_REDACTION = '[Workflow orchestration metadata hidden]';
|
|
42
|
+
/** Prefix on the random 256-bit bearer values issued by the Workflow coordinator. */
|
|
43
|
+
export const WORKFLOW_CAPABILITY_PREFIX = `crwf_v${WORKFLOW_PRIVATE_ENVELOPE_VERSION}_`;
|
|
44
|
+
/**
|
|
45
|
+
* Add a non-secret version prefix to 32 bytes of base64url entropy. Besides making
|
|
46
|
+
* accidental disclosure recognizable, the prefix lets a future protocol rotate its
|
|
47
|
+
* scrub rule without treating arbitrary long identifiers as capabilities.
|
|
48
|
+
*/
|
|
49
|
+
export function workflowCapabilityToken(base64urlEntropy) {
|
|
50
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(base64urlEntropy)) {
|
|
51
|
+
throw new Error('Workflow capability entropy must be 32 bytes encoded as base64url.');
|
|
52
|
+
}
|
|
53
|
+
return `${WORKFLOW_CAPABILITY_PREFIX}${base64urlEntropy}`;
|
|
54
|
+
}
|
|
55
|
+
/** The opaque, machine-readable block delivered only into the root model's context. */
|
|
56
|
+
export function workflowPrivateEnvelope(value) {
|
|
57
|
+
return [
|
|
58
|
+
WORKFLOW_PRIVATE_ENVELOPE_OPEN,
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
workflow_id: value.workflowId,
|
|
61
|
+
phase_capability: value.phaseCapability,
|
|
62
|
+
cycle: value.cycle,
|
|
63
|
+
revision: value.revision,
|
|
64
|
+
allowed_roles: [...value.allowedRoles]
|
|
65
|
+
}),
|
|
66
|
+
WORKFLOW_PRIVATE_ENVELOPE_CLOSE
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Remove Workflow bearer material from anything leaving the relay's private
|
|
71
|
+
* orchestration boundary. An unterminated opening marker consumes the rest of the
|
|
72
|
+
* string: losing display text is safer than exposing a capability after a truncated
|
|
73
|
+
* database row or error. The loose-token pass also covers rendered tool arguments,
|
|
74
|
+
* where the model necessarily repeats the capability outside its original envelope.
|
|
75
|
+
*/
|
|
76
|
+
export function scrubWorkflowSecrets(text) {
|
|
77
|
+
return text
|
|
78
|
+
.replace(/<conductor-remote-workflow-private\b[^>]*>[\s\S]*?<\/conductor-remote-workflow-private>/gi, WORKFLOW_PRIVATE_REDACTION)
|
|
79
|
+
.replace(/<conductor-remote-workflow-private\b[^>]*>[\s\S]*$/gi, WORKFLOW_PRIVATE_REDACTION)
|
|
80
|
+
.replace(/(^|[^A-Za-z0-9_-])crwf_v\d+_[A-Za-z0-9_-]{16,}(?![A-Za-z0-9_-])/g, '$1[Workflow capability hidden]');
|
|
81
|
+
}
|
|
31
82
|
/**
|
|
32
83
|
* The branch minus its prefix, sentence-cased — Conductor's own fallback title while a
|
|
33
84
|
* workspace is still in progress. Prefix-agnostic (github_username / custom / none): it
|
|
@@ -209,6 +260,25 @@ export function modelCatalogIncludes(model, groups) {
|
|
|
209
260
|
const wanted = modelPickerLabel(model);
|
|
210
261
|
return groups.some(group => group.models.some(candidate => modelPickerLabel(candidate) === wanted));
|
|
211
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Complete picker reads are whole-menu observations, not provider slices. Ignore
|
|
265
|
+
* groups learned only from a successful single-model selection, then use the
|
|
266
|
+
* newest complete observation to describe what Conductor can select.
|
|
267
|
+
*/
|
|
268
|
+
export function newestModelSnapshot(groups) {
|
|
269
|
+
let newest = null;
|
|
270
|
+
for (const group of groups) {
|
|
271
|
+
if (!group.models.some(model => modelPickerLabel(model).trim()))
|
|
272
|
+
continue;
|
|
273
|
+
if (group.snapshotAt === null)
|
|
274
|
+
continue;
|
|
275
|
+
const observedAt = group.snapshotAt ?? group.updatedAt;
|
|
276
|
+
const newestObservedAt = newest ? (newest.snapshotAt ?? newest.updatedAt) : -1;
|
|
277
|
+
if (!newest || observedAt >= newestObservedAt)
|
|
278
|
+
newest = group;
|
|
279
|
+
}
|
|
280
|
+
return newest;
|
|
281
|
+
}
|
|
212
282
|
/** Stable, case-insensitive grouping shared by every model selector. */
|
|
213
283
|
export function groupModelPickerLabels(models) {
|
|
214
284
|
const grouped = new Map();
|
|
@@ -419,6 +489,13 @@ export function isLockedError(error) {
|
|
|
419
489
|
export function withoutWindowEvidence(error) {
|
|
420
490
|
return error.replace(/\s*\[window server:.*$/s, '').trim();
|
|
421
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Only diagnostic-bearing wire fields lose the AX/window-server tail. Relay log
|
|
494
|
+
* entries use `text`, so they retain the evidence the phone's log viewer needs.
|
|
495
|
+
*/
|
|
496
|
+
export function withoutClientWindowEvidence(value, field) {
|
|
497
|
+
return field === 'error' || field === 'message' || field === 'reason' ? withoutWindowEvidence(value) : value;
|
|
498
|
+
}
|
|
422
499
|
/**
|
|
423
500
|
* Header naming the push device that sent a request, so the relay can tell which chat
|
|
424
501
|
* that device has on screen and skip notifying it about that one chat (src/notify.ts).
|
|
@@ -8,9 +8,19 @@
|
|
|
8
8
|
* tool_result blocks, never the user's own words. Real prompts are the
|
|
9
9
|
* plain-text rows. Never render an SDK user frame as a user bubble.
|
|
10
10
|
*/
|
|
11
|
-
import { isToolResult } from "./shared.js";
|
|
11
|
+
import { isToolResult, scrubWorkflowSecrets } from "./shared.js";
|
|
12
12
|
const clip = (s, n) => (s.length > n ? `${s.slice(0, n)}…` : s);
|
|
13
13
|
const str = (v) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
14
|
+
/** One exit for every human-readable string parseMessage can expose. */
|
|
15
|
+
function scrubEntry(entry) {
|
|
16
|
+
const scrubbed = { ...entry };
|
|
17
|
+
for (const key of ['text', 'detail', 'output', 'subagentLabel', 'tool']) {
|
|
18
|
+
const value = scrubbed[key];
|
|
19
|
+
if (typeof value === 'string')
|
|
20
|
+
scrubbed[key] = scrubWorkflowSecrets(value);
|
|
21
|
+
}
|
|
22
|
+
return scrubbed;
|
|
23
|
+
}
|
|
14
24
|
/**
|
|
15
25
|
* Turn Conductor's outbox payload into the same user entry its eventual
|
|
16
26
|
* `session_messages` row will become. Outbox rows have no transcript rowid yet;
|
|
@@ -32,7 +42,7 @@ export function parseOutboxMessage(row) {
|
|
|
32
42
|
id: row.message_id,
|
|
33
43
|
rowid: 0,
|
|
34
44
|
role: 'user',
|
|
35
|
-
text,
|
|
45
|
+
text: scrubWorkflowSecrets(text),
|
|
36
46
|
ts: row.created_at,
|
|
37
47
|
queued: true
|
|
38
48
|
};
|
|
@@ -198,14 +208,14 @@ export function parseMessage(row, worktree = null) {
|
|
|
198
208
|
if (!content.startsWith('{')) {
|
|
199
209
|
if (!content.trim())
|
|
200
210
|
return [];
|
|
201
|
-
return [{ ...base, id: row.id, role: 'user', text: content }];
|
|
211
|
+
return [scrubEntry({ ...base, id: row.id, role: 'user', text: content })];
|
|
202
212
|
}
|
|
203
213
|
let parsed;
|
|
204
214
|
try {
|
|
205
215
|
parsed = JSON.parse(content);
|
|
206
216
|
}
|
|
207
217
|
catch {
|
|
208
|
-
return [{ ...base, id: row.id, role: 'system', text: clip(content, 200) }];
|
|
218
|
+
return [scrubEntry({ ...base, id: row.id, role: 'system', text: clip(content, 200) })];
|
|
209
219
|
}
|
|
210
220
|
const parentToolUseId = str(parsed.parent_tool_use_id);
|
|
211
221
|
const frameBase = { ...base, ...(parentToolUseId ? { parentToolUseId } : {}) };
|
|
@@ -221,17 +231,17 @@ export function parseMessage(row, worktree = null) {
|
|
|
221
231
|
if (parsed.type === 'error') {
|
|
222
232
|
const said = str(parsed.content);
|
|
223
233
|
if (said)
|
|
224
|
-
return [{ ...frameBase, id: row.id, role: 'system', text: clip(said, 200) }];
|
|
234
|
+
return [scrubEntry({ ...frameBase, id: row.id, role: 'system', text: clip(said, 200) })];
|
|
225
235
|
}
|
|
226
236
|
const blocks = parsed.message?.content;
|
|
227
237
|
if (!Array.isArray(blocks)) {
|
|
228
238
|
if (parsed.type === 'user' || parsed.type === 'assistant')
|
|
229
239
|
return [];
|
|
230
240
|
// Unknown frame shape — keep a dim raw dump so Conductor drift stays visible.
|
|
231
|
-
return [{ ...frameBase, id: row.id, role: 'system', text: clip(content, 200) }];
|
|
241
|
+
return [scrubEntry({ ...frameBase, id: row.id, role: 'system', text: clip(content, 200) })];
|
|
232
242
|
}
|
|
233
243
|
const entries = [];
|
|
234
|
-
const push = (e) => entries.push({ ...frameBase, ...e, id: `${row.id}:${entries.length}` });
|
|
244
|
+
const push = (e) => entries.push(scrubEntry({ ...frameBase, ...e, id: `${row.id}:${entries.length}` }));
|
|
235
245
|
// Images are numbered per row, because that is all a reference needs to find one again
|
|
236
246
|
// (`toolImageAt`) and a row may hold several results.
|
|
237
247
|
let imageIndex = 0;
|
|
@@ -308,8 +318,8 @@ const HEADINGS = {
|
|
|
308
318
|
/** One tool row per line, the shape `read_chat` prints: what it did, then what it did it to. */
|
|
309
319
|
function toolLine(e) {
|
|
310
320
|
if (e.error)
|
|
311
|
-
return `- [error] ${e.text}`;
|
|
312
|
-
return `- [${e.tool ?? 'tool'}] ${e.text}${e.detail ? ` — \`${e.detail}\`` : ''}`;
|
|
321
|
+
return `- [error] ${scrubWorkflowSecrets(e.text)}`;
|
|
322
|
+
return `- [${scrubWorkflowSecrets(e.tool ?? 'tool')}] ${scrubWorkflowSecrets(e.text)}${e.detail ? ` — \`${scrubWorkflowSecrets(e.detail)}\`` : ''}`;
|
|
313
323
|
}
|
|
314
324
|
function plural(n, one) {
|
|
315
325
|
return `${n} ${one}${n === 1 ? '' : 's'}`;
|
|
@@ -406,7 +416,7 @@ export function renderTranscript(entries, format) {
|
|
|
406
416
|
heading = want;
|
|
407
417
|
}
|
|
408
418
|
flushElisions();
|
|
409
|
-
out.push(e.role === 'tool' ? toolLine(e) : e.text);
|
|
419
|
+
out.push(e.role === 'tool' ? toolLine(e) : scrubWorkflowSecrets(e.text));
|
|
410
420
|
kept++;
|
|
411
421
|
}
|
|
412
422
|
// Anything dropped after the last kept entry still has to be admitted to.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { processIdentityAlive, terminateRecordedProcessGroup } from "./relay-processes.js";
|
|
2
|
+
const DEFAULT_SETTLE_MS = 750;
|
|
3
|
+
const DEFAULT_RECOVERY_LEASE_MS = 10_000;
|
|
4
|
+
function probeAlive(probe, identity) {
|
|
5
|
+
try {
|
|
6
|
+
return probe(identity);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// A failed inspection cannot prove that a process is gone.
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function sameRelay(left, right) {
|
|
14
|
+
return (left.instanceId === right.instanceId &&
|
|
15
|
+
left.pid === right.pid &&
|
|
16
|
+
left.processStartedAt === right.processStartedAt &&
|
|
17
|
+
left.protocolVersion === right.protocolVersion);
|
|
18
|
+
}
|
|
19
|
+
function recordedGroup(identity) {
|
|
20
|
+
if (identity.processGroup === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
return { pid: identity.pid, processStartedAt: identity.processStartedAt, processGroup: identity.processGroup };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Recover one expired process-shared UI lease without ever using time as proof
|
|
26
|
+
* that its owner or detached helper is dead.
|
|
27
|
+
*
|
|
28
|
+
* A live owner is never stolen. This relay may terminate its own overdue helper,
|
|
29
|
+
* while a successor may terminate a proven helper only after the relay owner is
|
|
30
|
+
* dead. Reclamation then uses the database CAS path, which creates persistent
|
|
31
|
+
* quarantine for a lease that crossed mayExecute. The temporary recovery lease
|
|
32
|
+
* is intentionally interactive: it performs no UI work, and this lets it clear a
|
|
33
|
+
* stale mutex even when that same crash already created the global quarantine.
|
|
34
|
+
*/
|
|
35
|
+
export async function recoverExpiredUiLease(db, relay, options = {}) {
|
|
36
|
+
const now = options.now ?? Date.now;
|
|
37
|
+
const probe = options.processProbe ?? processIdentityAlive;
|
|
38
|
+
const terminate = options.terminateGroup ?? terminateRecordedProcessGroup;
|
|
39
|
+
const pause = options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
40
|
+
const settleMs = Math.max(0, Math.floor(options.settleMs ?? DEFAULT_SETTLE_MS));
|
|
41
|
+
const recoveryLeaseMs = Math.max(1_000, Math.floor(options.recoveryLeaseMs ?? DEFAULT_RECOVERY_LEASE_MS));
|
|
42
|
+
const observed = db.getUiLeaseOwner();
|
|
43
|
+
if (!observed)
|
|
44
|
+
return { status: 'idle' };
|
|
45
|
+
if (observed.deadlineAt > now())
|
|
46
|
+
return { status: 'not_expired' };
|
|
47
|
+
const ownerAlive = probeAlive(probe, observed);
|
|
48
|
+
const ownedHere = sameRelay(observed, relay);
|
|
49
|
+
if (ownerAlive && !ownedHere)
|
|
50
|
+
return { status: 'owner_alive', owner: observed };
|
|
51
|
+
let terminatedExternal = false;
|
|
52
|
+
if (observed.externalProcess && probeAlive(probe, observed.externalProcess)) {
|
|
53
|
+
const group = recordedGroup(observed.externalProcess);
|
|
54
|
+
if (!group || !(await terminate(group, options.terminateOptions))) {
|
|
55
|
+
return { status: 'external_process_alive', owner: observed };
|
|
56
|
+
}
|
|
57
|
+
terminatedExternal = true;
|
|
58
|
+
}
|
|
59
|
+
// The live owning relay will observe its helper's exit, reconcile the effect,
|
|
60
|
+
// and release its own nonce. A watchdog must never steal even from itself.
|
|
61
|
+
if (ownerAlive) {
|
|
62
|
+
if (terminatedExternal && settleMs > 0)
|
|
63
|
+
await pause(settleMs);
|
|
64
|
+
return terminatedExternal
|
|
65
|
+
? { status: 'owned_external_terminated', owner: observed }
|
|
66
|
+
: { status: 'owner_alive', owner: observed };
|
|
67
|
+
}
|
|
68
|
+
// An external action can outlive its wrapper by a short interval. Delay the
|
|
69
|
+
// receipt decision, but retain the old mutex throughout this window.
|
|
70
|
+
if (observed.mayExecute && settleMs > 0)
|
|
71
|
+
await pause(settleMs);
|
|
72
|
+
const acquired = db.acquireUiLease({
|
|
73
|
+
owner: relay,
|
|
74
|
+
actionId: `ui-recovery:${observed.nonce}`,
|
|
75
|
+
deadlineAt: now() + recoveryLeaseMs,
|
|
76
|
+
priority: 'interactive'
|
|
77
|
+
});
|
|
78
|
+
if (acquired.status !== 'acquired') {
|
|
79
|
+
return { status: 'changed', owner: observed };
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const quarantine = db.getUiQuarantine();
|
|
83
|
+
return {
|
|
84
|
+
status: 'reclaimed',
|
|
85
|
+
owner: observed,
|
|
86
|
+
quarantined: quarantine.active &&
|
|
87
|
+
(quarantine.actionId === observed.actionId ||
|
|
88
|
+
(observed.effectId !== undefined && quarantine.effectId === observed.effectId))
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
db.releaseUiLease(acquired.lease);
|
|
93
|
+
}
|
|
94
|
+
}
|