conductor-remote 1.125.0 → 1.126.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/dist/assets/{PierrePatch-Bdav8y3Q.js → PierrePatch-CxG5u4Gi.js} +1 -1
- package/dist/assets/index-iX8k3AVi.js +67 -0
- package/dist/assets/index-q-Z5Zz9y.css +1 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/src/files/attachments.js +2 -4
- package/dist-node/src/http/routes/prompts.js +1 -1
- package/dist-node/src/http/services/base.js +9 -3
- package/dist-node/src/http/services/workflow-notifications.js +53 -0
- package/dist-node/src/http/services/workflow.js +19 -4
- package/dist-node/src/mcp/client.js +66 -0
- package/dist-node/src/mcp.js +1 -54
- package/dist-node/src/orchestration/persistence/db.js +4 -2
- package/dist-node/src/orchestration/persistence/effect-transitions.js +17 -0
- package/dist-node/src/orchestration/persistence/events.js +10 -0
- package/dist-node/src/orchestration/workflow/blocked.js +93 -0
- package/dist-node/src/orchestration/workflow/compatibility.js +42 -0
- package/dist-node/src/orchestration/workflow/effects.js +46 -1
- package/dist-node/src/orchestration/workflow/errors.js +14 -0
- package/dist-node/src/orchestration/workflow/helpers.js +2 -0
- package/dist-node/src/orchestration/workflow/job-results.js +9 -1
- package/dist-node/src/orchestration/workflow/report.js +31 -0
- package/dist-node/src/orchestration/workflow/wake.js +20 -0
- package/dist-node/src/prefs.js +11 -2
- package/dist-node/src/shared.js +4 -0
- package/package.json +1 -1
- package/dist/assets/index-BteMsqk7.js +0 -65
- package/dist/assets/index-CJ_yLSwD.css +0 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { scrubWorkflowSecrets, withoutWindowEvidence } from "../../shared.js";
|
|
2
|
+
import { withUiPriority } from "../../writes/ui-lock.js";
|
|
3
|
+
import { errorMessage } from "./helpers.js";
|
|
4
|
+
export function workflowBlockMessage(notice, workspace) {
|
|
5
|
+
return withoutWindowEvidence(scrubWorkflowSecrets([
|
|
6
|
+
`Workflow paused in ${workspace} during ${notice.run.blocked?.resumePhase.replaceAll('_', ' ')}: ${notice.action}.`,
|
|
7
|
+
notice.run.blocked?.message,
|
|
8
|
+
`On your phone: ${notice.recovery}.`
|
|
9
|
+
]
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.join('\n\n')));
|
|
12
|
+
}
|
|
13
|
+
function blockedAction(context, run) {
|
|
14
|
+
const actionId = run.blocked?.actionId ?? '';
|
|
15
|
+
const kind = context.db.getWorkflowEffect(run.id, actionId)?.kind;
|
|
16
|
+
if (kind)
|
|
17
|
+
return ({
|
|
18
|
+
create_workspace: 'create the workspace',
|
|
19
|
+
configure_root: 'apply the planning settings',
|
|
20
|
+
send_root: 'send the objective to the planner',
|
|
21
|
+
open_child: 'open a helper chat',
|
|
22
|
+
configure_child: 'apply the helper settings',
|
|
23
|
+
send_task: 'send the helper assignment',
|
|
24
|
+
return_baton: 'return the helper result',
|
|
25
|
+
authorize_phase: 'deliver the next phase authorization'
|
|
26
|
+
}[kind] ?? 'advance the Workflow');
|
|
27
|
+
if (actionId.startsWith('compatibility:'))
|
|
28
|
+
return 'check relay compatibility';
|
|
29
|
+
if (actionId.startsWith('job:'))
|
|
30
|
+
return 'read the helper result';
|
|
31
|
+
if (actionId === 'bind-root')
|
|
32
|
+
return 'identify the planning chat';
|
|
33
|
+
return 'advance the Workflow';
|
|
34
|
+
}
|
|
35
|
+
/** Notifications are best effort, with a durable claim before each external send. */
|
|
36
|
+
export async function notifyBlockedRun(context, run) {
|
|
37
|
+
if (run.phase !== 'blocked' || !run.blocked)
|
|
38
|
+
return;
|
|
39
|
+
const event = context.db.listWorkflowEvents(run.id).findLast(event => event.type === 'workflow_blocked');
|
|
40
|
+
if (!event)
|
|
41
|
+
return;
|
|
42
|
+
const eventId = event.id;
|
|
43
|
+
const notice = {
|
|
44
|
+
run,
|
|
45
|
+
eventId,
|
|
46
|
+
action: blockedAction(context, run),
|
|
47
|
+
recovery: run.blocked.retryClass === 'deterministic'
|
|
48
|
+
? 'Retry saved action or Cancel'
|
|
49
|
+
: run.blocked.retryClass === 'ambiguous'
|
|
50
|
+
? 'Review risky replay or Cancel'
|
|
51
|
+
: 'Cancel this run'
|
|
52
|
+
};
|
|
53
|
+
async function attempt(channel, send) {
|
|
54
|
+
const eventKey = `block-${channel}:${eventId}`;
|
|
55
|
+
const claimed = context.db.idempotentMutation('workflow_block_notice', `${run.id}:${eventKey}`, { eventId, channel }, () => {
|
|
56
|
+
context.db.recordWorkflowObservation({
|
|
57
|
+
runId: run.id,
|
|
58
|
+
eventKey,
|
|
59
|
+
type: 'workflow_block_notice_claimed',
|
|
60
|
+
data: { channel, eventId }
|
|
61
|
+
});
|
|
62
|
+
return { runId: run.id };
|
|
63
|
+
});
|
|
64
|
+
if (claimed.replayed)
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
await send();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
context.db.recordWorkflowObservation({
|
|
71
|
+
runId: run.id,
|
|
72
|
+
eventKey: `${eventKey}:failed`,
|
|
73
|
+
type: 'workflow_block_notice_failed',
|
|
74
|
+
data: { channel, message: errorMessage(error) }
|
|
75
|
+
});
|
|
76
|
+
console.warn(`[workflow ${run.id}] ${channel} block notice failed: ${errorMessage(error)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (context.deps.notifyBlocked)
|
|
80
|
+
await attempt('push', () => context.deps.notifyBlocked(notice));
|
|
81
|
+
// A notice would make an unprompted root non-pristine and prevent its retry.
|
|
82
|
+
const canSendRoot = run.rootSessionId &&
|
|
83
|
+
!['creating_workspace', 'binding_root', 'pending_root'].includes(run.blocked.resumePhase) &&
|
|
84
|
+
run.blocked.errorCode !== 'workflow_root_not_pristine' &&
|
|
85
|
+
run.blocked.errorCode !== 'workflow_not_found';
|
|
86
|
+
if (!canSendRoot || !context.deps.sendBlockedNotice)
|
|
87
|
+
return;
|
|
88
|
+
await attempt('root', async () => {
|
|
89
|
+
if (context.db.getUiQuarantine().active)
|
|
90
|
+
return;
|
|
91
|
+
await withUiPriority('background', () => context.deps.sendBlockedNotice(notice));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { errorMessage } from "./helpers.js";
|
|
3
|
+
import { blockRun } from "./state.js";
|
|
4
|
+
const MAX_COMPATIBILITY_READ_FAILURES = 3;
|
|
5
|
+
function failuresSinceRecovery(context, runId) {
|
|
6
|
+
let count = 0;
|
|
7
|
+
for (const event of context.db.listWorkflowEvents(runId)) {
|
|
8
|
+
if (event.type === 'workflow_compatibility_read_failed')
|
|
9
|
+
count++;
|
|
10
|
+
if (event.type === 'workflow_compatibility_read_recovered' || event.type === 'workflow_retry_accepted')
|
|
11
|
+
count = 0;
|
|
12
|
+
}
|
|
13
|
+
return count;
|
|
14
|
+
}
|
|
15
|
+
/** One failed wake spends one persisted attempt. Nothing else runs on that tick. */
|
|
16
|
+
export function compatibilityReadFailed(context, run, error) {
|
|
17
|
+
const message = errorMessage(error);
|
|
18
|
+
context.db.recordWorkflowObservation({
|
|
19
|
+
runId: run.id,
|
|
20
|
+
eventKey: `compatibility-read:${randomUUID()}`,
|
|
21
|
+
type: 'workflow_compatibility_read_failed',
|
|
22
|
+
data: { message }
|
|
23
|
+
});
|
|
24
|
+
if (failuresSinceRecovery(context, run.id) >= MAX_COMPATIBILITY_READ_FAILURES) {
|
|
25
|
+
blockRun(context, run, {
|
|
26
|
+
actionId: `compatibility:${run.id}`,
|
|
27
|
+
errorCode: 'workflow_incompatible_relay',
|
|
28
|
+
message: `The relay process check failed ${MAX_COMPATIBILITY_READ_FAILURES} consecutive times. ${message}`,
|
|
29
|
+
retryClass: 'deterministic'
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Clear the streak only after an entire wake succeeds, including checks at the UI gate. */
|
|
34
|
+
export function compatibilityReadRecovered(context, runId) {
|
|
35
|
+
if (!failuresSinceRecovery(context, runId))
|
|
36
|
+
return;
|
|
37
|
+
context.db.recordWorkflowObservation({
|
|
38
|
+
runId,
|
|
39
|
+
eventKey: `compatibility-recovered:${randomUUID()}`,
|
|
40
|
+
type: 'workflow_compatibility_read_recovered'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -3,7 +3,7 @@ import { workflowCapabilityToken } from "../../shared.js";
|
|
|
3
3
|
import { withUiDispatchHook, withUiPriority } from "../../writes/ui-lock.js";
|
|
4
4
|
import { hashCapabilityToken, WorkflowTransitionError } from "../persistence/db.js";
|
|
5
5
|
import { quarantineAndBlock } from "./effect-recovery.js";
|
|
6
|
-
import { WorkflowCoordinatorError } from "./errors.js";
|
|
6
|
+
import { WorkflowCompatibilityReadError, WorkflowCoordinatorError, WorkflowRoleVerificationError } from "./errors.js";
|
|
7
7
|
import { cleanUnknown, errorCode, errorMessage, preExecutionRetryClass, sameRelay } from "./helpers.js";
|
|
8
8
|
import { isTerminalWorkflowPhase } from "./machine.js";
|
|
9
9
|
import { assertCompatible, blockRun, effectReadCall, requireEffect, requireRun } from "./state.js";
|
|
@@ -176,6 +176,51 @@ export async function reconcileClaimedEffect(context, run, effect, job) {
|
|
|
176
176
|
export async function failEffect(context, run, actionId, attemptNumber, error, job) {
|
|
177
177
|
let effect = requireEffect(context, run.id, actionId);
|
|
178
178
|
const currentRun = requireRun(context, run.id);
|
|
179
|
+
if (error instanceof WorkflowCompatibilityReadError &&
|
|
180
|
+
!isTerminalWorkflowPhase(currentRun.phase) &&
|
|
181
|
+
!effect.mayExecute) {
|
|
182
|
+
// The read failed before any external command was released. Settle this
|
|
183
|
+
// attempt, put the exact intent back, and let the wake budget decide when to pause.
|
|
184
|
+
const failure = {
|
|
185
|
+
runId: run.id,
|
|
186
|
+
actionId,
|
|
187
|
+
owner: context.relay,
|
|
188
|
+
attemptNumber,
|
|
189
|
+
errorCode: error.code,
|
|
190
|
+
errorMessage: error.message
|
|
191
|
+
};
|
|
192
|
+
if (effect.state === 'prepared' && sameRelay(effect.owner, context.relay)) {
|
|
193
|
+
context.db.markWorkflowEffectFailed(failure);
|
|
194
|
+
}
|
|
195
|
+
else if (effect.state === 'dispatched') {
|
|
196
|
+
context.db.markWorkflowEffectFailedBeforeMayExecute(failure);
|
|
197
|
+
}
|
|
198
|
+
if (requireEffect(context, run.id, actionId).state === 'failed') {
|
|
199
|
+
context.db.retryWorkflowEffect(run.id, actionId, `compatibility-retry:${actionId}:${attemptNumber}`);
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
if (error instanceof WorkflowRoleVerificationError &&
|
|
204
|
+
effect.state === 'dispatched' &&
|
|
205
|
+
effect.mayExecute &&
|
|
206
|
+
(effect.kind === 'configure_root' || effect.kind === 'configure_child')) {
|
|
207
|
+
effect = context.db.markWorkflowConfigurationRejected({
|
|
208
|
+
runId: run.id,
|
|
209
|
+
actionId,
|
|
210
|
+
owner: context.relay,
|
|
211
|
+
attemptNumber,
|
|
212
|
+
errorCode: error.code,
|
|
213
|
+
errorMessage: error.message,
|
|
214
|
+
evidence: { applied: true, matched: false }
|
|
215
|
+
});
|
|
216
|
+
blockRun(context, currentRun, {
|
|
217
|
+
actionId,
|
|
218
|
+
errorCode: error.code,
|
|
219
|
+
message: error.message,
|
|
220
|
+
retryClass: 'deterministic'
|
|
221
|
+
});
|
|
222
|
+
return { effect, changed: true };
|
|
223
|
+
}
|
|
179
224
|
if (currentRun.phase === 'cancelled' || currentRun.phase === 'completed') {
|
|
180
225
|
if (effect.state === 'dispatched') {
|
|
181
226
|
if (!effect.mayExecute) {
|
|
@@ -11,3 +11,17 @@ export class WorkflowCoordinatorError extends Error {
|
|
|
11
11
|
this.retryable = options.retryable ?? false;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/** Only raised after the role write has finished successfully and a present chat was read back. */
|
|
15
|
+
export class WorkflowRoleVerificationError extends WorkflowCoordinatorError {
|
|
16
|
+
constructor() {
|
|
17
|
+
super('workflow_role_verification_failed', 'Conductor no longer matches every frozen role setting; no fallback was selected.', { retryable: true });
|
|
18
|
+
this.name = 'WorkflowRoleVerificationError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** The process inventory could not be read. No incompatible relay was observed. */
|
|
22
|
+
export class WorkflowCompatibilityReadError extends WorkflowCoordinatorError {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super('workflow_incompatible_relay', message, { retryable: true });
|
|
25
|
+
this.name = 'WorkflowCompatibilityReadError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -31,6 +31,8 @@ export function sanitizeChildOutcome(outcome) {
|
|
|
31
31
|
? {
|
|
32
32
|
kind: 'success',
|
|
33
33
|
baton: scrubWorkflowSecrets(outcome.baton),
|
|
34
|
+
...(outcome.text === undefined ? {} : { text: scrubWorkflowSecrets(outcome.text) }),
|
|
35
|
+
...(outcome.assistantRowid === undefined ? {} : { assistantRowid: outcome.assistantRowid }),
|
|
34
36
|
...(outcome.evidence === undefined ? {} : { evidence: cleanUnknown(outcome.evidence) })
|
|
35
37
|
}
|
|
36
38
|
: {
|
|
@@ -4,6 +4,7 @@ import { markReceiptLost } from "./effect-recovery.js";
|
|
|
4
4
|
import { runDurableEffect } from "./effects.js";
|
|
5
5
|
import { assertDeliveryReceipt, effectGrant, guardJobs, isDeliveryReceipt, messageReceipt, privateCorrelationBlock, sanitizeChildOutcome, workflowEffectCorrelationMarker } from "./helpers.js";
|
|
6
6
|
import { phaseAfterDeliveredBaton } from "./machine.js";
|
|
7
|
+
import { workflowReportReturnText } from "./report.js";
|
|
7
8
|
import { blockRun, effectCall, effectReadCall, requireEffect, requireRun } from "./state.js";
|
|
8
9
|
export async function driveJobOutcome(context, run, job) {
|
|
9
10
|
const outcome = await context.deps.readChildOutcome({ run, job });
|
|
@@ -109,6 +110,11 @@ export async function driveJobBaton(context, run, job) {
|
|
|
109
110
|
// durable root message; accepted outbox rows do not satisfy the barrier.
|
|
110
111
|
if (designatedFinal && !grant)
|
|
111
112
|
return false;
|
|
113
|
+
// Old outcomes and already-prepared returns keep their original bytes. A new
|
|
114
|
+
// return freezes its public message before any UI attempt or receipt lookup.
|
|
115
|
+
const returnText = outcome.text === undefined
|
|
116
|
+
? outcome.baton
|
|
117
|
+
: workflowReportReturnText(job, outcome, await context.deps.materializeReport({ run, job, outcome }));
|
|
112
118
|
effect = context.db.prepareWorkflowEffect({
|
|
113
119
|
id: `${run.id}:${actionId}`,
|
|
114
120
|
runId: run.id,
|
|
@@ -117,6 +123,7 @@ export async function driveJobBaton(context, run, job) {
|
|
|
117
123
|
jobId: job.id,
|
|
118
124
|
target: { sessionId: run.rootSessionId },
|
|
119
125
|
inputs: {
|
|
126
|
+
returnText,
|
|
120
127
|
...(grant ? { grant } : {}),
|
|
121
128
|
correlationMarker: workflowEffectCorrelationMarker(run.id, actionId)
|
|
122
129
|
},
|
|
@@ -126,6 +133,7 @@ export async function driveJobBaton(context, run, job) {
|
|
|
126
133
|
}).effect;
|
|
127
134
|
}
|
|
128
135
|
const grant = effectGrant(effect) ?? undefined;
|
|
136
|
+
const savedText = effect.inputs?.returnText ?? outcome.baton;
|
|
129
137
|
let current = effect;
|
|
130
138
|
let changed = false;
|
|
131
139
|
if (effect.state !== 'committed') {
|
|
@@ -148,7 +156,7 @@ export async function driveJobBaton(context, run, job) {
|
|
|
148
156
|
...effectCall(run, effect, dispatch, job),
|
|
149
157
|
job,
|
|
150
158
|
sessionId: run.rootSessionId,
|
|
151
|
-
text: [
|
|
159
|
+
text: [savedText, envelope, privateCorrelationBlock(run.id, effect.actionId)].filter(Boolean).join('\n\n')
|
|
152
160
|
});
|
|
153
161
|
},
|
|
154
162
|
validate: assertDeliveryReceipt
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { scrubWorkflowSecrets } from "../../shared.js";
|
|
2
|
+
import { chatCursor } from "../../transcript/cursor.js";
|
|
3
|
+
/** Completion freezes the source. Returning a report never reads the child's later turns. */
|
|
4
|
+
export function workflowReportBody(run, job, outcome) {
|
|
5
|
+
if (!job.childSessionId || outcome.text === undefined)
|
|
6
|
+
throw new Error('Workflow report source is incomplete');
|
|
7
|
+
return scrubWorkflowSecrets([
|
|
8
|
+
`# Workflow ${job.role} report`,
|
|
9
|
+
'',
|
|
10
|
+
`Workflow: ${run.id}`,
|
|
11
|
+
`Workflow job: ${job.id} (${job.logicalKey}, attempt ${job.attemptCount})`,
|
|
12
|
+
`Child chat: ${job.childSessionId}`,
|
|
13
|
+
...(outcome.assistantRowid === undefined ? [] : [`Completion cursor: ${chatCursor(outcome.assistantRowid)}`]),
|
|
14
|
+
'',
|
|
15
|
+
outcome.text
|
|
16
|
+
].join('\n'));
|
|
17
|
+
}
|
|
18
|
+
export function workflowReportReturnText(job, outcome, token) {
|
|
19
|
+
if (!job.childSessionId)
|
|
20
|
+
throw new Error('Workflow report has no child chat');
|
|
21
|
+
const read = {
|
|
22
|
+
session_id: job.childSessionId,
|
|
23
|
+
...(outcome.assistantRowid === undefined ? {} : { near: chatCursor(outcome.assistantRowid), before: 6, after: 0 })
|
|
24
|
+
};
|
|
25
|
+
return scrubWorkflowSecrets([
|
|
26
|
+
outcome.baton,
|
|
27
|
+
`Workflow ${job.role} task ${job.logicalKey} completed.`,
|
|
28
|
+
`Report: ${token}`,
|
|
29
|
+
`For earlier investigation, use read_chat(${JSON.stringify(read)}).`
|
|
30
|
+
].join('\n\n'));
|
|
31
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { WorkflowTransitionError } from "../persistence/db.js";
|
|
2
|
+
import { notifyBlockedRun } from "./blocked.js";
|
|
3
|
+
import { compatibilityReadFailed, compatibilityReadRecovered } from "./compatibility.js";
|
|
2
4
|
import { markReceiptLost, reconcileBlockedEffect } from "./effect-recovery.js";
|
|
5
|
+
import { WorkflowCompatibilityReadError } from "./errors.js";
|
|
3
6
|
import { cleanUnknown, effectSessionId, errorMessage, isDeliveryReceipt, receiptEventSuffix, sanitizeChildOutcome } from "./helpers.js";
|
|
4
7
|
import { driveNextJob } from "./job-dispatch.js";
|
|
5
8
|
import { advanceDeliveredBarrier } from "./job-results.js";
|
|
@@ -16,6 +19,8 @@ export function wake(context, runId) {
|
|
|
16
19
|
}
|
|
17
20
|
export async function wakeInternal(context, runId) {
|
|
18
21
|
heartbeat(context);
|
|
22
|
+
let compatibilityFailed = false;
|
|
23
|
+
let compatibilityChecked = false;
|
|
19
24
|
for (let step = 0; step < MAX_WAKE_STEPS; step++) {
|
|
20
25
|
const run = requireRun(context, runId);
|
|
21
26
|
if (run.phase === 'cancelled') {
|
|
@@ -25,14 +30,21 @@ export async function wakeInternal(context, runId) {
|
|
|
25
30
|
if (run.phase === 'completed')
|
|
26
31
|
break;
|
|
27
32
|
if (run.phase === 'blocked') {
|
|
33
|
+
await notifyBlockedRun(context, run);
|
|
28
34
|
if (await reconcileBlockedEffect(context, run))
|
|
29
35
|
continue;
|
|
30
36
|
break;
|
|
31
37
|
}
|
|
32
38
|
try {
|
|
33
39
|
await assertCompatible(context);
|
|
40
|
+
compatibilityChecked = true;
|
|
34
41
|
}
|
|
35
42
|
catch (error) {
|
|
43
|
+
compatibilityFailed = true;
|
|
44
|
+
if (error instanceof WorkflowCompatibilityReadError) {
|
|
45
|
+
compatibilityReadFailed(context, requireRun(context, runId), error);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
36
48
|
blockRun(context, run, {
|
|
37
49
|
actionId: `compatibility:${run.id}`,
|
|
38
50
|
errorCode: 'workflow_incompatible_relay',
|
|
@@ -46,6 +58,11 @@ export async function wakeInternal(context, runId) {
|
|
|
46
58
|
changed = await driveOnce(context, run);
|
|
47
59
|
}
|
|
48
60
|
catch (error) {
|
|
61
|
+
if (error instanceof WorkflowCompatibilityReadError) {
|
|
62
|
+
compatibilityFailed = true;
|
|
63
|
+
compatibilityReadFailed(context, requireRun(context, runId), error);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
49
66
|
if (error instanceof WorkflowTransitionError) {
|
|
50
67
|
const current = requireRun(context, runId);
|
|
51
68
|
if (current.phase !== run.phase || current.cancellationGeneration !== run.cancellationGeneration)
|
|
@@ -56,6 +73,9 @@ export async function wakeInternal(context, runId) {
|
|
|
56
73
|
if (!changed)
|
|
57
74
|
break;
|
|
58
75
|
}
|
|
76
|
+
if (compatibilityChecked && !compatibilityFailed)
|
|
77
|
+
compatibilityReadRecovered(context, runId);
|
|
78
|
+
await notifyBlockedRun(context, requireRun(context, runId));
|
|
59
79
|
return projection(context, runId);
|
|
60
80
|
}
|
|
61
81
|
export async function observeCancelledRun(context, run) {
|
package/dist-node/src/prefs.js
CHANGED
|
@@ -72,7 +72,8 @@ function sanitizeAttachment(raw) {
|
|
|
72
72
|
token.length > MAX_ATTACHMENT_TOKEN_LENGTH ||
|
|
73
73
|
!Number.isSafeInteger(bytes) ||
|
|
74
74
|
bytes < 0 ||
|
|
75
|
-
|
|
75
|
+
// Forks reference an already-written transcript, which can exceed the upload limit.
|
|
76
|
+
(bytes > MAX_ATTACHMENT_BYTES && value.source !== 'fork'))
|
|
76
77
|
return null;
|
|
77
78
|
const match = attachmentPath.match(/^\.context\/attachments\/([A-Za-z0-9]{6})\/([^/]+)$/);
|
|
78
79
|
if (!match || match[2] !== name || attachmentName(name) !== name || token !== attachmentToken(name, attachmentPath))
|
|
@@ -80,7 +81,14 @@ function sanitizeAttachment(raw) {
|
|
|
80
81
|
const stageId = value.stageId;
|
|
81
82
|
if (stageId !== undefined && (typeof stageId !== 'string' || stageId !== match[1]))
|
|
82
83
|
return null;
|
|
83
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
name,
|
|
86
|
+
path: attachmentPath,
|
|
87
|
+
bytes,
|
|
88
|
+
token,
|
|
89
|
+
...(value.source === 'fork' ? { source: 'fork' } : {}),
|
|
90
|
+
...(stageId ? { stageId } : {})
|
|
91
|
+
};
|
|
84
92
|
}
|
|
85
93
|
function sanitizeAttachments(raw) {
|
|
86
94
|
if (!Array.isArray(raw))
|
|
@@ -151,6 +159,7 @@ function sameDraft(a, b) {
|
|
|
151
159
|
attachment.path === other.path &&
|
|
152
160
|
attachment.bytes === other.bytes &&
|
|
153
161
|
attachment.token === other.token &&
|
|
162
|
+
attachment.source === other.source &&
|
|
154
163
|
attachment.stageId === other.stageId);
|
|
155
164
|
}) &&
|
|
156
165
|
a.agent.model === b.agent.model &&
|
package/dist-node/src/shared.js
CHANGED
|
@@ -387,6 +387,10 @@ export function isVoiceSpeed(value) {
|
|
|
387
387
|
return typeof value === 'number' && Number.isFinite(value) && value >= 0.25 && value <= 1.5;
|
|
388
388
|
}
|
|
389
389
|
const ATTACHMENT_PREFIX = '.context/attachments/';
|
|
390
|
+
/** Conductor's attachment syntax encodes the whole relative path, including slashes. */
|
|
391
|
+
export function attachmentToken(name, relPath) {
|
|
392
|
+
return `@⟦${name}⟧(${encodeURIComponent(relPath)})`;
|
|
393
|
+
}
|
|
390
394
|
/**
|
|
391
395
|
* Read Conductor attachment tokens from prompt text.
|
|
392
396
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.126.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.15.0",
|
|
6
6
|
"description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
|