@sjawhar/opencode-legion-envoy 9.8.1 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dispatch.js +323 -29
- package/dist/src/server.js +5 -1
- package/package.json +1 -1
package/dist/dispatch.js
CHANGED
|
@@ -13867,6 +13867,21 @@ var MAX_ISSUE_PAGE_LIMIT = 250;
|
|
|
13867
13867
|
var DEFAULT_ISSUE_PAGE_LIMIT = 50;
|
|
13868
13868
|
var ASK_TURNS = ["human", "agent"];
|
|
13869
13869
|
var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
|
|
13870
|
+
var IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
13871
|
+
var IDEMPOTENCY_REPLAYED_HEADER = "Idempotency-Replayed";
|
|
13872
|
+
var IDEMPOTENCY_KEY_MAX = 128;
|
|
13873
|
+
var IDEMPOTENCY_KEY_PATTERN = new RegExp(`^[A-Za-z0-9._:-]{1,${IDEMPOTENCY_KEY_MAX}}$`);
|
|
13874
|
+
var IDEMPOTENCY_KEY_IN_FLIGHT = "KEY_IN_FLIGHT";
|
|
13875
|
+
var IDEMPOTENCY_KEY_COMMITTED = "KEY_COMMITTED";
|
|
13876
|
+
var IDEMPOTENCY_ANSWER_NOT_KEPT = "ANSWER_NOT_KEPT";
|
|
13877
|
+
function retriesUnderKey(status, code) {
|
|
13878
|
+
return status >= 500 || status === 408 || status === 429 || status === 409 && code === IDEMPOTENCY_KEY_IN_FLIGHT;
|
|
13879
|
+
}
|
|
13880
|
+
function answerNotKept(status, replayed, body) {
|
|
13881
|
+
return replayed && status >= 200 && status < 300 && typeof body === "object" && body !== null && Reflect.get(body, "code") === IDEMPOTENCY_ANSWER_NOT_KEPT;
|
|
13882
|
+
}
|
|
13883
|
+
var IDEMPOTENCY_KEY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
13884
|
+
var IDEMPOTENCY_LEASE_MS = 60 * 1000;
|
|
13870
13885
|
var DELIVERY_DUPLICATE_WINDOW_MS = 72 * 60 * 60 * 1000;
|
|
13871
13886
|
var SEARCH_DEGRADED_EMBEDDER_UNAVAILABLE = "embedder_unavailable";
|
|
13872
13887
|
var IssueDeployedEventPayloadSchema = strictObject({
|
|
@@ -15534,6 +15549,43 @@ class DispatchGatewayError extends DispatchServiceError {
|
|
|
15534
15549
|
this.mayHaveReachedDispatch = reachesDispatch(status);
|
|
15535
15550
|
}
|
|
15536
15551
|
}
|
|
15552
|
+
|
|
15553
|
+
class DispatchWriteUnsettledError extends Error {
|
|
15554
|
+
last;
|
|
15555
|
+
attempts;
|
|
15556
|
+
name = "DispatchWriteUnsettledError";
|
|
15557
|
+
constructor(last, attempts, unanswered) {
|
|
15558
|
+
let answer;
|
|
15559
|
+
let why = "an earlier attempt of the write may have reached Dispatch";
|
|
15560
|
+
if (last instanceof DispatchGatewayError) {
|
|
15561
|
+
answer = last.answer;
|
|
15562
|
+
} else if (last instanceof DispatchServiceError) {
|
|
15563
|
+
answer = `${last.code}: ${last.message}`;
|
|
15564
|
+
if (last.code === IDEMPOTENCY_KEY_IN_FLIGHT) {
|
|
15565
|
+
why = "Dispatch was still running an earlier attempt of the write";
|
|
15566
|
+
}
|
|
15567
|
+
} else {
|
|
15568
|
+
answer = unanswered ?? last.message;
|
|
15569
|
+
why = "an attempt of the write may have reached Dispatch";
|
|
15570
|
+
}
|
|
15571
|
+
const tried = attempts === 1 ? "its only attempt" : `the last of ${attempts} attempts`;
|
|
15572
|
+
super(`${answer} (${tried}), and ${why}, so it may or may not have taken effect: check whether ` + "it did before retrying it.", { cause: last });
|
|
15573
|
+
this.last = last;
|
|
15574
|
+
this.attempts = attempts;
|
|
15575
|
+
}
|
|
15576
|
+
}
|
|
15577
|
+
|
|
15578
|
+
class DispatchWriteLandedError extends Error {
|
|
15579
|
+
code;
|
|
15580
|
+
status;
|
|
15581
|
+
name = "DispatchWriteLandedError";
|
|
15582
|
+
constructor(code, status, method, url) {
|
|
15583
|
+
const lost = code === IDEMPOTENCY_ANSWER_NOT_KEPT ? "Dispatch kept no copy of its answer, which was too large" : "its answer was lost on the way back";
|
|
15584
|
+
super(`${method} ${url} was made, but ${lost} (${code}), so this command cannot print what it ` + "made: read it back with `dispatch read`, or with the matching list for something it created.");
|
|
15585
|
+
this.code = code;
|
|
15586
|
+
this.status = status;
|
|
15587
|
+
}
|
|
15588
|
+
}
|
|
15537
15589
|
function asErrorShape(value) {
|
|
15538
15590
|
return typeof value === "object" && value !== null ? value : {};
|
|
15539
15591
|
}
|
|
@@ -15549,6 +15601,100 @@ function requestSignal(signal) {
|
|
|
15549
15601
|
const deadline = AbortSignal.timeout(DISPATCH_TOOL_DEADLINE_MS);
|
|
15550
15602
|
return signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
|
|
15551
15603
|
}
|
|
15604
|
+
var DISPATCH_WRITE_RETRY = {
|
|
15605
|
+
pausesMs: [1000, 2000, 4000, 8000],
|
|
15606
|
+
withinMs: 30000,
|
|
15607
|
+
silenceMs: 20000
|
|
15608
|
+
};
|
|
15609
|
+
function pauseFor(ms, signal) {
|
|
15610
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
15611
|
+
if (signal.aborted) {
|
|
15612
|
+
reject(signal.reason);
|
|
15613
|
+
return promise;
|
|
15614
|
+
}
|
|
15615
|
+
const stop = () => {
|
|
15616
|
+
clearTimeout(timer);
|
|
15617
|
+
reject(signal.reason);
|
|
15618
|
+
};
|
|
15619
|
+
const timer = setTimeout(() => {
|
|
15620
|
+
signal.removeEventListener("abort", stop);
|
|
15621
|
+
resolve();
|
|
15622
|
+
}, ms);
|
|
15623
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
15624
|
+
return promise;
|
|
15625
|
+
}
|
|
15626
|
+
function retryAfterMs(response) {
|
|
15627
|
+
const value = response.headers.get("Retry-After")?.trim() ?? "";
|
|
15628
|
+
if (/^\d+$/.test(value))
|
|
15629
|
+
return Number(value) * 1000;
|
|
15630
|
+
const at = Date.parse(value);
|
|
15631
|
+
return Number.isNaN(at) ? 0 : Math.max(0, at - Date.now());
|
|
15632
|
+
}
|
|
15633
|
+
var KEYED_LAYER_MEMORY_MS = 5 * 60000;
|
|
15634
|
+
var KEYED_LAYER_SEEN = new Map;
|
|
15635
|
+
var KEYED_LAYER_PROBES = new Map;
|
|
15636
|
+
var KEY_LAYER_PROBE_MS = 5000;
|
|
15637
|
+
function keyEchoed(response, key) {
|
|
15638
|
+
const echoed = response.headers.get(IDEMPOTENCY_KEY_HEADER)?.trim() ?? "";
|
|
15639
|
+
return echoed.replace(/^"(.*)"$/, "$1") === key;
|
|
15640
|
+
}
|
|
15641
|
+
var STREAMED_BODY_MIN = 1 << 20;
|
|
15642
|
+
var STREAMED_BODY_CHUNK = 64 << 10;
|
|
15643
|
+
function attemptBody(body, headers, progress) {
|
|
15644
|
+
const bytes = typeof body.content === "string" ? new TextEncoder().encode(body.content) : body.content;
|
|
15645
|
+
if (bytes.byteLength < STREAMED_BODY_MIN)
|
|
15646
|
+
return { body: body.content, headers };
|
|
15647
|
+
let sent = 0;
|
|
15648
|
+
const stream = new ReadableStream({
|
|
15649
|
+
pull(controller) {
|
|
15650
|
+
progress();
|
|
15651
|
+
if (sent >= bytes.byteLength) {
|
|
15652
|
+
controller.close();
|
|
15653
|
+
return;
|
|
15654
|
+
}
|
|
15655
|
+
const end = Math.min(sent + STREAMED_BODY_CHUNK, bytes.byteLength);
|
|
15656
|
+
controller.enqueue(bytes.subarray(sent, end));
|
|
15657
|
+
sent = end;
|
|
15658
|
+
}
|
|
15659
|
+
});
|
|
15660
|
+
return { body: stream, headers: { ...headers, "Content-Length": String(bytes.byteLength) } };
|
|
15661
|
+
}
|
|
15662
|
+
|
|
15663
|
+
class WriteAttemptSilent extends Error {
|
|
15664
|
+
name = "WriteAttemptSilent";
|
|
15665
|
+
constructor(silenceMs) {
|
|
15666
|
+
super(`made no progress for ${silenceMs / 1000} s, so it was cut off`);
|
|
15667
|
+
}
|
|
15668
|
+
}
|
|
15669
|
+
var NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
|
|
15670
|
+
async function writeAnswer(response, key) {
|
|
15671
|
+
const echoed = keyEchoed(response, key);
|
|
15672
|
+
if (response.ok || NULL_BODY_STATUSES.has(response.status)) {
|
|
15673
|
+
return { response, again: false, reached: false, echoed, unjudged: false };
|
|
15674
|
+
}
|
|
15675
|
+
const { status } = response;
|
|
15676
|
+
const text = await response.text();
|
|
15677
|
+
let shape = {};
|
|
15678
|
+
if (isJson(response)) {
|
|
15679
|
+
try {
|
|
15680
|
+
shape = asErrorShape(JSON.parse(text));
|
|
15681
|
+
} catch {
|
|
15682
|
+
shape = {};
|
|
15683
|
+
}
|
|
15684
|
+
}
|
|
15685
|
+
const again = retriesUnderKey(status, shape.code);
|
|
15686
|
+
return {
|
|
15687
|
+
response: new Response(text, {
|
|
15688
|
+
status,
|
|
15689
|
+
statusText: response.statusText,
|
|
15690
|
+
headers: response.headers
|
|
15691
|
+
}),
|
|
15692
|
+
again,
|
|
15693
|
+
reached: status >= 500 || again && status === 409,
|
|
15694
|
+
echoed,
|
|
15695
|
+
unjudged: !echoed && typeof shape.error === "string"
|
|
15696
|
+
};
|
|
15697
|
+
}
|
|
15552
15698
|
function whyNotAPage(answer) {
|
|
15553
15699
|
if (typeof answer === "string") {
|
|
15554
15700
|
return `text that is not a JSON page, ${GATEWAY_PAGE}, so a retry may succeed.`;
|
|
@@ -15633,12 +15779,14 @@ function excerpt(text, token) {
|
|
|
15633
15779
|
class DispatchClient {
|
|
15634
15780
|
token;
|
|
15635
15781
|
fetchImpl;
|
|
15782
|
+
writeRetry;
|
|
15636
15783
|
#baseUrl;
|
|
15637
15784
|
#resolvedIssues = new Map;
|
|
15638
15785
|
#signal;
|
|
15639
|
-
constructor(baseUrl, token, fetchImpl = fetch, signal) {
|
|
15786
|
+
constructor(baseUrl, token, fetchImpl = fetch, signal, writeRetry = DISPATCH_WRITE_RETRY) {
|
|
15640
15787
|
this.token = token;
|
|
15641
15788
|
this.fetchImpl = fetchImpl;
|
|
15789
|
+
this.writeRetry = writeRetry;
|
|
15642
15790
|
this.#baseUrl = baseUrl.replace(/\/+$/, "");
|
|
15643
15791
|
this.#signal = requestSignal(signal);
|
|
15644
15792
|
}
|
|
@@ -15864,6 +16012,10 @@ class DispatchClient {
|
|
|
15864
16012
|
async messageReply(id, input, options = {}) {
|
|
15865
16013
|
return this.#json("POST", ["api", "v1", "messages", id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
|
|
15866
16014
|
}
|
|
16015
|
+
async deliveryReply(resource, id, input) {
|
|
16016
|
+
const collection = resource === "message" ? "messages" : "comments";
|
|
16017
|
+
await this.#json("POST", ["api", "v1", collection, id, "reply"], input);
|
|
16018
|
+
}
|
|
15867
16019
|
async getMessage(issue, id) {
|
|
15868
16020
|
return this.#json("GET", [
|
|
15869
16021
|
"api",
|
|
@@ -15891,11 +16043,7 @@ class DispatchClient {
|
|
|
15891
16043
|
}
|
|
15892
16044
|
async fileVersion(id, version) {
|
|
15893
16045
|
const url = this.#url(["api", "v1", "artifacts", id, "versions", String(version)]);
|
|
15894
|
-
const response = await this
|
|
15895
|
-
method: "GET",
|
|
15896
|
-
headers: { Accept: "*/*", Authorization: `Bearer ${this.token}` },
|
|
15897
|
-
signal: this.#signal
|
|
15898
|
-
});
|
|
16046
|
+
const response = await this.#send("GET", url, undefined, "*/*");
|
|
15899
16047
|
if (!response.ok)
|
|
15900
16048
|
return this.#response("GET", url, response);
|
|
15901
16049
|
return {
|
|
@@ -15973,19 +16121,8 @@ class DispatchClient {
|
|
|
15973
16121
|
return (await this.#jsonAnswer(method, path, body, query)).payload;
|
|
15974
16122
|
}
|
|
15975
16123
|
async#jsonAnswer(method, path, body, query) {
|
|
15976
|
-
const headers = {
|
|
15977
|
-
Accept: "application/json",
|
|
15978
|
-
Authorization: `Bearer ${this.token}`
|
|
15979
|
-
};
|
|
15980
|
-
if (body !== undefined)
|
|
15981
|
-
headers["Content-Type"] = "application/json";
|
|
15982
16124
|
const url = this.#url(path, query);
|
|
15983
|
-
const response = await this
|
|
15984
|
-
method,
|
|
15985
|
-
headers,
|
|
15986
|
-
signal: this.#signal,
|
|
15987
|
-
...body === undefined ? {} : { body: JSON.stringify(body) }
|
|
15988
|
-
});
|
|
16125
|
+
const response = await this.#send(method, url, body === undefined ? undefined : { content: JSON.stringify(body), type: "application/json" });
|
|
15989
16126
|
return { status: response.status, payload: await this.#response(method, url, response) };
|
|
15990
16127
|
}
|
|
15991
16128
|
async#upload(path, input) {
|
|
@@ -16000,16 +16137,152 @@ class DispatchClient {
|
|
|
16000
16137
|
form.set("file", input.file, input.name);
|
|
16001
16138
|
return this.#form("POST", path, form);
|
|
16002
16139
|
}
|
|
16003
|
-
async#form(method, path,
|
|
16140
|
+
async#form(method, path, form) {
|
|
16004
16141
|
const url = this.#url(path);
|
|
16005
|
-
const
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
signal: this.#signal
|
|
16010
|
-
});
|
|
16142
|
+
const encoded = new Response(form);
|
|
16143
|
+
const type = encoded.headers.get("Content-Type") ?? "multipart/form-data";
|
|
16144
|
+
const content = new Uint8Array(await encoded.arrayBuffer());
|
|
16145
|
+
const response = await this.#send(method, url, { content, type });
|
|
16011
16146
|
return this.#response(method, url, response);
|
|
16012
16147
|
}
|
|
16148
|
+
async#send(method, url, body, accept = "application/json") {
|
|
16149
|
+
const headers = {
|
|
16150
|
+
Accept: accept,
|
|
16151
|
+
Authorization: `Bearer ${this.token}`
|
|
16152
|
+
};
|
|
16153
|
+
if (body !== undefined)
|
|
16154
|
+
headers["Content-Type"] = body.type;
|
|
16155
|
+
if (method !== "POST" && method !== "PATCH") {
|
|
16156
|
+
return this.fetchImpl(url, {
|
|
16157
|
+
method,
|
|
16158
|
+
headers,
|
|
16159
|
+
signal: this.#signal,
|
|
16160
|
+
...body === undefined ? {} : { body: body.content }
|
|
16161
|
+
});
|
|
16162
|
+
}
|
|
16163
|
+
const key = crypto.randomUUID();
|
|
16164
|
+
headers[IDEMPOTENCY_KEY_HEADER] = key;
|
|
16165
|
+
const { pausesMs, withinMs, silenceMs, pause = pauseFor } = this.writeRetry;
|
|
16166
|
+
const request = `${method} ${url}`;
|
|
16167
|
+
const started = this.#now();
|
|
16168
|
+
let reached = false;
|
|
16169
|
+
for (let attempt = 1;; attempt++) {
|
|
16170
|
+
let answer;
|
|
16171
|
+
let thrown;
|
|
16172
|
+
try {
|
|
16173
|
+
answer = await writeAnswer(await this.#attempt(url, method, headers, body, silenceMs), key);
|
|
16174
|
+
if (answer.echoed)
|
|
16175
|
+
KEYED_LAYER_SEEN.set(this.#baseUrl, this.#now());
|
|
16176
|
+
if (!answer.again)
|
|
16177
|
+
return answer.response;
|
|
16178
|
+
reached ||= answer.reached;
|
|
16179
|
+
} catch (error) {
|
|
16180
|
+
const name = error instanceof Error || error instanceof DOMException ? error.name : "";
|
|
16181
|
+
if (!(error instanceof WriteAttemptSilent) && (this.#signal.aborted || name === "AbortError" || name === "TimeoutError")) {
|
|
16182
|
+
throw this.#ended(error, request, attempt, true);
|
|
16183
|
+
}
|
|
16184
|
+
thrown = error;
|
|
16185
|
+
reached = true;
|
|
16186
|
+
}
|
|
16187
|
+
const scheduled = pausesMs[attempt - 1];
|
|
16188
|
+
const wait = Math.max(scheduled ?? 0, answer === undefined ? 0 : retryAfterMs(answer.response));
|
|
16189
|
+
const inTime = scheduled !== undefined && this.#now() - started + wait <= withinMs;
|
|
16190
|
+
if (!inTime || !(answer?.echoed || await this.#runsKeyedLayer(answer?.unjudged ?? false))) {
|
|
16191
|
+
if (answer !== undefined) {
|
|
16192
|
+
return this.#unsettled(method, url, answer.response, reached, attempt);
|
|
16193
|
+
}
|
|
16194
|
+
if (thrown instanceof WriteAttemptSilent) {
|
|
16195
|
+
throw new DispatchWriteUnsettledError(thrown, attempt, `${request} ${thrown.message}`);
|
|
16196
|
+
}
|
|
16197
|
+
throw thrown;
|
|
16198
|
+
}
|
|
16199
|
+
await answer?.response.body?.cancel().catch(() => {
|
|
16200
|
+
return;
|
|
16201
|
+
});
|
|
16202
|
+
try {
|
|
16203
|
+
await pause(wait, this.#signal);
|
|
16204
|
+
} catch (error) {
|
|
16205
|
+
throw this.#ended(error, request, attempt, reached);
|
|
16206
|
+
}
|
|
16207
|
+
}
|
|
16208
|
+
}
|
|
16209
|
+
#now() {
|
|
16210
|
+
return this.writeRetry.now?.() ?? performance.now();
|
|
16211
|
+
}
|
|
16212
|
+
async#attempt(url, method, headers, body, silenceMs) {
|
|
16213
|
+
const cut = new AbortController;
|
|
16214
|
+
const silent = () => cut.abort(new WriteAttemptSilent(silenceMs));
|
|
16215
|
+
let timer = setTimeout(silent, silenceMs);
|
|
16216
|
+
let answered = false;
|
|
16217
|
+
const progress = () => {
|
|
16218
|
+
if (answered)
|
|
16219
|
+
return;
|
|
16220
|
+
clearTimeout(timer);
|
|
16221
|
+
timer = setTimeout(silent, silenceMs);
|
|
16222
|
+
};
|
|
16223
|
+
try {
|
|
16224
|
+
return await this.fetchImpl(url, {
|
|
16225
|
+
method,
|
|
16226
|
+
signal: AbortSignal.any([this.#signal, cut.signal]),
|
|
16227
|
+
...body === undefined ? { headers } : attemptBody(body, headers, progress)
|
|
16228
|
+
});
|
|
16229
|
+
} catch (error) {
|
|
16230
|
+
if (cut.signal.aborted && !this.#signal.aborted)
|
|
16231
|
+
throw cut.signal.reason;
|
|
16232
|
+
throw error;
|
|
16233
|
+
} finally {
|
|
16234
|
+
answered = true;
|
|
16235
|
+
clearTimeout(timer);
|
|
16236
|
+
}
|
|
16237
|
+
}
|
|
16238
|
+
#ended(error, request, attempts, mayHaveReached) {
|
|
16239
|
+
const reason = this.#signal.aborted ? this.#signal.reason : error;
|
|
16240
|
+
const timedOut = (reason instanceof Error || reason instanceof DOMException) && reason.name === "TimeoutError";
|
|
16241
|
+
if (!timedOut || !mayHaveReached || !(error instanceof Error))
|
|
16242
|
+
return error;
|
|
16243
|
+
return new DispatchWriteUnsettledError(error, attempts, `${request} had no definitive answer when its deadline ended it`);
|
|
16244
|
+
}
|
|
16245
|
+
async#runsKeyedLayer(unjudged) {
|
|
16246
|
+
const origin = this.#baseUrl;
|
|
16247
|
+
if (unjudged)
|
|
16248
|
+
KEYED_LAYER_SEEN.delete(origin);
|
|
16249
|
+
const seen = KEYED_LAYER_SEEN.get(origin);
|
|
16250
|
+
if (seen !== undefined && this.#now() - seen < KEYED_LAYER_MEMORY_MS)
|
|
16251
|
+
return true;
|
|
16252
|
+
let probe = KEYED_LAYER_PROBES.get(origin);
|
|
16253
|
+
if (probe === undefined) {
|
|
16254
|
+
probe = this.#probeKeyedLayer().finally(() => KEYED_LAYER_PROBES.delete(origin));
|
|
16255
|
+
KEYED_LAYER_PROBES.set(origin, probe);
|
|
16256
|
+
}
|
|
16257
|
+
const runs = await probe;
|
|
16258
|
+
if (runs)
|
|
16259
|
+
KEYED_LAYER_SEEN.set(origin, this.#now());
|
|
16260
|
+
return runs;
|
|
16261
|
+
}
|
|
16262
|
+
async#probeKeyedLayer() {
|
|
16263
|
+
try {
|
|
16264
|
+
const response = await this.fetchImpl(`${this.#baseUrl}/healthz`, {
|
|
16265
|
+
method: "GET",
|
|
16266
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${this.token}` },
|
|
16267
|
+
signal: AbortSignal.any([this.#signal, AbortSignal.timeout(KEY_LAYER_PROBE_MS)])
|
|
16268
|
+
});
|
|
16269
|
+
if (!isJson(response))
|
|
16270
|
+
return false;
|
|
16271
|
+
const health = await response.json();
|
|
16272
|
+
return typeof health === "object" && health !== null && Reflect.get(health, "idempotency_keys") === true;
|
|
16273
|
+
} catch {
|
|
16274
|
+
return false;
|
|
16275
|
+
}
|
|
16276
|
+
}
|
|
16277
|
+
async#unsettled(method, url, response, reached, attempts) {
|
|
16278
|
+
const refusal = await this.#response(method, url, response).catch((error) => error);
|
|
16279
|
+
if (reached && refusal instanceof DispatchServiceError) {
|
|
16280
|
+
const landedAlone = refusal instanceof DispatchGatewayError ? refusal.mayHaveReachedDispatch : refusal.status >= 500;
|
|
16281
|
+
if (!landedAlone)
|
|
16282
|
+
throw new DispatchWriteUnsettledError(refusal, attempts);
|
|
16283
|
+
}
|
|
16284
|
+
throw refusal;
|
|
16285
|
+
}
|
|
16013
16286
|
#url(path, query) {
|
|
16014
16287
|
const url = new URL(`${path.map((segment) => encodeURIComponent(segment)).join("/")}`, `${this.#baseUrl}/`);
|
|
16015
16288
|
if (query) {
|
|
@@ -16037,6 +16310,9 @@ class DispatchClient {
|
|
|
16037
16310
|
if (!response.ok) {
|
|
16038
16311
|
const error = asErrorShape(payload);
|
|
16039
16312
|
if (typeof error.error === "string") {
|
|
16313
|
+
if (response.status === 409 && error.code === IDEMPOTENCY_KEY_COMMITTED) {
|
|
16314
|
+
throw new DispatchWriteLandedError(IDEMPOTENCY_KEY_COMMITTED, response.status, method, url);
|
|
16315
|
+
}
|
|
16040
16316
|
throw new DispatchServiceError(error.code ?? `HTTP_${response.status}`, response.status, error.error, error.candidates, error.current, error.mismatches);
|
|
16041
16317
|
}
|
|
16042
16318
|
const quoted = excerpt(text, this.token);
|
|
@@ -16054,6 +16330,10 @@ class DispatchClient {
|
|
|
16054
16330
|
const reason = reasonPhrase === "" ? "" : ` ${reasonPhrase}`;
|
|
16055
16331
|
throw new DispatchGatewayError(response.status, `${method} ${url} answered ${response.status}${reason} with ${body}, ${GATEWAY_PAGE}`, gatewayAdvice(method, response.status));
|
|
16056
16332
|
}
|
|
16333
|
+
const replayed = response.headers.get(IDEMPOTENCY_REPLAYED_HEADER)?.trim() === "?1";
|
|
16334
|
+
if (answerNotKept(response.status, replayed, payload)) {
|
|
16335
|
+
throw new DispatchWriteLandedError(IDEMPOTENCY_ANSWER_NOT_KEPT, response.status, method, url);
|
|
16336
|
+
}
|
|
16057
16337
|
return payload;
|
|
16058
16338
|
}
|
|
16059
16339
|
}
|
|
@@ -18349,7 +18629,7 @@ async function executeDispatchTool(input) {
|
|
|
18349
18629
|
throw new ToolInputError(input.tool, checked.problems);
|
|
18350
18630
|
const ownerArguments = checked.owner;
|
|
18351
18631
|
const pictures = checked.pictures;
|
|
18352
|
-
const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
18632
|
+
const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal, input.writeRetry);
|
|
18353
18633
|
if (input.tool === "dispatch_open_asks") {
|
|
18354
18634
|
const client = dispatchClient();
|
|
18355
18635
|
const project = optionalString(ownerArguments.args, "project");
|
|
@@ -18405,7 +18685,11 @@ async function executeDispatchTool(input) {
|
|
|
18405
18685
|
throw new Error("issue or project is required");
|
|
18406
18686
|
return owner;
|
|
18407
18687
|
};
|
|
18408
|
-
const postWithPictures = (text, limit, owner, upload) => textWithPictures(input.tool, text, pictures, limit, owner, upload
|
|
18688
|
+
const postWithPictures = (text, limit, owner, upload) => textWithPictures(input.tool, text, pictures, limit, owner, (picture) => upload(picture).catch((error) => {
|
|
18689
|
+
if (!(error instanceof DispatchWriteLandedError))
|
|
18690
|
+
throw error;
|
|
18691
|
+
throw new Error(`${error.message} That was a picture uploaded before this call's own write, so nothing else was written: run the call again, which uploads it as its next version.`, { cause: error });
|
|
18692
|
+
}), actor);
|
|
18409
18693
|
const writePictures = (resolved, text, limit) => {
|
|
18410
18694
|
if (resolved?.owner.kind === "project") {
|
|
18411
18695
|
const { project } = resolved.owner;
|
|
@@ -18517,6 +18801,9 @@ async function executeDispatchTool(input) {
|
|
|
18517
18801
|
ref: dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id)
|
|
18518
18802
|
};
|
|
18519
18803
|
} catch (error) {
|
|
18804
|
+
if (error instanceof DispatchWriteLandedError) {
|
|
18805
|
+
throw new Error(`${error.message} That was the closing reason, so the close was not sent: read the issue's messages for it, then close with a reason that points at it, since retrying this call posts its reason again.`, { cause: error });
|
|
18806
|
+
}
|
|
18520
18807
|
const told = writeMayHaveLanded(error) ? "the reason may or may not have been posted, and the close was not sent: read the issue's messages before retrying, since retrying this call posts its reason again" : "the reason was not posted, so the close was not sent";
|
|
18521
18808
|
if (error instanceof DispatchServiceError)
|
|
18522
18809
|
throw refusalWithCode(error, told);
|
|
@@ -18547,6 +18834,8 @@ async function executeDispatchTool(input) {
|
|
|
18547
18834
|
actor
|
|
18548
18835
|
});
|
|
18549
18836
|
} catch (error) {
|
|
18837
|
+
if (error instanceof DispatchWriteLandedError)
|
|
18838
|
+
throw error;
|
|
18550
18839
|
const taken = dispatchAnswered(error, 500) && newLinks.length > 0 ? `one of ${newLinks.join(", ")} may already be linked from another issue (a URL links exactly one issue)` : "";
|
|
18551
18840
|
if (closingNote === undefined)
|
|
18552
18841
|
throw refusalWithCode(error, taken);
|
|
@@ -20716,8 +21005,13 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
20716
21005
|
code = OK;
|
|
20717
21006
|
} catch (error) {
|
|
20718
21007
|
text = messageFor(error);
|
|
20719
|
-
|
|
20720
|
-
|
|
21008
|
+
if (error instanceof DispatchWriteLandedError) {
|
|
21009
|
+
entry = { tool, details: { landed_without_answer: error.code } };
|
|
21010
|
+
code = OK;
|
|
21011
|
+
} else {
|
|
21012
|
+
entry = { tool, error: text };
|
|
21013
|
+
code = REFUSED;
|
|
21014
|
+
}
|
|
20721
21015
|
}
|
|
20722
21016
|
try {
|
|
20723
21017
|
recordOutcome(dir, sessionId, follows, entry);
|
package/dist/src/server.js
CHANGED
|
@@ -13568,6 +13568,10 @@ var MAX_ISSUE_PAGE_LIMIT = 250;
|
|
|
13568
13568
|
var DEFAULT_ISSUE_PAGE_LIMIT = 50;
|
|
13569
13569
|
var ASK_TURNS = ["human", "agent"];
|
|
13570
13570
|
var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
|
|
13571
|
+
var IDEMPOTENCY_KEY_MAX = 128;
|
|
13572
|
+
var IDEMPOTENCY_KEY_PATTERN = new RegExp(`^[A-Za-z0-9._:-]{1,${IDEMPOTENCY_KEY_MAX}}$`);
|
|
13573
|
+
var IDEMPOTENCY_KEY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
13574
|
+
var IDEMPOTENCY_LEASE_MS = 60 * 1000;
|
|
13571
13575
|
var DELIVERY_DUPLICATE_WINDOW_MS = 72 * 60 * 60 * 1000;
|
|
13572
13576
|
var IssueDeployedEventPayloadSchema = strictObject({
|
|
13573
13577
|
repo: string2(),
|
|
@@ -15586,7 +15590,7 @@ function sourceSession(sessionID) {
|
|
|
15586
15590
|
// src/server.ts
|
|
15587
15591
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
15588
15592
|
// package.json
|
|
15589
|
-
var version2 = "
|
|
15593
|
+
var version2 = "10.0.0";
|
|
15590
15594
|
|
|
15591
15595
|
// src/log.ts
|
|
15592
15596
|
import { appendFile, mkdir } from "fs/promises";
|