conductor-remote 1.118.0 → 1.118.2

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-BaS6qXUW.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-fLxTPz6E.js"></script>
29
29
  <link rel="stylesheet" crossorigin href="/assets/index-BRzsSW8g.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 a=e=>i(e,l),t={module:{uri:l},exports:o,require:a};s[l]=Promise.all(n.map(e=>t[e]||a(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:"47b96bd0013bdc2d5054d22af641f7cf"},{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-BaS6qXUW.js",revision:null},{url:"assets/index-BRzsSW8g.css",revision:null},{url:"assets/PierrePatch-D4YbKD8Q.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:"9c3450f87fb10a154104307c604040b5"},{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-fLxTPz6E.js",revision:null},{url:"assets/index-BRzsSW8g.css",revision:null},{url:"assets/PierrePatch-8CEmHqku.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")});
@@ -36,12 +36,12 @@ async function noIndexDiff(cwd, file) {
36
36
  async function untrackedDiff(cwd) {
37
37
  let listing = '';
38
38
  try {
39
- listing = await git(cwd, ['ls-files', '--others', '--exclude-standard']);
39
+ listing = await git(cwd, ['ls-files', '--others', '--exclude-standard', '-z']);
40
40
  }
41
41
  catch {
42
42
  return { files: [], patch: '' };
43
43
  }
44
- const paths = listing.split('\n').filter(Boolean);
44
+ const paths = listing.split('\0').filter(Boolean);
45
45
  const files = [];
46
46
  const patches = [];
47
47
  for (const p of paths) {
@@ -135,6 +135,37 @@ export async function workspaceDiffStats(worktree, base) {
135
135
  const untracked = await untrackedStats(worktree);
136
136
  return { added: tracked.added + untracked.added, removed: tracked.removed + untracked.removed };
137
137
  }
138
+ /**
139
+ * NUL-delimited numstat keeps paths literal. Renames/copies have an empty path in
140
+ * the count record followed by separate old/new path records; the human format's
141
+ * `src/{old.ts => folder/new.ts}` is a label, never a path we can navigate to.
142
+ */
143
+ async function trackedDiffFiles(worktree, against) {
144
+ const numstat = await git(worktree, ['diff', '--numstat', '-z', against]).catch(() => '');
145
+ const records = numstat.split('\0');
146
+ const files = [];
147
+ for (let i = 0; i < records.length; i++) {
148
+ const record = records[i];
149
+ if (!record)
150
+ continue;
151
+ const [added, removed, ...rest] = record.split('\t');
152
+ const file = {
153
+ path: rest.join('\t'),
154
+ added: added === '-' ? 0 : Number(added),
155
+ removed: removed === '-' ? 0 : Number(removed)
156
+ };
157
+ if (!file.path) {
158
+ const oldPath = records[++i];
159
+ const newPath = records[++i];
160
+ if (!oldPath || !newPath)
161
+ break;
162
+ file.oldPath = oldPath;
163
+ file.path = newPath;
164
+ }
165
+ files.push(file);
166
+ }
167
+ return files;
168
+ }
138
169
  /**
139
170
  * Everything the workspace changed relative to its target branch — committed
140
171
  * plus uncommitted — which is what a reviewer wants to see. Computed straight
@@ -142,18 +173,7 @@ export async function workspaceDiffStats(worktree, base) {
142
173
  */
143
174
  export async function workspaceDiff(worktree, base) {
144
175
  const { ref, mergeBase, against } = await diffBasis(worktree, base);
145
- const numstat = await git(worktree, ['diff', '--numstat', against]).catch(() => '');
146
- const files = numstat
147
- .split('\n')
148
- .filter(Boolean)
149
- .map(line => {
150
- const [added, removed, ...rest] = line.split('\t');
151
- return {
152
- path: rest.join('\t'),
153
- added: added === '-' ? 0 : Number(added),
154
- removed: removed === '-' ? 0 : Number(removed)
155
- };
156
- });
176
+ const files = await trackedDiffFiles(worktree, against);
157
177
  const trackedPatch = await git(worktree, ['diff', against]).catch(() => '');
158
178
  const untracked = await untrackedDiff(worktree);
159
179
  files.push(...untracked.files);
@@ -178,9 +198,14 @@ export async function workspaceFileDiff(worktree, base, requestedPath) {
178
198
  const { against } = await diffBasis(worktree, base);
179
199
  // A literal pathspec prevents a filename beginning with `:` from becoming Git pathspec syntax.
180
200
  const literalPathspec = `:(literal)${relative}`;
181
- const tracked = await git(worktree, ['diff', '--no-color', against, '--', literalPathspec]).catch(() => '');
182
- if (tracked)
183
- return { path: relative, patch: tracked };
201
+ const file = (await trackedDiffFiles(worktree, against)).find(file => file.path === relative);
202
+ if (file) {
203
+ // Filtering to only the destination hides the source from rename detection and
204
+ // turns even a pure move into a full-file addition. Keep both sides in the diff.
205
+ const pathspecs = file.oldPath ? [`:(literal)${file.oldPath}`, literalPathspec] : [literalPathspec];
206
+ const tracked = await git(worktree, ['diff', '--no-color', against, '--', ...pathspecs]).catch(() => '');
207
+ return tracked ? { path: relative, patch: tracked } : null;
208
+ }
184
209
  // `git diff <base>` omits untracked files. Only synthesize a patch when Git says this
185
210
  // exact path is untracked and not ignored; never let the request name an arbitrary file.
186
211
  const untracked = await git(worktree, [
@@ -6,7 +6,7 @@ import { renderTranscript, transcriptThrough } from "../../transcript/parser.js"
6
6
  import { lockBlocked, screenLocked } from "../../writes/guards.js";
7
7
  import { UiBusyError, uiBusy, withUiPriority } from "../../writes/ui-lock.js";
8
8
  export function createDelegationsServices(services) {
9
- const { reads, actuator, sessionPoller, delegationStore, openChat, applyAgentPatch, deliverPrompt, deliveredRowSince, SEND_BUDGET_MS, locateChat, confirmDeliveryRow, CONFIRM_WINDOW_MS } = services;
9
+ const { reads, actuator, sessionPoller, delegationStore, openChat, applyAgentPatch, deliverPrompt, deliveredRowSince, SEND_BUDGET_MS, locateChat, confirmDelivery, CONFIRM_WINDOW_MS } = services;
10
10
  function delegationError(code, error, retryable = true) {
11
11
  const clean = withoutWindowEvidence(error);
12
12
  if (clean !== error)
@@ -97,6 +97,52 @@ export function createDelegationsServices(services) {
97
97
  }
98
98
  return { ok: true };
99
99
  }
100
+ /** Prepare durably before sending, then follow the accepted id without spending retries. */
101
+ async function trackedPrompt(ws, sessionId, text, delivery, code, queue = false) {
102
+ if (!delivery) {
103
+ const cursor = reads.deliveryCursor(sessionId);
104
+ return {
105
+ ok: true,
106
+ pending: true,
107
+ delivery: { rowid: cursor.rowid, outboxIds: [...cursor.outboxIds] }
108
+ };
109
+ }
110
+ const cursor = { rowid: delivery.rowid, outboxIds: new Set(delivery.outboxIds) };
111
+ let receipt = delivery.messageId
112
+ ? reads.deliveryReceiptForId(sessionId, delivery.messageId)
113
+ : reads.deliveryReceiptSince(sessionId, text, cursor);
114
+ if (!receipt && delivery.messageId) {
115
+ return delegationError(code, 'the accepted delegated message was removed from Conductor before dispatch', false);
116
+ }
117
+ if (!receipt && delivery.accepted)
118
+ return { ok: true, pending: true, delivery };
119
+ if (!receipt && queue) {
120
+ const located = locateChat(ws, sessionId);
121
+ if ('error' in located)
122
+ return delegationError(code, located.error, false);
123
+ const result = await withUiPriority('background', () => actuator.send({ workspace: ws, sessionId, tab: located.tab }, text, {
124
+ deadline: Date.now() + SEND_BUDGET_MS,
125
+ queue: true
126
+ }));
127
+ receipt = result.ok
128
+ ? reads.deliveryReceiptSince(sessionId, text, cursor)
129
+ : await confirmDelivery(sessionId, text, cursor, Date.now() + CONFIRM_WINDOW_MS);
130
+ if (!receipt) {
131
+ return result.ok
132
+ ? { ok: true, pending: true, delivery: { ...delivery, accepted: true } }
133
+ : delegationError(code, result.error ?? 'Conductor did not accept the queued result');
134
+ }
135
+ }
136
+ if (!receipt) {
137
+ const result = await withUiPriority('background', () => deliverPrompt(ws, sessionId, text, SEND_BUDGET_MS, false, cursor));
138
+ if (!result.ok)
139
+ return delegationError(code, result.error ?? 'the delegated message did not land');
140
+ receipt = result.receipt;
141
+ }
142
+ return receipt.kind === 'message'
143
+ ? { ok: true, rowid: receipt.rowid }
144
+ : { ok: true, pending: true, delivery: { ...delivery, messageId: receipt.id } };
145
+ }
100
146
  async function sendDelegation(job) {
101
147
  const ws = reads.getWorkspace(job.workspaceId);
102
148
  if (!ws)
@@ -110,14 +156,12 @@ export function createDelegationsServices(services) {
110
156
  catch (err) {
111
157
  return delegationError('state_invalid', err instanceof Error ? err.message : String(err), false);
112
158
  }
113
- const cursor = reads.getMessages(job.childSessionId).cursor;
114
- const result = await withUiPriority('background', () => deliverPrompt(ws, job.childSessionId, text));
159
+ const result = await trackedPrompt(ws, job.childSessionId, text, job.sendDelivery, 'send_failed');
115
160
  if (!result.ok)
116
- return delegationError('send_failed', result.error ?? 'the delegated prompt did not land');
117
- const sentRowid = deliveredRowSince(job.childSessionId, text, cursor);
118
- if (sentRowid === null)
119
- return delegationError('send_failed', 'the delegated prompt has no transcript receipt');
120
- return { ok: true, sentRowid };
161
+ return result;
162
+ return 'pending' in result
163
+ ? { ok: true, pending: true, sendDelivery: result.delivery }
164
+ : { ok: true, sentRowid: result.rowid };
121
165
  }
122
166
  function delegationCompletion(job) {
123
167
  if (!job.childSessionId || job.sentRowid === undefined)
@@ -190,7 +234,7 @@ export function createDelegationsServices(services) {
190
234
  if (!reads.getSession(job.parentSessionId)) {
191
235
  return delegationError('session_not_found', 'the parent chat is gone', false);
192
236
  }
193
- if (job.returnCursor !== undefined) {
237
+ if (job.returnCursor !== undefined && !job.returnDelivery) {
194
238
  if (!job.returnAttachment || !job.returnText) {
195
239
  return delegationError('state_invalid', 'the queued return receipt state is incomplete', false);
196
240
  }
@@ -208,45 +252,25 @@ export function createDelegationsServices(services) {
208
252
  let attachment;
209
253
  let text;
210
254
  try {
211
- attachment = delegationReturnAttachment(job, ws);
212
- text = delegationReturnText(job, attachment);
255
+ attachment = job.returnAttachment ?? delegationReturnAttachment(job, ws);
256
+ text = job.returnText ?? delegationReturnText(job, attachment);
213
257
  }
214
258
  catch (err) {
215
259
  return delegationError('return_failed', err instanceof Error ? err.message : String(err), false);
216
260
  }
217
- const cursor = reads.getMessages(job.parentSessionId).cursor;
218
- if (job.returnMode === 'steer') {
219
- const result = await withUiPriority('background', () => deliverPrompt(ws, job.parentSessionId, text, SEND_BUDGET_MS, false));
220
- if (!result.ok)
221
- return delegationError('return_failed', result.error ?? 'the delegated result did not return');
222
- const rowid = deliveredRowSince(job.parentSessionId, text, cursor);
223
- return rowid === null
224
- ? delegationError('return_failed', 'the delegated result has no transcript receipt')
225
- : { ok: true, returnRowid: rowid };
226
- }
227
- const located = locateChat(ws, job.parentSessionId);
228
- if ('error' in located)
229
- return delegationError('return_failed', located.error, false);
230
- const result = await withUiPriority('background', () => actuator.send({ workspace: ws, sessionId: job.parentSessionId, tab: located.tab }, text, {
231
- deadline: Date.now() + SEND_BUDGET_MS,
232
- queue: true
233
- }));
234
- const immediate = deliveredRowSince(job.parentSessionId, text, cursor);
235
- if (immediate !== null)
236
- return { ok: true, returnRowid: immediate };
237
- if (!result.ok) {
238
- const late = await confirmDeliveryRow(job.parentSessionId, text, cursor, Date.now() + CONFIRM_WINDOW_MS);
239
- if (late !== null)
240
- return { ok: true, returnRowid: late };
241
- return delegationError('return_failed', result.error ?? 'Conductor did not accept the queued result');
242
- }
243
- return {
244
- ok: true,
245
- pending: true,
246
- returnCursor: cursor,
247
- returnAttachment: attachment,
248
- returnText: text
249
- };
261
+ const result = await trackedPrompt(ws, job.parentSessionId, text, job.returnDelivery, 'return_failed', job.returnMode === 'queue');
262
+ if (!result.ok)
263
+ return result;
264
+ return 'pending' in result
265
+ ? {
266
+ ok: true,
267
+ pending: true,
268
+ returnCursor: result.delivery.rowid,
269
+ returnDelivery: result.delivery,
270
+ returnAttachment: attachment,
271
+ returnText: text
272
+ }
273
+ : { ok: true, returnRowid: result.rowid };
250
274
  }
251
275
  const delegationQueue = new DelegationQueue({
252
276
  open: openDelegation,
@@ -21,7 +21,7 @@ export function createDeliveryServices(services) {
21
21
  * Conductor's durable outbox also counts, before it becomes a transcript row.
22
22
  */
23
23
  function deliveredSince(sessionId, text, since) {
24
- return reads.promptDeliveredSince(sessionId, text, since);
24
+ return reads.deliveryReceiptSince(sessionId, text, since);
25
25
  }
26
26
  function deliveredRowSince(sessionId, text, sinceRowid) {
27
27
  const target = text.trim();
@@ -29,7 +29,7 @@ export function createDeliveryServices(services) {
29
29
  return entries.find(e => e.role === 'user' && e.text.trim() === target)?.rowid ?? null;
30
30
  }
31
31
  /**
32
- * Watch for that row, ending on a check rather than a sleep, and never past
32
+ * Watch for a receipt, ending on a check rather than a sleep, and never past
33
33
  * `budgetDeadline`. Conductor records the row or outbox item right after the send
34
34
  * presses Enter, so a real send is confirmed in a tick and only the failure path
35
35
  * waits the window out.
@@ -41,24 +41,14 @@ 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 confirmDeliveryRow(sessionId, text, sinceRowid, budgetDeadline) {
45
- const stopAt = Math.min(Date.now() + CONFIRM_WINDOW_MS, budgetDeadline);
46
- for (;;) {
47
- const rowid = deliveredRowSince(sessionId, text, sinceRowid);
48
- if (rowid !== null)
49
- return rowid;
50
- if (Date.now() >= stopAt)
51
- return null;
52
- await sleep(300);
53
- }
54
- }
55
44
  async function confirmDelivery(sessionId, text, since, budgetDeadline) {
56
45
  const stopAt = Math.min(Date.now() + CONFIRM_WINDOW_MS, budgetDeadline);
57
46
  for (;;) {
58
- if (deliveredSince(sessionId, text, since))
59
- return true;
47
+ const receipt = deliveredSince(sessionId, text, since);
48
+ if (receipt)
49
+ return receipt;
60
50
  if (Date.now() >= stopAt)
61
- return false;
51
+ return null;
62
52
  await sleep(300);
63
53
  }
64
54
  }
@@ -176,19 +166,24 @@ export function createDeliveryServices(services) {
176
166
  * minutes schedule sits *outside* this and is unaffected — it retries a delivery that
177
167
  * never got off the ground (worktree still setting up), not one Conductor fumbled.
178
168
  */
179
- async function deliverPrompt(ws, sessionId, text, budgetMs = SEND_BUDGET_MS, queue = false) {
169
+ async function deliverPrompt(ws, sessionId, text, budgetMs = SEND_BUDGET_MS, queue = false, cursor) {
180
170
  const located = locateChat(ws, sessionId);
181
171
  if ('error' in located)
182
172
  return { ok: false, strategy: actuator.name, attempts: 0, error: located.error };
183
173
  // Snapshot the transcript cursor and outbox ids once: every check below asks "did
184
174
  // *this* prompt arrive since we started", so a retry can't be fooled by an older
185
175
  // identical prompt moving from the outbox into a new transcript row.
186
- const before = reads.deliveryCursor(sessionId);
176
+ const before = cursor ?? reads.deliveryCursor(sessionId);
187
177
  const label = ws.branch ?? ws.id;
188
178
  const deadline = Date.now() + budgetMs;
189
179
  let attempts = 0;
190
180
  let last = { ok: false, strategy: actuator.name };
191
181
  for (;;) {
182
+ // A persisted delegation cursor can already have a receipt after a restart
183
+ // or a late acceptance. Recover it before touching the composer again.
184
+ const existing = deliveredSince(sessionId, text, before);
185
+ if (existing)
186
+ return { ok: true, strategy: last.strategy, attempts, receipt: existing };
192
187
  attempts++;
193
188
  // The run gets the deadline, not a duration: `uiTurn` may hold it behind another
194
189
  // write, and only the run knows what was left of the budget when it started. Minus
@@ -208,7 +203,7 @@ export function createDeliveryServices(services) {
208
203
  if (landed) {
209
204
  if (attempts > 1)
210
205
  console.info(`[relay] send to ${label} landed on attempt ${attempts}`);
211
- return { ok: true, strategy: last.strategy, attempts };
206
+ return { ok: true, strategy: last.strategy, attempts, receipt: landed };
212
207
  }
213
208
  if (retryWontHelp(last.error))
214
209
  break;
@@ -419,7 +414,6 @@ export function createDeliveryServices(services) {
419
414
  deliveredRowSince,
420
415
  SEND_BUDGET_MS,
421
416
  locateChat,
422
- confirmDeliveryRow,
423
417
  CONFIRM_WINDOW_MS,
424
418
  firstPrompts,
425
419
  parkedPrompts,
@@ -100,6 +100,19 @@ function decodeResolvedRole(raw) {
100
100
  }).roles.resolved;
101
101
  return { ...role, agentType };
102
102
  }
103
+ function decodeDelivery(raw, field) {
104
+ const value = object(raw);
105
+ if (!value || !Array.isArray(value.outboxIds))
106
+ throw new Error(`${field} is invalid`);
107
+ if (value.accepted !== undefined && value.accepted !== true)
108
+ throw new Error(`${field}.accepted is invalid`);
109
+ return {
110
+ rowid: integer(value.rowid, `${field}.rowid`),
111
+ outboxIds: value.outboxIds.map(id => text(id, `${field}.outboxIds`)),
112
+ ...(value.messageId === undefined ? {} : { messageId: text(value.messageId, `${field}.messageId`) }),
113
+ ...(value.accepted === true ? { accepted: true } : {})
114
+ };
115
+ }
103
116
  function requireStageFields(job) {
104
117
  if (['configuring', 'sending', 'running', 'returning', 'returned'].includes(job.status) && !job.childSessionId) {
105
118
  throw new Error(`${job.status} requires childSessionId`);
@@ -115,6 +128,9 @@ function requireStageFields(job) {
115
128
  if (job.returnCursor !== undefined && (!job.returnAttachment || !job.returnText)) {
116
129
  throw new Error('a dispatched return requires returnAttachment and returnText');
117
130
  }
131
+ if (job.returnDelivery && job.returnCursor !== job.returnDelivery.rowid) {
132
+ throw new Error('returnDelivery requires its original returnCursor');
133
+ }
118
134
  if (job.status === 'returned' && job.returnRowid === undefined)
119
135
  throw new Error('returned requires returnRowid');
120
136
  if (job.status === 'failed' && !job.failure)
@@ -154,6 +170,7 @@ export function decodeDelegation(raw) {
154
170
  createdAt: integer(value.createdAt, 'createdAt'),
155
171
  updatedAt: integer(value.updatedAt, 'updatedAt'),
156
172
  ...(value.handoff === undefined ? {} : { handoff: decodeAttachment(value.handoff) }),
173
+ ...(value.sendDelivery === undefined ? {} : { sendDelivery: decodeDelivery(value.sendDelivery, 'sendDelivery') }),
157
174
  ...(value.sentRowid === undefined ? {} : { sentRowid: integer(value.sentRowid, 'sentRowid', 1) }),
158
175
  ...(value.completionRowid === undefined
159
176
  ? {}
@@ -161,6 +178,9 @@ export function decodeDelegation(raw) {
161
178
  ...(value.returnCursor === undefined ? {} : { returnCursor: integer(value.returnCursor, 'returnCursor') }),
162
179
  ...(value.returnAttachment === undefined ? {} : { returnAttachment: decodeAttachment(value.returnAttachment) }),
163
180
  ...(value.returnText === undefined ? {} : { returnText: text(value.returnText, 'returnText', MAX_TEXT) }),
181
+ ...(value.returnDelivery === undefined
182
+ ? {}
183
+ : { returnDelivery: decodeDelivery(value.returnDelivery, 'returnDelivery') }),
164
184
  ...(value.returnRowid === undefined ? {} : { returnRowid: integer(value.returnRowid, 'returnRowid', 1) }),
165
185
  ...(value.outcome === undefined ? {} : { outcome: decodeOutcome(value.outcome) }),
166
186
  ...(value.failure === undefined ? {} : { failure: decodeError(value.failure, 'failure') }),
@@ -174,6 +174,14 @@ export class DelegationQueue {
174
174
  return true;
175
175
  continue;
176
176
  }
177
+ if ('pending' in result) {
178
+ // Persist the baseline before the UI action and then its accepted id.
179
+ // An unchanged pending receipt only needs the next session-poller tick.
180
+ if (JSON.stringify(job.sendDelivery) === JSON.stringify(result.sendDelivery))
181
+ continue;
182
+ store.put(decodeDelegation({ ...job, sendDelivery: result.sendDelivery, updatedAt: this.now() }));
183
+ return true;
184
+ }
177
185
  store.put(transitionDelegation(job, 'running', { sentRowid: result.sentRowid }, this.now()));
178
186
  return true;
179
187
  }
@@ -208,16 +216,17 @@ export class DelegationQueue {
208
216
  continue;
209
217
  }
210
218
  if ('pending' in result) {
211
- // A queued Baton clears the composer before SQLite exposes any row.
212
- // Persist the pre-dispatch cursor once, then only poll for the exact
213
- // eventual row; repeating the UI action here would enqueue a duplicate.
214
- if (job.returnCursor === result.returnCursor)
219
+ // Save the return text and baseline before sending, then its acceptance.
220
+ // Pending receipts only need a poll; another UI action would duplicate them.
221
+ if (job.returnCursor === result.returnCursor &&
222
+ JSON.stringify(job.returnDelivery) === JSON.stringify(result.returnDelivery))
215
223
  continue;
216
224
  store.put(decodeDelegation({
217
225
  ...job,
218
226
  returnCursor: result.returnCursor,
219
227
  returnAttachment: result.returnAttachment,
220
228
  returnText: result.returnText,
229
+ returnDelivery: result.returnDelivery,
221
230
  updatedAt: this.now()
222
231
  }));
223
232
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.118.0",
3
+ "version": "1.118.2",
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.",