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/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-x6nsv58D.js"></script>
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:"cf434f420a15be6e303151087025c1cf"},{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-x6nsv58D.js",revision:null},{url:"assets/index-Dbu5DOed.css",revision:null},{url:"assets/PierrePatch-BZvD6Gh8.js",revision:null},{url:"assets/AutoModelSettings-bXS1_9Es.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
+ 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
- if (!opened.sessionId)
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
- let receipt = delivery.messageId
113
- ? reads.deliveryReceiptForId(sessionId, delivery.messageId)
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
- ? reads.deliveryReceiptSince(sessionId, text, cursor)
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 = deliveredSince(sessionId, text, since);
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
- * ⌘T is fire-and-forget like every other keystroke here, so the id is not something
112
- * the write can return the DB is the receipt. Which row is the new one is decided by
113
- * diffing the tab list against the one taken *before* the keystroke, not by taking the
114
- * newest: a sibling tab or another agent may have opened one in between, and picking by
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
- const before = new Set(reads.listSessions(ws.id).map(s => s.id));
119
- const result = await newChat(ws);
120
- if (!result.ok)
121
- return { error: true, result };
122
- // The new session lands in the DB a beat after Cmd+T — poll for the fresh id.
123
- for (let i = 0; i < 12; i++) {
124
- await sleep(500);
125
- const fresh = reads.listSessions(ws.id).filter(s => !before.has(s.id));
126
- if (fresh.length > 1) {
127
- return {
128
- error: true,
129
- retryable: false,
130
- result: {
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: actuator.name,
133
- error: 'more than one new chat appeared; refusing to guess which one this request opened'
146
+ strategy: result.strategy,
147
+ error: 'Conductor did not confirm a new chat. Check the workspace before trying again.'
134
148
  }
135
- };
136
- }
137
- if (fresh[0])
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 = deliveredSince(sessionId, text, before);
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
- ? deliveredSince(sessionId, text, before)
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
- ? `Send didn’t land in the chat Conductor may have been asleep or unfocused${tried}. Try again.`
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
- continue;
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.1",
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.",