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/assets/{PierrePatch-D4YbKD8Q.js → PierrePatch-8CEmHqku.js} +1 -1
- package/dist/assets/{index-BaS6qXUW.js → index-fLxTPz6E.js} +4 -4
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/dist-node/src/git/diff.js +42 -17
- package/dist-node/src/http/services/delegations.js +68 -44
- package/dist-node/src/http/services/delivery.js +14 -20
- package/dist-node/src/orchestration/delegation/codec.js +20 -0
- package/dist-node/src/orchestration/delegation/queue.js +13 -4
- 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-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
|
|
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('\
|
|
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
|
|
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
|
|
182
|
-
if (
|
|
183
|
-
|
|
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,
|
|
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
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
47
|
+
const receipt = deliveredSince(sessionId, text, since);
|
|
48
|
+
if (receipt)
|
|
49
|
+
return receipt;
|
|
60
50
|
if (Date.now() >= stopAt)
|
|
61
|
-
return
|
|
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
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
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.",
|