conductor-remote 1.131.1 → 1.131.3
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/{AutoModelSettings-bXS1_9Es.js → AutoModelSettings-CX-bCwFp.js} +1 -1
- package/dist/assets/{PierrePatch-BZvD6Gh8.js → PierrePatch-BKDmpkha.js} +1 -1
- package/dist/assets/{index-x6nsv58D.js → index-Bsirxp4Y.js} +4 -4
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/dist-node/src/http/services/delegations.js +46 -13
- package/dist-node/src/http/services/delivery.js +46 -36
- package/dist-node/src/http/services/workflow.js +0 -3
- package/dist-node/src/orchestration/delegation/codec.js +2 -0
- package/dist-node/src/orchestration/delegation/prompt.js +26 -1
- package/dist-node/src/orchestration/delegation/queue.js +24 -3
- package/dist-node/src/reads/messages.js +13 -0
- package/dist-node/src/reads/repository.js +3 -0
- package/package.json +1 -1
package/dist/index.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<title>Conductor Remote</title>
|
|
26
26
|
<!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
|
|
27
27
|
<script src="/self-heal.js"></script>
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-Bsirxp4Y.js"></script>
|
|
29
29
|
<link rel="stylesheet" crossorigin href="/assets/index-Dbu5DOed.css">
|
|
30
30
|
<link rel="manifest" href="/manifest.webmanifest"></head>
|
|
31
31
|
<body>
|
package/dist/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const l=e||("document"in self?document.currentScript.src:"")||location.href;if(s[l])return;let o={};const t=e=>i(e,l),a={module:{uri:l},exports:o,require:t};s[l]=Promise.all(n.map(e=>a[e]||t(e))).then(e=>(r(...e),o))}}define(["./workbox-dcde9eb3"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"4e0b714c0ea53e85c167e6d8f5b767aa"},{url:"index.html",revision:"
|
|
1
|
+
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const l=e||("document"in self?document.currentScript.src:"")||location.href;if(s[l])return;let o={};const t=e=>i(e,l),a={module:{uri:l},exports:o,require:t};s[l]=Promise.all(n.map(e=>a[e]||t(e))).then(e=>(r(...e),o))}}define(["./workbox-dcde9eb3"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"4e0b714c0ea53e85c167e6d8f5b767aa"},{url:"index.html",revision:"f7c164d3affb878065d2f69b2fef65de"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/pierre-shiki-wasm-C8ETeZem.js",revision:null},{url:"assets/pierre-light-480U9XYS.js",revision:null},{url:"assets/pierre-dark-CyvmCCZW.js",revision:null},{url:"assets/index-Dbu5DOed.css",revision:null},{url:"assets/index-Bsirxp4Y.js",revision:null},{url:"assets/PierrePatch-BKDmpkha.js",revision:null},{url:"assets/AutoModelSettings-CX-bCwFp.js",revision:null},{url:"apple-touch-icon.png",revision:"1127bb396b4648add53dce3f22c92aee"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a9b0d962686287452216492cd2247499"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]})),e.registerRoute(({url:e})=>e.pathname.startsWith("/assets/diff-syntax/"),new e.CacheFirst({cacheName:"diff-syntax",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:31536e3})]}),"GET")});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeAttachment } from "../../files/attachments.js";
|
|
2
|
-
import { delegatedPrompt } from "../../orchestration/delegation/prompt.js";
|
|
2
|
+
import { delegatedPrompt, writeDelegatedAssignment } from "../../orchestration/delegation/prompt.js";
|
|
3
3
|
import { DelegationQueue } from "../../orchestration/delegation/queue.js";
|
|
4
4
|
import { delegationReturnAttachment, delegationReturnText } from "../../orchestration/delegation/return.js";
|
|
5
5
|
import { withoutWindowEvidence } from "../../shared.js";
|
|
@@ -60,8 +60,10 @@ export function createDelegationsServices(services) {
|
|
|
60
60
|
return delegationError('opening_failed', 'The Mac is locked — unlock it and try again.');
|
|
61
61
|
}
|
|
62
62
|
let handoff;
|
|
63
|
+
let assignment;
|
|
63
64
|
try {
|
|
64
65
|
handoff = delegationHandoff(job, ws);
|
|
66
|
+
assignment = writeDelegatedAssignment({ ...job, handoff }, ws.worktree);
|
|
65
67
|
}
|
|
66
68
|
catch (err) {
|
|
67
69
|
return delegationError('opening_failed', err instanceof Error ? err.message : String(err), false);
|
|
@@ -70,9 +72,7 @@ export function createDelegationsServices(services) {
|
|
|
70
72
|
if ('error' in opened) {
|
|
71
73
|
return delegationError('opening_failed', opened.result.error ?? 'Conductor did not open a child chat', opened.retryable !== false);
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
return delegationError('opening_failed', 'Conductor opened a tab but did not record its chat id', false);
|
|
75
|
-
return { ok: true, childSessionId: opened.sessionId, handoff };
|
|
75
|
+
return { ok: true, childSessionId: opened.sessionId, handoff, assignment };
|
|
76
76
|
}
|
|
77
77
|
async function configureDelegation(job) {
|
|
78
78
|
const ws = reads.getWorkspace(job.workspaceId);
|
|
@@ -98,8 +98,20 @@ export function createDelegationsServices(services) {
|
|
|
98
98
|
}
|
|
99
99
|
return { ok: true };
|
|
100
100
|
}
|
|
101
|
+
/** Correlate an ordinary delegation without treating changed text as successful delivery. */
|
|
102
|
+
function trackedReceipt(sessionId, text, delivery, attachment) {
|
|
103
|
+
if (delivery.messageId)
|
|
104
|
+
return reads.deliveryReceiptForId(sessionId, delivery.messageId);
|
|
105
|
+
const cursor = { rowid: delivery.rowid, outboxIds: new Set(delivery.outboxIds) };
|
|
106
|
+
// Each handoff/report has its own persisted attachment directory. Its ASCII
|
|
107
|
+
// URL survives UTF-8 chunk corruption and also identifies sends from older relays.
|
|
108
|
+
// This is correlation only: verify the complete raw text after saving the id.
|
|
109
|
+
const marker = attachment ? `(${encodeURIComponent(attachment.path)})` : '';
|
|
110
|
+
return (reads.deliveryReceiptSince(sessionId, text, cursor) ??
|
|
111
|
+
(marker && text.includes(marker) ? reads.deliveryReceiptContainingSince(sessionId, marker, cursor) : null));
|
|
112
|
+
}
|
|
101
113
|
/** Prepare durably before sending, then follow the accepted id without spending retries. */
|
|
102
|
-
async function trackedPrompt(ws, sessionId, text, delivery, code, queue = false) {
|
|
114
|
+
async function trackedPrompt(ws, sessionId, text, delivery, code, attachment, queue = false) {
|
|
103
115
|
if (!delivery) {
|
|
104
116
|
const cursor = reads.deliveryCursor(sessionId);
|
|
105
117
|
return {
|
|
@@ -109,9 +121,8 @@ export function createDelegationsServices(services) {
|
|
|
109
121
|
};
|
|
110
122
|
}
|
|
111
123
|
const cursor = { rowid: delivery.rowid, outboxIds: new Set(delivery.outboxIds) };
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
: reads.deliveryReceiptSince(sessionId, text, cursor);
|
|
124
|
+
const probe = () => trackedReceipt(sessionId, text, delivery, attachment);
|
|
125
|
+
let receipt = probe();
|
|
115
126
|
if (!receipt && delivery.messageId) {
|
|
116
127
|
return delegationError(code, 'the accepted delegated message was removed from Conductor before dispatch', false);
|
|
117
128
|
}
|
|
@@ -126,8 +137,8 @@ export function createDelegationsServices(services) {
|
|
|
126
137
|
queue: true
|
|
127
138
|
}));
|
|
128
139
|
receipt = result.ok
|
|
129
|
-
?
|
|
130
|
-
: await confirmDelivery(sessionId, text, cursor, Date.now() + CONFIRM_WINDOW_MS);
|
|
140
|
+
? probe()
|
|
141
|
+
: await confirmDelivery(sessionId, text, cursor, Date.now() + CONFIRM_WINDOW_MS, probe);
|
|
131
142
|
if (!receipt) {
|
|
132
143
|
return result.ok
|
|
133
144
|
? { ok: true, pending: true, delivery: { ...delivery, accepted: true } }
|
|
@@ -135,11 +146,17 @@ export function createDelegationsServices(services) {
|
|
|
135
146
|
}
|
|
136
147
|
}
|
|
137
148
|
if (!receipt) {
|
|
138
|
-
const result = await withUiPriority('background', () => deliverPrompt(ws, sessionId, text, SEND_BUDGET_MS, false, cursor));
|
|
149
|
+
const result = await withUiPriority('background', () => deliverPrompt(ws, sessionId, text, SEND_BUDGET_MS, false, cursor, probe));
|
|
139
150
|
if (!result.ok)
|
|
140
151
|
return delegationError(code, result.error ?? 'the delegated message did not land');
|
|
141
152
|
receipt = result.receipt;
|
|
142
153
|
}
|
|
154
|
+
if (delivery.messageId !== receipt.id) {
|
|
155
|
+
return { ok: true, pending: true, delivery: { ...delivery, messageId: receipt.id } };
|
|
156
|
+
}
|
|
157
|
+
if (!reads.deliveryTextMatches(sessionId, receipt.id, text)) {
|
|
158
|
+
return delegationError('delivery_altered', `Conductor accepted the delegated ${code === 'send_failed' ? 'assignment' : 'result'} as message ${receipt.id}, but its text changed in transit. It will not be resent automatically. Inspect chat ${sessionId} before continuing.`, false);
|
|
159
|
+
}
|
|
143
160
|
return receipt.kind === 'message'
|
|
144
161
|
? { ok: true, rowid: receipt.rowid }
|
|
145
162
|
: { ok: true, pending: true, delivery: { ...delivery, messageId: receipt.id } };
|
|
@@ -157,7 +174,7 @@ export function createDelegationsServices(services) {
|
|
|
157
174
|
catch (err) {
|
|
158
175
|
return delegationError('state_invalid', err instanceof Error ? err.message : String(err), false);
|
|
159
176
|
}
|
|
160
|
-
const result = await trackedPrompt(ws, job.childSessionId, text, job.sendDelivery, 'send_failed');
|
|
177
|
+
const result = await trackedPrompt(ws, job.childSessionId, text, job.sendDelivery, 'send_failed', job.assignment ?? job.handoff);
|
|
161
178
|
if (!result.ok)
|
|
162
179
|
return result;
|
|
163
180
|
return 'pending' in result
|
|
@@ -237,7 +254,7 @@ export function createDelegationsServices(services) {
|
|
|
237
254
|
catch (err) {
|
|
238
255
|
return delegationError('return_failed', err instanceof Error ? err.message : String(err), false);
|
|
239
256
|
}
|
|
240
|
-
const result = await trackedPrompt(ws, job.parentSessionId, text, job.returnDelivery, 'return_failed', job.returnMode === 'queue');
|
|
257
|
+
const result = await trackedPrompt(ws, job.parentSessionId, text, job.returnDelivery, 'return_failed', attachment, job.returnMode === 'queue');
|
|
241
258
|
if (!result.ok)
|
|
242
259
|
return result;
|
|
243
260
|
return 'pending' in result
|
|
@@ -252,6 +269,22 @@ export function createDelegationsServices(services) {
|
|
|
252
269
|
: { ok: true, returnRowid: result.rowid };
|
|
253
270
|
}
|
|
254
271
|
const delegationQueue = new DelegationQueue({
|
|
272
|
+
reconcile: job => {
|
|
273
|
+
const returning = job.failure?.code === 'return_failed';
|
|
274
|
+
if (!returning && job.failure?.code !== 'send_failed')
|
|
275
|
+
return null;
|
|
276
|
+
const delivery = returning ? job.returnDelivery : job.sendDelivery;
|
|
277
|
+
const sessionId = returning ? job.parentSessionId : job.childSessionId;
|
|
278
|
+
if (!delivery || !sessionId)
|
|
279
|
+
return null;
|
|
280
|
+
const text = returning ? job.returnText : delegatedPrompt(job);
|
|
281
|
+
if (!text)
|
|
282
|
+
return null;
|
|
283
|
+
const receipt = trackedReceipt(sessionId, text, delivery, returning ? job.returnAttachment : (job.assignment ?? job.handoff));
|
|
284
|
+
return receipt
|
|
285
|
+
? { stage: returning ? 'returning' : 'sending', delivery: { ...delivery, messageId: receipt.id } }
|
|
286
|
+
: null;
|
|
287
|
+
},
|
|
255
288
|
open: openDelegation,
|
|
256
289
|
configure: configureDelegation,
|
|
257
290
|
send: sendDelegation,
|
|
@@ -10,7 +10,7 @@ import { readPrefs } from "../../prefs.js";
|
|
|
10
10
|
import { setAgentOptions } from "../../writes/agent-options.js";
|
|
11
11
|
import { newChat } from "../../writes/chats.js";
|
|
12
12
|
import { lockBlocked, retryWontHelp, screenLocked, sendNeverStarted } from "../../writes/guards.js";
|
|
13
|
-
import { withUiPriority } from "../../writes/ui-lock.js";
|
|
13
|
+
import { uiTurn, withUiPriority } from "../../writes/ui-lock.js";
|
|
14
14
|
export function createDeliveryServices(services) {
|
|
15
15
|
const { reads, sleep, actuator, STAGED_ATTACHMENTS_DIR, assignWorkflowRoot, orchestration, stagedAttachmentIdsInObjective, modelCache } = services;
|
|
16
16
|
/**
|
|
@@ -41,10 +41,10 @@ export function createDeliveryServices(services) {
|
|
|
41
41
|
* a confirm *followed by another attempt* always gets its full window. Only the
|
|
42
42
|
* last confirm of all can be cut short, and nothing follows it to duplicate a row.
|
|
43
43
|
*/
|
|
44
|
-
async function confirmDelivery(sessionId, text, since, budgetDeadline) {
|
|
44
|
+
async function confirmDelivery(sessionId, text, since, budgetDeadline, receiptProbe = () => deliveredSince(sessionId, text, since)) {
|
|
45
45
|
const stopAt = Math.min(Date.now() + CONFIRM_WINDOW_MS, budgetDeadline);
|
|
46
46
|
for (;;) {
|
|
47
|
-
const receipt =
|
|
47
|
+
const receipt = receiptProbe();
|
|
48
48
|
if (receipt)
|
|
49
49
|
return receipt;
|
|
50
50
|
if (Date.now() >= stopAt)
|
|
@@ -108,38 +108,47 @@ export function createDeliveryServices(services) {
|
|
|
108
108
|
/**
|
|
109
109
|
* Open a chat tab in a workspace and come back with its id.
|
|
110
110
|
*
|
|
111
|
-
*
|
|
112
|
-
* the
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* `created_at` would hand back theirs.
|
|
111
|
+
* The UI lease spans the baseline, shortcut and DB receipt. Snapshotting before
|
|
112
|
+
* the lease or releasing it before the receipt lets concurrent opens claim each
|
|
113
|
+
* other's tabs. A script exit is only an attempted action; only one new row is
|
|
114
|
+
* success, including when a script fails after its shortcut already took effect.
|
|
116
115
|
*/
|
|
117
116
|
async function openChat(ws) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
117
|
+
return uiTurn(async () => {
|
|
118
|
+
const before = new Set(reads.listSessions(ws.id).map(s => s.id));
|
|
119
|
+
const result = await newChat(ws);
|
|
120
|
+
for (let i = 0; i <= 12; i++) {
|
|
121
|
+
const fresh = reads.listSessions(ws.id).filter(s => !before.has(s.id));
|
|
122
|
+
if (fresh.length > 1) {
|
|
123
|
+
return {
|
|
124
|
+
error: true,
|
|
125
|
+
retryable: false,
|
|
126
|
+
result: {
|
|
127
|
+
ok: false,
|
|
128
|
+
strategy: result.strategy,
|
|
129
|
+
error: 'more than one new chat appeared; refusing to guess which one this request opened'
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (fresh[0])
|
|
134
|
+
return { sessionId: fresh[0].id };
|
|
135
|
+
if (i < 12)
|
|
136
|
+
await sleep(500);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
error: true,
|
|
140
|
+
// A late tab cannot be distinguished from a retry's tab. Do not replay an
|
|
141
|
+
// unconfirmed creation automatically; the user can inspect the workspace.
|
|
142
|
+
retryable: false,
|
|
143
|
+
result: result.ok
|
|
144
|
+
? {
|
|
131
145
|
ok: false,
|
|
132
|
-
strategy:
|
|
133
|
-
error: '
|
|
146
|
+
strategy: result.strategy,
|
|
147
|
+
error: 'Conductor did not confirm a new chat. Check the workspace before trying again.'
|
|
134
148
|
}
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return { sessionId: fresh[0].id };
|
|
139
|
-
}
|
|
140
|
-
// The tab is almost certainly on screen; only its id is missing. Say so rather than
|
|
141
|
-
// failing the call, so a caller can still tell the user where the work went.
|
|
142
|
-
return { sessionId: null };
|
|
149
|
+
: result
|
|
150
|
+
};
|
|
151
|
+
});
|
|
143
152
|
}
|
|
144
153
|
/**
|
|
145
154
|
* Deliver a prompt to one chat and confirm it landed, retrying until the caller's
|
|
@@ -166,7 +175,7 @@ export function createDeliveryServices(services) {
|
|
|
166
175
|
* minutes schedule sits *outside* this and is unaffected — it retries a delivery that
|
|
167
176
|
* never got off the ground (worktree still setting up), not one Conductor fumbled.
|
|
168
177
|
*/
|
|
169
|
-
async function deliverPrompt(ws, sessionId, text, budgetMs = SEND_BUDGET_MS, queue = false, cursor) {
|
|
178
|
+
async function deliverPrompt(ws, sessionId, text, budgetMs = SEND_BUDGET_MS, queue = false, cursor, receiptProbe) {
|
|
170
179
|
const located = locateChat(ws, sessionId);
|
|
171
180
|
if ('error' in located)
|
|
172
181
|
return { ok: false, strategy: actuator.name, attempts: 0, error: located.error };
|
|
@@ -174,6 +183,7 @@ export function createDeliveryServices(services) {
|
|
|
174
183
|
// *this* prompt arrive since we started", so a retry can't be fooled by an older
|
|
175
184
|
// identical prompt moving from the outbox into a new transcript row.
|
|
176
185
|
const before = cursor ?? reads.deliveryCursor(sessionId);
|
|
186
|
+
const probe = receiptProbe ?? (() => deliveredSince(sessionId, text, before));
|
|
177
187
|
const label = ws.branch ?? ws.id;
|
|
178
188
|
const deadline = Date.now() + budgetMs;
|
|
179
189
|
let attempts = 0;
|
|
@@ -181,7 +191,7 @@ export function createDeliveryServices(services) {
|
|
|
181
191
|
for (;;) {
|
|
182
192
|
// A persisted delegation cursor can already have a receipt after a restart
|
|
183
193
|
// or a late acceptance. Recover it before touching the composer again.
|
|
184
|
-
const existing =
|
|
194
|
+
const existing = probe();
|
|
185
195
|
if (existing)
|
|
186
196
|
return { ok: true, strategy: last.strategy, attempts, receipt: existing };
|
|
187
197
|
attempts++;
|
|
@@ -198,8 +208,8 @@ export function createDeliveryServices(services) {
|
|
|
198
208
|
// an *earlier* attempt's row can be arriving, and typing again over that is the
|
|
199
209
|
// duplicate this whole path exists to avoid.
|
|
200
210
|
const landed = sendNeverStarted(last.error)
|
|
201
|
-
?
|
|
202
|
-
: await confirmDelivery(sessionId, text, before, deadline);
|
|
211
|
+
? probe()
|
|
212
|
+
: await confirmDelivery(sessionId, text, before, deadline, probe);
|
|
203
213
|
if (landed) {
|
|
204
214
|
if (attempts > 1)
|
|
205
215
|
console.info(`[relay] send to ${label} landed on attempt ${attempts}`);
|
|
@@ -220,7 +230,7 @@ export function createDeliveryServices(services) {
|
|
|
220
230
|
}
|
|
221
231
|
const tried = attempts > 1 ? ` (tried ${attempts}×)` : '';
|
|
222
232
|
const error = last.ok
|
|
223
|
-
? `
|
|
233
|
+
? `Could not confirm the sent message in Conductor${tried}. Check the chat before trying again.`
|
|
224
234
|
: `${last.error}${tried}`;
|
|
225
235
|
console.warn(`[relay] send to ${label} failed after ${attempts} attempt(s): ${error}`);
|
|
226
236
|
return { ok: false, strategy: last.strategy, attempts, error };
|
|
@@ -137,9 +137,6 @@ export function createWorkflowServices(services) {
|
|
|
137
137
|
if ('error' in opened) {
|
|
138
138
|
throw new WorkflowCoordinatorError('workflow_effect_failed', opened.result.error ?? 'Conductor did not open a tracked child chat.', { retryable: opened.retryable !== false });
|
|
139
139
|
}
|
|
140
|
-
if (!opened.sessionId) {
|
|
141
|
-
throw new WorkflowCoordinatorError('workflow_effect_failed', 'Conductor opened a child tab but did not record its exact session ID.');
|
|
142
|
-
}
|
|
143
140
|
return { sessionId: opened.sessionId };
|
|
144
141
|
});
|
|
145
142
|
},
|
|
@@ -29,6 +29,7 @@ const ERROR_CODES = new Set([
|
|
|
29
29
|
'opening_failed',
|
|
30
30
|
'configuration_failed',
|
|
31
31
|
'send_failed',
|
|
32
|
+
'delivery_altered',
|
|
32
33
|
'completion_failed',
|
|
33
34
|
'return_failed'
|
|
34
35
|
]);
|
|
@@ -170,6 +171,7 @@ export function decodeDelegation(raw) {
|
|
|
170
171
|
createdAt: integer(value.createdAt, 'createdAt'),
|
|
171
172
|
updatedAt: integer(value.updatedAt, 'updatedAt'),
|
|
172
173
|
...(value.handoff === undefined ? {} : { handoff: decodeAttachment(value.handoff) }),
|
|
174
|
+
...(value.assignment === undefined ? {} : { assignment: decodeAttachment(value.assignment) }),
|
|
173
175
|
...(value.sendDelivery === undefined ? {} : { sendDelivery: decodeDelivery(value.sendDelivery, 'sendDelivery') }),
|
|
174
176
|
...(value.sentRowid === undefined ? {} : { sentRowid: integer(value.sentRowid, 'sentRowid', 1) }),
|
|
175
177
|
...(value.completionRowid === undefined
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import { attachmentPrompt } from "../../files/attachments.js";
|
|
1
|
+
import { attachmentPrompt, writeAttachment } from "../../files/attachments.js";
|
|
2
2
|
import { chatCursor } from "../../transcript/cursor.js";
|
|
3
|
+
/**
|
|
4
|
+
* Conductor's runtime decodes socket chunks independently, so a split UTF-8
|
|
5
|
+
* character can be replaced before the prompt reaches its outbox. Keep new
|
|
6
|
+
* assignments in a file and send only an ASCII reference through that runtime.
|
|
7
|
+
* The reference is persisted with the job before any send. Existing jobs without
|
|
8
|
+
* it must retain the exact inline text used by their saved delivery receipts.
|
|
9
|
+
*/
|
|
10
|
+
export function writeDelegatedAssignment(job, worktree) {
|
|
11
|
+
if (!job.handoff)
|
|
12
|
+
throw new Error('the delegated handoff is missing');
|
|
13
|
+
const body = [
|
|
14
|
+
'# Delegated assignment',
|
|
15
|
+
`Read the parent transcript at ${JSON.stringify(job.handoff.path)} as background context before working.`,
|
|
16
|
+
delegatedPrompt({ ...job, assignment: undefined })
|
|
17
|
+
].join('\n\n');
|
|
18
|
+
const written = writeAttachment(worktree, 'assignment.md', body);
|
|
19
|
+
return { name: written.name, path: written.relPath, bytes: written.bytes, token: written.token };
|
|
20
|
+
}
|
|
3
21
|
/** Ordinary delegation supplies transport context; the saved role and assignment supply the task. */
|
|
4
22
|
export function delegatedPrompt(job) {
|
|
23
|
+
if (job.assignment) {
|
|
24
|
+
return [
|
|
25
|
+
'Read and follow the complete delegated assignment in this UTF-8 file before doing any work:',
|
|
26
|
+
`[Delegated assignment](${encodeURIComponent(job.assignment.path)})`,
|
|
27
|
+
'Read it from the shared worktree, complete the assignment, and put the full result in your final reply.'
|
|
28
|
+
].join('\n\n');
|
|
29
|
+
}
|
|
5
30
|
if (!job.handoff)
|
|
6
31
|
throw new Error('the delegated handoff is missing');
|
|
7
32
|
const parentRead = {
|
|
@@ -26,6 +26,7 @@ export function transitionDelegation(job, status, patch, updatedAt = Date.now())
|
|
|
26
26
|
updatedAt,
|
|
27
27
|
...(patch.childSessionId === undefined ? {} : { childSessionId: patch.childSessionId }),
|
|
28
28
|
...(patch.handoff === undefined ? {} : { handoff: patch.handoff }),
|
|
29
|
+
...(patch.assignment === undefined ? {} : { assignment: patch.assignment }),
|
|
29
30
|
...(patch.sentRowid === undefined ? {} : { sentRowid: patch.sentRowid }),
|
|
30
31
|
...(patch.completionRowid === undefined ? {} : { completionRowid: patch.completionRowid }),
|
|
31
32
|
...(patch.returnCursor === undefined ? {} : { returnCursor: patch.returnCursor }),
|
|
@@ -129,8 +130,28 @@ export class DelegationQueue {
|
|
|
129
130
|
.flatMap(store => store.list().jobs.map(job => ({ store, job })))
|
|
130
131
|
.sort((a, b) => a.job.createdAt - b.job.createdAt || a.job.id.localeCompare(b.job.id));
|
|
131
132
|
for (const { store, job } of entries) {
|
|
132
|
-
if (job.status === 'failed')
|
|
133
|
-
|
|
133
|
+
if (job.status === 'failed') {
|
|
134
|
+
try {
|
|
135
|
+
const recovered = this.deps.reconcile?.(job);
|
|
136
|
+
if (!recovered)
|
|
137
|
+
continue;
|
|
138
|
+
// Only a positive read-only receipt reopens a terminal send failure.
|
|
139
|
+
// The normal delivery stage verifies integrity before observing completion.
|
|
140
|
+
store.put(decodeDelegation({
|
|
141
|
+
...job,
|
|
142
|
+
status: recovered.stage,
|
|
143
|
+
[recovered.stage === 'sending' ? 'sendDelivery' : 'returnDelivery']: recovered.delivery,
|
|
144
|
+
failure: undefined,
|
|
145
|
+
lastAttemptAt: undefined,
|
|
146
|
+
updatedAt: this.now()
|
|
147
|
+
}));
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
this.onError(`delegation reconciliation failed: ${err instanceof Error ? err.message : err}`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
134
155
|
if (job.status === 'returned') {
|
|
135
156
|
store.remove(job.id);
|
|
136
157
|
return true;
|
|
@@ -154,7 +175,7 @@ export class DelegationQueue {
|
|
|
154
175
|
parentSessionId: job.parentSessionId,
|
|
155
176
|
assignedAt: this.now()
|
|
156
177
|
});
|
|
157
|
-
store.put(transitionDelegation(job, 'configuring', { childSessionId: result.childSessionId, handoff: result.handoff }, this.now()));
|
|
178
|
+
store.put(transitionDelegation(job, 'configuring', { childSessionId: result.childSessionId, handoff: result.handoff, assignment: result.assignment }, this.now()));
|
|
158
179
|
return true;
|
|
159
180
|
}
|
|
160
181
|
if (job.status === 'configuring') {
|
|
@@ -254,6 +254,19 @@ export class MessageReads {
|
|
|
254
254
|
const accepted = this.rawOutboxRows(sessionId).find(row => this.outboxText(row.delivery_payload)?.includes(marker) && !before.outboxIds.has(row.message_id));
|
|
255
255
|
return accepted ? { kind: 'outbox', id: accepted.message_id } : durableReceipt();
|
|
256
256
|
}
|
|
257
|
+
/** Compare an identified delivery against raw text without exposing private envelopes. */
|
|
258
|
+
// Recheck the durable row if promotion straddles the two reads.
|
|
259
|
+
deliveryTextMatches(sessionId, messageId, text) {
|
|
260
|
+
const target = text.trim();
|
|
261
|
+
const durable = () => this.deliveryMessageRows('id = ?', [sessionId, messageId])[0];
|
|
262
|
+
const row = durable();
|
|
263
|
+
if (row)
|
|
264
|
+
return row.content?.trim() === target;
|
|
265
|
+
const queued = this.rawOutboxRows(sessionId, messageId)[0];
|
|
266
|
+
if (queued)
|
|
267
|
+
return this.outboxText(queued.delivery_payload)?.trim() === target;
|
|
268
|
+
return durable()?.content?.trim() === target;
|
|
269
|
+
}
|
|
257
270
|
/** Follow one accepted id as Conductor promotes it from outbox to transcript. */
|
|
258
271
|
deliveryReceiptForId(sessionId, messageId) {
|
|
259
272
|
if (!this.hasMessageOutbox()) {
|
|
@@ -75,6 +75,9 @@ export class Reads {
|
|
|
75
75
|
deliveryReceiptForId(sessionId, messageId) {
|
|
76
76
|
return this.messages.deliveryReceiptForId(sessionId, messageId);
|
|
77
77
|
}
|
|
78
|
+
deliveryTextMatches(sessionId, messageId, text) {
|
|
79
|
+
return this.messages.deliveryTextMatches(sessionId, messageId, text);
|
|
80
|
+
}
|
|
78
81
|
getMessages(sessionId, afterRowid = 0) {
|
|
79
82
|
return this.messages.getMessages(sessionId, afterRowid);
|
|
80
83
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.131.
|
|
3
|
+
"version": "1.131.3",
|
|
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.",
|