@coreplane/switchboard 1.262.4 → 1.264.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/assets/config/config.example.yaml +4 -2
- package/dist/assets/deploy/cloudflare/prImages.ts +118 -0
- package/dist/assets/deploy/cloudflare/worker.ts +7 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +1 -1
- package/dist/assets/src/artifacts/prImages.ts +51 -0
- package/dist/assets/src/core/coordinator/driver.ts +1 -0
- package/dist/assets/src/core/provider.ts +1 -1
- package/dist/assets/src/core/ship/coordinator.ts +11 -0
- package/dist/cli.js +200 -42
- package/package.json +1 -1
|
@@ -435,8 +435,10 @@ workspaceDir: ./workspaces
|
|
|
435
435
|
# integer >= 14 (the loop's reserve plus one round). `maxMinutes` is the ship
|
|
436
436
|
# preset's declared budget: a channel or user `boundary.maxMinutes` and a
|
|
437
437
|
# per-message `budget:` directive clip it like any preset's, and the card says
|
|
438
|
-
# so.
|
|
439
|
-
#
|
|
438
|
+
# so. An unfinished pipeline keeps only its remaining rounds and minutes on
|
|
439
|
+
# continuation. A new task after merge-ready starts with fresh caps, even when
|
|
440
|
+
# it adopts that pull request. Defaults shown; `maxRounds` must be an integer
|
|
441
|
+
# >= 1; any other key under `ship` fails the load by name.
|
|
440
442
|
# ship:
|
|
441
443
|
# maxRounds: 3 # review rounds per unit
|
|
442
444
|
# maxMinutes: 240 # the ship preset's wall-clock budget in minutes (the registry's default)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { MAX_PR_IMAGE_BYTES, prImageKey, readPrImage, validPrImageSource } from "../../src/artifacts/prImages.ts";
|
|
2
|
+
import { DAY_MS } from "../../src/core/budgets.ts";
|
|
3
|
+
import { systemClock } from "../../src/core/trace/clock.ts";
|
|
4
|
+
import { sameToken } from "./artifactsCopy.ts";
|
|
5
|
+
|
|
6
|
+
/** The small part of R2 needed for deliberate publication. No list or public
|
|
7
|
+
* bucket capability: reads can address only copies this handler marked. */
|
|
8
|
+
export interface PrImageBucket {
|
|
9
|
+
get(key: string): Promise<{
|
|
10
|
+
size: number;
|
|
11
|
+
body: ReadableStream<Uint8Array>;
|
|
12
|
+
httpMetadata?: { contentType?: string };
|
|
13
|
+
customMetadata?: Record<string, string>;
|
|
14
|
+
} | null>;
|
|
15
|
+
put(
|
|
16
|
+
key: string,
|
|
17
|
+
bytes: Uint8Array,
|
|
18
|
+
options: {
|
|
19
|
+
httpMetadata: { contentType: string };
|
|
20
|
+
customMetadata: Record<string, string>;
|
|
21
|
+
},
|
|
22
|
+
): Promise<unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const json = (status: number, body: Record<string, unknown>) =>
|
|
26
|
+
new Response(JSON.stringify(body), {
|
|
27
|
+
status,
|
|
28
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export async function publishPrImage(
|
|
32
|
+
request: Request,
|
|
33
|
+
deps: { bucket?: PrImageBucket; token?: string; clock?: () => number },
|
|
34
|
+
): Promise<Response> {
|
|
35
|
+
const bearer = /^Bearer\s+(\S+)$/.exec(request.headers.get("authorization") ?? "")?.[1];
|
|
36
|
+
if (!deps.token || !bearer || !sameToken(bearer, deps.token))
|
|
37
|
+
return json(401, { error: "publication bearer refused" });
|
|
38
|
+
if (request.method !== "POST") return json(405, { error: "publication requires POST" });
|
|
39
|
+
if (!deps.bucket) return json(503, { error: "artifact store unavailable" });
|
|
40
|
+
let key: unknown;
|
|
41
|
+
let retentionDays: unknown;
|
|
42
|
+
try {
|
|
43
|
+
const input = (await request.json()) as { key?: unknown; retentionDays?: unknown } | null;
|
|
44
|
+
key = input?.key;
|
|
45
|
+
retentionDays = input?.retentionDays;
|
|
46
|
+
} catch {
|
|
47
|
+
/* Refuse below, before reading storage. */
|
|
48
|
+
}
|
|
49
|
+
if (typeof key !== "string" || !validPrImageSource(key))
|
|
50
|
+
return json(400, { error: "publication requires an outbound artifact key" });
|
|
51
|
+
if (typeof retentionDays !== "number" || !Number.isSafeInteger(retentionDays) || retentionDays < 1)
|
|
52
|
+
return json(400, { error: "publication requires retentionDays as a positive integer" });
|
|
53
|
+
const expiresAt = (deps.clock ?? systemClock)() + retentionDays * DAY_MS;
|
|
54
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt < 1)
|
|
55
|
+
return json(400, { error: "publication expiry is out of range" });
|
|
56
|
+
const object = await deps.bucket.get(key);
|
|
57
|
+
if (!object) return json(404, { error: "artifact not found" });
|
|
58
|
+
let bytes: Uint8Array;
|
|
59
|
+
try {
|
|
60
|
+
// R2ObjectBody is a runtime object; its size and body need not be enumerable.
|
|
61
|
+
bytes = await readPrImage({
|
|
62
|
+
size: object.size,
|
|
63
|
+
body: object.body,
|
|
64
|
+
contentType: object.httpMetadata?.contentType ?? "",
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
// A storage stream can fail with internal paths or stack details; the caller
|
|
68
|
+
// needs only the stable validation result.
|
|
69
|
+
return json(400, { error: "invalid PR image" });
|
|
70
|
+
}
|
|
71
|
+
const path = `/pr-images/${crypto.randomUUID()}.png`;
|
|
72
|
+
await deps.bucket.put(prImageKey(path)!, bytes, {
|
|
73
|
+
httpMetadata: { contentType: "image/png" },
|
|
74
|
+
customMetadata: { publishedForPr: "true", sourceKey: key, expiresAt: String(expiresAt) },
|
|
75
|
+
});
|
|
76
|
+
return json(200, { path });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Public only by deliberate copy. The URL is an address, not a run-reader
|
|
80
|
+
* capability; there is no way to substitute a private key into this route. */
|
|
81
|
+
export async function handlePrImage(
|
|
82
|
+
request: Request,
|
|
83
|
+
bucket?: PrImageBucket,
|
|
84
|
+
clock: () => number = systemClock,
|
|
85
|
+
): Promise<Response> {
|
|
86
|
+
if (request.method !== "GET" && request.method !== "HEAD") return json(405, { error: "GET or HEAD only" });
|
|
87
|
+
const url = new URL(request.url);
|
|
88
|
+
const key = prImageKey(url.pathname);
|
|
89
|
+
if (!key || url.search || !bucket) return json(404, { error: "not found" });
|
|
90
|
+
const object = await bucket.get(key);
|
|
91
|
+
// Lifecycle deletion is asynchronous; it must not extend public availability.
|
|
92
|
+
// Older or malformed copies without a valid deadline also fail closed.
|
|
93
|
+
const expiresAt = Number(object?.customMetadata?.expiresAt);
|
|
94
|
+
if (
|
|
95
|
+
!object ||
|
|
96
|
+
object.customMetadata?.publishedForPr !== "true" ||
|
|
97
|
+
!Number.isSafeInteger(expiresAt) ||
|
|
98
|
+
expiresAt < 1 ||
|
|
99
|
+
clock() >= expiresAt ||
|
|
100
|
+
object.httpMetadata?.contentType !== "image/png" ||
|
|
101
|
+
object.size < 1 ||
|
|
102
|
+
object.size > MAX_PR_IMAGE_BYTES
|
|
103
|
+
) {
|
|
104
|
+
await object?.body.cancel();
|
|
105
|
+
return json(404, { error: "not found" });
|
|
106
|
+
}
|
|
107
|
+
if (request.method === "HEAD") await object.body.cancel();
|
|
108
|
+
return new Response(request.method === "HEAD" ? null : object.body, {
|
|
109
|
+
headers: {
|
|
110
|
+
"content-type": "image/png",
|
|
111
|
+
"content-length": String(object.size),
|
|
112
|
+
"content-disposition": 'inline; filename="screenshot.png"',
|
|
113
|
+
"cache-control": "no-store",
|
|
114
|
+
"x-content-type-options": "nosniff",
|
|
115
|
+
"content-security-policy": "default-src 'none'; sandbox",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -46,6 +46,8 @@ import { systemClock } from "../../src/core/trace/clock.ts";
|
|
|
46
46
|
import { createTracer } from "../../src/core/trace/tracer.ts";
|
|
47
47
|
import { shimRoute, stripTraceContext, withTraceContext, workerLogSink } from "../../src/core/trace/workerTrace.ts";
|
|
48
48
|
import { COPY_PATH, handleArtifactsCopy } from "./artifactsCopy.ts";
|
|
49
|
+
import { handlePrImage, publishPrImage } from "./prImages.ts";
|
|
50
|
+
import { PR_IMAGE_PUBLISH_PATH } from "../../src/artifacts/prImages.ts";
|
|
49
51
|
import { withKnownLength } from "./knownLength.ts";
|
|
50
52
|
import type { ShipCoordinatorParams } from "./coordinator";
|
|
51
53
|
import { INSTANCE, INTERNAL } from "./shared";
|
|
@@ -456,6 +458,11 @@ const withLength = (res: Response): Response => withKnownLength(res, (size) => n
|
|
|
456
458
|
export default {
|
|
457
459
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
458
460
|
const pathname = new URL(request.url).pathname;
|
|
461
|
+
// Only deliberately published copies are public. The existing run reader
|
|
462
|
+
// and the bucket stay private; the write endpoint checks its own bearer.
|
|
463
|
+
if (pathname.startsWith("/pr-images/")) return withLength(await handlePrImage(request, env.ARTIFACTS));
|
|
464
|
+
if (pathname === PR_IMAGE_PUBLISH_PATH)
|
|
465
|
+
return publishPrImage(request, { bucket: env.ARTIFACTS, token: env.ARTIFACTS_COPY_TOKEN });
|
|
459
466
|
// The public edge (docs/reference/specs/tracing.md item 22): whatever trace context the
|
|
460
467
|
// caller sent is stripped, and what the container sees carries this
|
|
461
468
|
// Worker's own root. A static asset or the live view's SSE stream gets no
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.264.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.264.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -21060,7 +21060,7 @@
|
|
|
21060
21060
|
},
|
|
21061
21061
|
"packages/switchboard": {
|
|
21062
21062
|
"name": "@coreplane/switchboard",
|
|
21063
|
-
"version": "1.
|
|
21063
|
+
"version": "1.264.0",
|
|
21064
21064
|
"license": "Apache-2.0",
|
|
21065
21065
|
"dependencies": {
|
|
21066
21066
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.264.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/source.json
CHANGED
|
@@ -216,7 +216,7 @@ const RESIDENT_TOOLCHAIN = `The resident image carries ${IMAGE_TOOLCHAIN}; plus
|
|
|
216
216
|
// item 10): the one way a run's screenshot reaches the person. Said once so
|
|
217
217
|
// the sandbox and resident variants cannot drift on it.
|
|
218
218
|
const SHOW_FILES = `Files the person should SEE go through the attach_file tool: a screenshot from \`playwright screenshot\`, a rendered PDF, a recording — it posts the workspace file into this conversation, where an image renders inline. Use it whenever you produce an image worth showing (a visual change, a rendered page, a before/after); a link to a file on GitHub is not a picture.
|
|
219
|
-
SCREENSHOTS GO TO BOTH PLACES, ALL OF THEM: when the request asks for screenshots, or the change is visual, every capture is attached here with attach_file AND published on the pull request —
|
|
219
|
+
SCREENSHOTS GO TO BOTH PLACES, ALL OF THEM: when the request asks for screenshots, or the change is visual, every capture is attached here with attach_file AND published on the pull request — use PNG captures, inspect/redact sensitive content, and call attach_file with publishForPr: true to deliberately make a public copy after thread delivery. Embed the returned public URL with Markdown image syntax in the description's validation section or a PR comment, never as a bare link. This applies unless the request names one destination; for thread-only delivery omit publishForPr. Never attach a subset and link the rest. Only deliberately published PNGs (at most 10 MiB) are public, for the artifact retention window (default 30 days), not forever. Never push an assets branch, put captures in the PR's diff, or expose a tokened run-artifact URL as a workaround. If publication is unavailable or refused, report that limit; if attachment was refused too, still attach privately with publishForPr omitted. Do not claim the PR renders an image.
|
|
220
220
|
Whole files: up to 1 GiB where the artifact store is configured (a recording, a large PDF), 10 MiB otherwise — the tool's result says which applies; over the limit, link to the file.
|
|
221
221
|
Files the person dropped on the thread that were too large to show you inline are already in ./attachments/ in your workspace when the turn's text names them (a video for ffmpeg, a large PDF, a zip); read them from there — never ask for a re-upload.
|
|
222
222
|
Text stays in your message; do not attach what you can say.`;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Deliberate public copies, not a public bucket or an exception to the run reader.
|
|
2
|
+
export const PR_IMAGE_PUBLISH_PATH = "/artifacts/publish-pr-image";
|
|
3
|
+
export const MAX_PR_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
4
|
+
const UUID = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
|
|
5
|
+
const PUBLIC_PATH = new RegExp(`^/pr-images/(${UUID})\\.png$`);
|
|
6
|
+
|
|
7
|
+
export function validPrImagePath(path: string): boolean {
|
|
8
|
+
return PUBLIC_PATH.test(path);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function prImageKey(path: string): string | undefined {
|
|
12
|
+
const match = PUBLIC_PATH.exec(path);
|
|
13
|
+
return match ? `published-pr/${match[1]}.png` : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validPrImageSource(key: string): boolean {
|
|
17
|
+
return /^runs\/[A-Za-z0-9_-]+\/out\/\d+-[A-Za-z0-9._-]+$/.test(key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Bounded to one small screenshot in the Worker, never in the bot process.
|
|
21
|
+
* Count the stream as well as its metadata: a dishonest length must not
|
|
22
|
+
* turn the public endpoint into an unbounded read or an active-content host. */
|
|
23
|
+
export async function readPrImage(object: {
|
|
24
|
+
size: number;
|
|
25
|
+
contentType: string;
|
|
26
|
+
body: ReadableStream<Uint8Array>;
|
|
27
|
+
}): Promise<Uint8Array> {
|
|
28
|
+
const reader = object.body.getReader();
|
|
29
|
+
try {
|
|
30
|
+
if (object.contentType !== "image/png") throw new Error("PR images require PNG content type");
|
|
31
|
+
if (!Number.isInteger(object.size) || object.size < 1 || object.size > MAX_PR_IMAGE_BYTES) {
|
|
32
|
+
throw new Error(`PR image size must be 1..${MAX_PR_IMAGE_BYTES} bytes`);
|
|
33
|
+
}
|
|
34
|
+
const bytes = new Uint8Array(object.size);
|
|
35
|
+
let used = 0;
|
|
36
|
+
for (;;) {
|
|
37
|
+
const { done, value } = await reader.read();
|
|
38
|
+
if (done) break;
|
|
39
|
+
if (used + value.length > bytes.length) throw new Error("PR image length exceeds its declared size");
|
|
40
|
+
bytes.set(value, used);
|
|
41
|
+
used += value.length;
|
|
42
|
+
}
|
|
43
|
+
if (used !== bytes.length) throw new Error("PR image length differs from its declared size");
|
|
44
|
+
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
45
|
+
if (!signature.every((byte, index) => bytes[index] === byte)) throw new Error("PR images require a PNG signature");
|
|
46
|
+
return bytes;
|
|
47
|
+
} finally {
|
|
48
|
+
await reader.cancel().catch(() => {});
|
|
49
|
+
reader.releaseLock();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1050,6 +1050,7 @@ async function runUnit(
|
|
|
1050
1050
|
generated: plan.generated,
|
|
1051
1051
|
...(plan.runPageBase !== undefined ? { runPageBase: plan.runPageBase } : {}),
|
|
1052
1052
|
...(resume !== undefined ? { resume } : {}),
|
|
1053
|
+
...(row?.publication !== undefined && resume === undefined && lastPush === undefined ? { freshAdopt: true } : {}),
|
|
1053
1054
|
...(lastPush !== undefined ? { lastPush } : {}),
|
|
1054
1055
|
...(session !== undefined ? { session } : {}),
|
|
1055
1056
|
...(row?.recovery !== undefined
|
|
@@ -446,7 +446,7 @@ export function classifyProviderFailure(input: ProviderFailureAnswer): ProviderF
|
|
|
446
446
|
});
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
function typedProviderFailureOf(error: unknown, seen = new Set<object>()): ProviderFailure | undefined {
|
|
449
|
+
export function typedProviderFailureOf(error: unknown, seen = new Set<object>()): ProviderFailure | undefined {
|
|
450
450
|
if (error instanceof ProviderFailure) return error;
|
|
451
451
|
const row = providerFailureRecord(error);
|
|
452
452
|
if (!row || seen.has(row)) return undefined;
|
|
@@ -1167,6 +1167,9 @@ export interface UnitPipelineInput {
|
|
|
1167
1167
|
idleDays?: number;
|
|
1168
1168
|
/** Resume at review: an open pull request of ship's own the requester named. */
|
|
1169
1169
|
resume?: { pr: number; headSha?: string; url?: string };
|
|
1170
|
+
/** A task adopted an existing PR without a prior coding push. Its old
|
|
1171
|
+
* approval and checks cannot answer this task, so pre-check starts coding. */
|
|
1172
|
+
freshAdopt?: boolean;
|
|
1170
1173
|
/** The head the previous attempt's coding child last pushed (a
|
|
1171
1174
|
* `review_pending` ending's `headSha`, carried on the unit's row): when the
|
|
1172
1175
|
* pre-check finds the open pull request still at exactly this head, there is
|
|
@@ -3083,6 +3086,8 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
3083
3086
|
};
|
|
3084
3087
|
return nextReview(adopted);
|
|
3085
3088
|
}
|
|
3089
|
+
if (s.input.freshAdopt === true)
|
|
3090
|
+
return enterRound({ ...clocked, pr: { number: r.pr.prNumber, url: r.pr.url } }, { index: 0, kind: "coding" });
|
|
3086
3091
|
const branchHead = normalizeHead(r.pr.branchHead);
|
|
3087
3092
|
const lastPush = normalizeHead(s.input.lastPush);
|
|
3088
3093
|
const atBranchHead = head !== undefined && branchHead !== undefined && sameCommit(head, branchHead);
|
|
@@ -3121,6 +3126,12 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
3121
3126
|
return nextReview(adopted);
|
|
3122
3127
|
}
|
|
3123
3128
|
}
|
|
3129
|
+
if (s.input.freshAdopt === true)
|
|
3130
|
+
return end(clocked, {
|
|
3131
|
+
kind: "aborted",
|
|
3132
|
+
reason: `⚠️ The adopted pull request on ${s.input.repo} is no longer open, so coding did not start.`,
|
|
3133
|
+
reviewRounds: s.reviewRounds,
|
|
3134
|
+
});
|
|
3124
3135
|
return { state: { ...clocked, phase: { at: "branch" } }, notes: [] };
|
|
3125
3136
|
}
|
|
3126
3137
|
case "branch": {
|
package/dist/cli.js
CHANGED
|
@@ -362,6 +362,24 @@ var init_clock = __esm({
|
|
|
362
362
|
}
|
|
363
363
|
});
|
|
364
364
|
|
|
365
|
+
// ../../src/artifacts/prImages.ts
|
|
366
|
+
function validPrImagePath(path) {
|
|
367
|
+
return PUBLIC_PATH.test(path);
|
|
368
|
+
}
|
|
369
|
+
function validPrImageSource(key) {
|
|
370
|
+
return /^runs\/[A-Za-z0-9_-]+\/out\/\d+-[A-Za-z0-9._-]+$/.test(key);
|
|
371
|
+
}
|
|
372
|
+
var PR_IMAGE_PUBLISH_PATH, MAX_PR_IMAGE_BYTES, UUID, PUBLIC_PATH;
|
|
373
|
+
var init_prImages = __esm({
|
|
374
|
+
"../../src/artifacts/prImages.ts"() {
|
|
375
|
+
"use strict";
|
|
376
|
+
PR_IMAGE_PUBLISH_PATH = "/artifacts/publish-pr-image";
|
|
377
|
+
MAX_PR_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
378
|
+
UUID = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
|
|
379
|
+
PUBLIC_PATH = new RegExp(`^/pr-images/(${UUID})\\.png$`);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
365
383
|
// ../../src/artifacts/store.ts
|
|
366
384
|
import { AwsClient } from "aws4fetch";
|
|
367
385
|
function parseByteRange(header4) {
|
|
@@ -392,6 +410,7 @@ var init_store = __esm({
|
|
|
392
410
|
"use strict";
|
|
393
411
|
init_clock();
|
|
394
412
|
init_config();
|
|
413
|
+
init_prImages();
|
|
395
414
|
PRESIGN_TTL_SECONDS = ARTIFACT_DEFAULTS.presignTtlSeconds;
|
|
396
415
|
IDENTITY = { "accept-encoding": "identity" };
|
|
397
416
|
R2ArtifactStore = class {
|
|
@@ -400,6 +419,7 @@ var init_store = __esm({
|
|
|
400
419
|
endpoint;
|
|
401
420
|
fetchImpl;
|
|
402
421
|
clock;
|
|
422
|
+
retentionDays;
|
|
403
423
|
copy;
|
|
404
424
|
constructor(opts) {
|
|
405
425
|
this.bucket = opts.bucket;
|
|
@@ -413,6 +433,7 @@ var init_store = __esm({
|
|
|
413
433
|
});
|
|
414
434
|
this.fetchImpl = opts.fetch ?? fetch;
|
|
415
435
|
this.clock = opts.clock ?? systemClock;
|
|
436
|
+
this.retentionDays = opts.retentionDays ?? ARTIFACT_DEFAULTS.retentionDays;
|
|
416
437
|
this.copy = { ...opts.copy, timeoutMs: opts.copy.timeoutMs ?? ARTIFACT_DEFAULTS.copyTimeoutMs };
|
|
417
438
|
}
|
|
418
439
|
objectUrl(key) {
|
|
@@ -472,6 +493,26 @@ var init_store = __esm({
|
|
|
472
493
|
if (!Number.isFinite(size) || size < 0) throw new Error(`artifact store: GET ${key} answered without a length`);
|
|
473
494
|
return { size, contentType, body: res.body };
|
|
474
495
|
}
|
|
496
|
+
async publishPrImage(key) {
|
|
497
|
+
if (!validPrImageSource(key)) throw new Error("PR publication requires an outbound artifact key");
|
|
498
|
+
const base = new URL(this.copy.baseUrl);
|
|
499
|
+
if (base.protocol !== "https:" || base.username || base.password || base.search || base.hash || base.pathname !== "/") {
|
|
500
|
+
throw new Error("PR publication requires a credential-free HTTPS origin");
|
|
501
|
+
}
|
|
502
|
+
const res = await this.fetchImpl(`${base.origin}${PR_IMAGE_PUBLISH_PATH}`, {
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers: { authorization: `Bearer ${this.copy.token.reveal()}`, "content-type": "application/json" },
|
|
505
|
+
body: JSON.stringify({ key, retentionDays: this.retentionDays }),
|
|
506
|
+
redirect: "error",
|
|
507
|
+
signal: AbortSignal.timeout(this.copy.timeoutMs)
|
|
508
|
+
});
|
|
509
|
+
if (!res.ok) throw new Error(`PR publication answered HTTP ${res.status}`);
|
|
510
|
+
const answer = await res.json();
|
|
511
|
+
if (typeof answer.path !== "string" || !validPrImagePath(answer.path)) {
|
|
512
|
+
throw new Error("PR publication returned an invalid public path");
|
|
513
|
+
}
|
|
514
|
+
return `${base.origin}${answer.path}`;
|
|
515
|
+
}
|
|
475
516
|
async copyFromUrl(input) {
|
|
476
517
|
const res = await this.fetchImpl(`${this.copy.baseUrl.replace(/\/$/, "")}/artifacts/copy`, {
|
|
477
518
|
method: "POST",
|
|
@@ -509,6 +550,7 @@ function buildArtifactStore(cfg, secrets, opts) {
|
|
|
509
550
|
return new R2ArtifactStore({
|
|
510
551
|
accountId: cfg.r2.accountId,
|
|
511
552
|
bucket: cfg.r2.bucket,
|
|
553
|
+
retentionDays: cfg.retentionDays,
|
|
512
554
|
accessKeyId: secrets.get("ARTIFACTS_R2_ACCESS_KEY_ID"),
|
|
513
555
|
secretAccessKey: secrets.get("ARTIFACTS_R2_SECRET_ACCESS_KEY"),
|
|
514
556
|
copy: {
|
|
@@ -2335,7 +2377,7 @@ EVERY PR includes one that already exists when you push \u2014 opened by a perso
|
|
|
2335
2377
|
SANDBOX_TOOLCHAIN = `The sandbox image carries ${IMAGE_TOOLCHAIN}; and Docker (the engine starts on the first \`docker\` call).`;
|
|
2336
2378
|
RESIDENT_TOOLCHAIN = `The resident image carries ${IMAGE_TOOLCHAIN}; plus yarn and bun \u2014 and no Docker.`;
|
|
2337
2379
|
SHOW_FILES = `Files the person should SEE go through the attach_file tool: a screenshot from \`playwright screenshot\`, a rendered PDF, a recording \u2014 it posts the workspace file into this conversation, where an image renders inline. Use it whenever you produce an image worth showing (a visual change, a rendered page, a before/after); a link to a file on GitHub is not a picture.
|
|
2338
|
-
SCREENSHOTS GO TO BOTH PLACES, ALL OF THEM: when the request asks for screenshots, or the change is visual, every capture is attached here with attach_file AND published on the pull request \u2014
|
|
2380
|
+
SCREENSHOTS GO TO BOTH PLACES, ALL OF THEM: when the request asks for screenshots, or the change is visual, every capture is attached here with attach_file AND published on the pull request \u2014 use PNG captures, inspect/redact sensitive content, and call attach_file with publishForPr: true to deliberately make a public copy after thread delivery. Embed the returned public URL with Markdown image syntax in the description's validation section or a PR comment, never as a bare link. This applies unless the request names one destination; for thread-only delivery omit publishForPr. Never attach a subset and link the rest. Only deliberately published PNGs (at most 10 MiB) are public, for the artifact retention window (default 30 days), not forever. Never push an assets branch, put captures in the PR's diff, or expose a tokened run-artifact URL as a workaround. If publication is unavailable or refused, report that limit; if attachment was refused too, still attach privately with publishForPr omitted. Do not claim the PR renders an image.
|
|
2339
2381
|
Whole files: up to 1 GiB where the artifact store is configured (a recording, a large PDF), 10 MiB otherwise \u2014 the tool's result says which applies; over the limit, link to the file.
|
|
2340
2382
|
Files the person dropped on the thread that were too large to show you inline are already in ./attachments/ in your workspace when the turn's text names them (a video for ffmpeg, a large PDF, a zip); read them from there \u2014 never ask for a re-upload.
|
|
2341
2383
|
Text stays in your message; do not attach what you can say.`;
|
|
@@ -40747,21 +40789,26 @@ function instanceOf(runs) {
|
|
|
40747
40789
|
return void 0;
|
|
40748
40790
|
}
|
|
40749
40791
|
async function ownerOf2(runs, unitsOf, threadKey) {
|
|
40750
|
-
const live = runs.find((r) => !r.finished);
|
|
40792
|
+
const live = runs.find((r) => !r.finished && r.hosted !== true);
|
|
40751
40793
|
if (live !== void 0) return { kind: "live", run: live };
|
|
40794
|
+
const hosted = runs.find((r) => !r.finished && r.hosted === true);
|
|
40752
40795
|
const instanceId = instanceOf(runs);
|
|
40753
40796
|
if (instanceId !== void 0) {
|
|
40754
40797
|
const units = await unitsOf(instanceId).catch(() => []);
|
|
40755
40798
|
const unit = units.find((u) => u.threadKey === threadKey && u.ending === void 0);
|
|
40756
40799
|
if (unit !== void 0) return { kind: "unit", instanceId, unit };
|
|
40800
|
+
if (hosted !== void 0) return { kind: "live", run: hosted };
|
|
40757
40801
|
const ended = units.filter(
|
|
40758
|
-
(u) => u.threadKey === threadKey && u.ending !== void 0 && u.ending.kind !== "merged" && u.ending.kind !== "already_landed"
|
|
40802
|
+
(u) => u.threadKey === threadKey && u.ending !== void 0 && u.ending.kind !== "merged" && u.ending.kind !== "already_landed" && // A completed, publication-bound PR is new work on the next reply.
|
|
40803
|
+
// Keep older rows without that binding on the guarded recovery path.
|
|
40804
|
+
(u.ending.kind !== "merge_ready" || u.publication === void 0)
|
|
40759
40805
|
);
|
|
40760
40806
|
const ship = runs.find((r) => r.instanceId === instanceId && r.finished);
|
|
40761
40807
|
if (ended.length === 1 && ship !== void 0) return { kind: "pipeline", instanceId, run: ship, unit: ended[0] };
|
|
40762
40808
|
if (ended.length > 1 && ship !== void 0)
|
|
40763
40809
|
return { kind: "pipeline_ambiguous", instanceId, run: ship, units: ended };
|
|
40764
40810
|
}
|
|
40811
|
+
if (hosted !== void 0) return { kind: "live", run: hosted };
|
|
40765
40812
|
const agent = stickyAgentOf(runs);
|
|
40766
40813
|
if (agent !== void 0) return { kind: "session", agent };
|
|
40767
40814
|
return { kind: "none" };
|
|
@@ -40962,6 +41009,39 @@ function operatorPresets() {
|
|
|
40962
41009
|
}
|
|
40963
41010
|
];
|
|
40964
41011
|
}
|
|
41012
|
+
function requestedModelWord(request2, modelWord, ref) {
|
|
41013
|
+
const word = modelWord.trim().toLowerCase();
|
|
41014
|
+
if (!word) return false;
|
|
41015
|
+
const words = [];
|
|
41016
|
+
let current = "";
|
|
41017
|
+
for (const char of request2.toLowerCase()) {
|
|
41018
|
+
const code = char.charCodeAt(0);
|
|
41019
|
+
if (code >= 97 && code <= 122 || code >= 48 && code <= 57 || char === "-" || char === ".") {
|
|
41020
|
+
current += char;
|
|
41021
|
+
} else if (current) {
|
|
41022
|
+
words.push(current);
|
|
41023
|
+
current = "";
|
|
41024
|
+
}
|
|
41025
|
+
}
|
|
41026
|
+
if (current) words.push(current);
|
|
41027
|
+
const cues = /* @__PURE__ */ new Set(["with", "using", "use", "on", "via", "model"]);
|
|
41028
|
+
let selected = false;
|
|
41029
|
+
for (let i = 0; i < words.length; i++) {
|
|
41030
|
+
if (words[i] !== word) continue;
|
|
41031
|
+
const previous = words[i - 1];
|
|
41032
|
+
if (previous !== void 0 && cues.has(previous)) selected = true;
|
|
41033
|
+
if ((previous === "the" || previous === "a") && i >= 2 && cues.has(words[i - 2])) selected = true;
|
|
41034
|
+
}
|
|
41035
|
+
if (!selected) return false;
|
|
41036
|
+
let modelId;
|
|
41037
|
+
try {
|
|
41038
|
+
modelId = parseModelRef(ref).model.toLowerCase();
|
|
41039
|
+
} catch {
|
|
41040
|
+
return true;
|
|
41041
|
+
}
|
|
41042
|
+
const modelName = modelId.split("/").pop();
|
|
41043
|
+
return modelName === word || modelName?.endsWith(`-${word}`) === true;
|
|
41044
|
+
}
|
|
40965
41045
|
function compareCapability(a, b) {
|
|
40966
41046
|
const machine = Number(a.machine !== "none") - Number(b.machine !== "none");
|
|
40967
41047
|
if (machine !== 0) return machine;
|
|
@@ -41021,8 +41101,8 @@ function buildOperatorPrompt(input) {
|
|
|
41021
41101
|
] : [],
|
|
41022
41102
|
"A write ask in a named or inherited repository binds the write preset even when a detail inside it is unresolved \u2014 the run it starts resolves the detail with the repository in front of it. Ask a question only for a fork the run itself could not resolve, and a question's proposal must be a line that would do the asked work: a write line for a write ask, never a read (an exploration, a listing, a summary) standing in for the work.",
|
|
41023
41103
|
"A read command answers only a read intent: an ask to change, set, switch or update something is a write, and a listing or a show never answers it. Every command call declares its `intent`. When a write ask misses a required detail, or names a model provider this deployment does not have, read `provider_models` for the refs this deployment can run, then call `ask` with a proposal that would do the write built from them \u2014 the person's yes runs it, and their next words refine it.",
|
|
41024
|
-
"When the request names a model in plain words \u2014 'with astra, \u2026', 'use sol for this', 'on gpt-6' \u2014 read `provider_models` to resolve the word to exactly ONE ref this deployment can run
|
|
41025
|
-
"`bind_preset` runs the preset on the request as the author asked it \u2014 the author's own words ride by reference, so the call names only the preset,
|
|
41104
|
+
"When the request names a model in plain words \u2014 'with astra, \u2026', 'use sol for this', 'on gpt-6' \u2014 read `provider_models` to resolve the word to exactly ONE ref this deployment can run. Pass that ref as `bind_preset`'s `model` and one exact model-name word from the person's request as `modelWord` (such as 'astra', 'o3', or 'gpt-6'): the run then uses it, exactly as a typed `model:` directive would. The request still rides verbatim \u2014 never strip the model word from it. A word that matches several refs, or none, is one `ask` naming the catalogue's candidate refs \u2014 never a guess and never a silent default; a request naming no model omits both `model` and `modelWord`.",
|
|
41105
|
+
"`bind_preset` runs the preset on the request as the author asked it \u2014 the author's own words ride by reference, so the call names only the preset, optional model and modelWord, and the reason: never re-type the request, never a flag form and never a paraphrase.",
|
|
41026
41106
|
"",
|
|
41027
41107
|
// 2. Projection: the presets and commands THIS author may run.
|
|
41028
41108
|
"Presets this author may run:",
|
|
@@ -41084,6 +41164,10 @@ function operatorTools(input) {
|
|
|
41084
41164
|
type: "string",
|
|
41085
41165
|
description: "the model ref the run uses, ONLY when the request names a model in plain words: a `<provider>/<model>` ref `provider_models` lists, resolved from the person's word \u2014 omit when no model is named, and ask instead of guessing when the word matches several refs or none"
|
|
41086
41166
|
},
|
|
41167
|
+
modelWord: {
|
|
41168
|
+
type: "string",
|
|
41169
|
+
description: "one exact model-name word in the person's request that `model` resolves, such as astra, o3, or gpt-6; omit with `model` when no model is named"
|
|
41170
|
+
},
|
|
41087
41171
|
repo: {
|
|
41088
41172
|
type: "string",
|
|
41089
41173
|
pattern: "^[\\w.-]+/[\\w.-]+$",
|
|
@@ -41285,15 +41369,15 @@ function parseOperatorTurn(answer, ctx) {
|
|
|
41285
41369
|
...typeof input.filter === "string" && input.filter.trim().length > 0 ? { filter: input.filter } : {}
|
|
41286
41370
|
};
|
|
41287
41371
|
if (answer.tool === OPERATOR_BIND_TOOL) {
|
|
41288
|
-
const { preset, reason: reason2, model, repo } = input;
|
|
41372
|
+
const { preset, reason: reason2, model, modelWord, repo } = input;
|
|
41289
41373
|
if (typeof preset !== "string" || !ctx.presets.includes(preset))
|
|
41290
41374
|
return {
|
|
41291
41375
|
kind: "violation",
|
|
41292
41376
|
violation: `bind_preset named "${String(preset)}", not a preset the projection offers`
|
|
41293
41377
|
};
|
|
41294
41378
|
let ref;
|
|
41295
|
-
if (model !==
|
|
41296
|
-
const trimmed2 =
|
|
41379
|
+
if (typeof model === "string" && model.trim() !== "" && typeof modelWord === "string" && requestedModelWord(ctx.requestText, modelWord, model)) {
|
|
41380
|
+
const trimmed2 = model.trim();
|
|
41297
41381
|
const parsed = /^([A-Za-z0-9_.-]+)\/\S+$/.exec(trimmed2);
|
|
41298
41382
|
if (parsed === null)
|
|
41299
41383
|
return {
|
|
@@ -41504,6 +41588,16 @@ async function runOperator(input, model, opts = {}) {
|
|
|
41504
41588
|
const carried = attemptsOfThrow(err2);
|
|
41505
41589
|
if (carried) attempts.push(...carried);
|
|
41506
41590
|
const failure2 = providerFailureOf(err2);
|
|
41591
|
+
const typedFailure = typedProviderFailureOf(err2);
|
|
41592
|
+
const localAbort = err2 instanceof Error && err2.name === "AbortError";
|
|
41593
|
+
if (signal2.aborted && typedFailure === void 0 && (localAbort || failure2.cause === "permanent" && failure2.status === void 0)) {
|
|
41594
|
+
return answered({
|
|
41595
|
+
kind: "refusal",
|
|
41596
|
+
cause: "timeout",
|
|
41597
|
+
reason: "operator_timeout",
|
|
41598
|
+
text: "The routing request timed out; nothing started."
|
|
41599
|
+
});
|
|
41600
|
+
}
|
|
41507
41601
|
const rejected = failure2.cause === "request-rejected" && failure2.status === 400 ? failure2.schemaRejection : void 0;
|
|
41508
41602
|
const offered = rejected !== void 0 && [prompt.tool, ...prompt.tools ?? []].some((candidate) => candidate.name === rejected.tool);
|
|
41509
41603
|
const retry = offered && rejected !== void 0 && schemaReasks < OPERATOR_SCHEMA_REASKS_MAX ? promptWithoutTool(prompt, rejected.tool) : void 0;
|
|
@@ -41744,9 +41838,11 @@ async function executeOperatorDecision(deps, ctx) {
|
|
|
41744
41838
|
await recordOperatorDecision(deps, msg, event, ctx.ending, ctx.trace);
|
|
41745
41839
|
return answered;
|
|
41746
41840
|
}
|
|
41747
|
-
if (event.providerFailure !== void 0) {
|
|
41841
|
+
if (event.providerFailure !== void 0 || event.refusalCause === "timeout") {
|
|
41748
41842
|
io.requestFailed?.();
|
|
41749
|
-
await io.reply(
|
|
41843
|
+
await io.reply(
|
|
41844
|
+
event.refusalText ?? (event.providerFailure !== void 0 ? renderProviderFailure(event.providerFailure, "ended") : "The routing request timed out; nothing started.")
|
|
41845
|
+
);
|
|
41750
41846
|
await recordOperatorDecision(deps, msg, event, ctx.ending, ctx.trace);
|
|
41751
41847
|
return answered;
|
|
41752
41848
|
}
|
|
@@ -41890,7 +41986,7 @@ var init_operator = __esm({
|
|
|
41890
41986
|
};
|
|
41891
41987
|
OPERATOR_READS_MAX = 4;
|
|
41892
41988
|
OPERATOR_SCHEMA_REASKS_MAX = 4;
|
|
41893
|
-
OPERATOR_TIMEOUT_MS =
|
|
41989
|
+
OPERATOR_TIMEOUT_MS = MINUTE_MS;
|
|
41894
41990
|
OPERATOR_QUESTION_MARKER = "Did you mean:";
|
|
41895
41991
|
OPERATOR_REQUEST_CAP = 600;
|
|
41896
41992
|
OPERATOR_SOURCES_MAX = 12;
|
|
@@ -43149,7 +43245,7 @@ function parsePutReport(output) {
|
|
|
43149
43245
|
function postCommandFor(path, url) {
|
|
43150
43246
|
return `curl -fsS --upload-file ${shellQuote(path)} -X POST ${shellQuote(url)}`;
|
|
43151
43247
|
}
|
|
43152
|
-
async function attachThroughStore(path, name, lead, artifacts, ctx) {
|
|
43248
|
+
async function attachThroughStore(path, name, lead, artifacts, ctx, publishForPr) {
|
|
43153
43249
|
const exec = (command2, timeoutMs2) => {
|
|
43154
43250
|
const opts = { timeoutMs: timeoutMs2, ...ctx.signal ? { signal: ctx.signal } : {} };
|
|
43155
43251
|
return ctx.executor.exec(command2, opts);
|
|
@@ -43161,6 +43257,7 @@ async function attachThroughStore(path, name, lead, artifacts, ctx) {
|
|
|
43161
43257
|
const size = parseByteSize(statOut);
|
|
43162
43258
|
if (size === null) return `error: could not measure ${path}: ${statOut.trim()}`;
|
|
43163
43259
|
if (size === 0) return `error: ${path} is empty \u2014 nothing to attach`;
|
|
43260
|
+
if (publishForPr && size > MAX_PR_IMAGE_BYTES) return `error: PR images take at most ${MAX_PR_IMAGE_BYTES} bytes`;
|
|
43164
43261
|
if (size > MAX_ARTIFACT_BYTES) {
|
|
43165
43262
|
return `error: ${path} is ${size} bytes; attach_file takes files up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) \u2014 link to the file instead`;
|
|
43166
43263
|
}
|
|
@@ -43191,12 +43288,22 @@ async function attachThroughStore(path, name, lead, artifacts, ctx) {
|
|
|
43191
43288
|
return `error: the artifact store holds ${head.size} bytes for ${name}, not the ${size} measured${curlSaid} \u2014 nothing was posted`;
|
|
43192
43289
|
}
|
|
43193
43290
|
ctx.publish?.({ type: "artifact", direction: "out", key, name, size, contentType, callId });
|
|
43291
|
+
const finish2 = async (attached) => {
|
|
43292
|
+
if (!publishForPr) return attached;
|
|
43293
|
+
try {
|
|
43294
|
+
const url = await artifacts.store.publishPrImage(key);
|
|
43295
|
+
return `${attached}; public PR image (retained by the artifact lifecycle): `;
|
|
43296
|
+
} catch (err2) {
|
|
43297
|
+
return `error: ${attached}; PR publication failed: ${describe4(err2)} \u2014 no public PR image was returned`;
|
|
43298
|
+
}
|
|
43299
|
+
};
|
|
43194
43300
|
if (!ctx.uploadTicket) {
|
|
43195
43301
|
const link = artifacts.artifactUrl?.(key);
|
|
43196
43302
|
const where = link ? ` \u2014 ${link}` : ` is on the run page as ${key}`;
|
|
43197
43303
|
await artifacts.reply(`${lead}
|
|
43198
43304
|
\u{1F4CE} ${name} (${size} bytes)${where}`);
|
|
43199
|
-
|
|
43305
|
+
const linked = `attached ${name} (${size} bytes) to the run page as ${key}; this conversation's channel takes no file uploads, so the lead and the file's link were posted instead`;
|
|
43306
|
+
return publishForPr ? `error: ${linked}; PR publication requires a completed channel upload \u2014 no public PR image was published` : linked;
|
|
43200
43307
|
}
|
|
43201
43308
|
const kept = "the file is kept on the run page";
|
|
43202
43309
|
let ticket;
|
|
@@ -43214,7 +43321,7 @@ async function attachThroughStore(path, name, lead, artifacts, ctx) {
|
|
|
43214
43321
|
} catch (err2) {
|
|
43215
43322
|
return `error: the channel refused to complete the upload of ${name}: ${describe4(err2)}; ${kept}`;
|
|
43216
43323
|
}
|
|
43217
|
-
return `attached ${name} (${size} bytes) to the conversation and the run page
|
|
43324
|
+
return finish2(`attached ${name} (${size} bytes) to the conversation and the run page`);
|
|
43218
43325
|
}
|
|
43219
43326
|
var MAX_ARTIFACT_BYTES, TRANSFER_FLOOR_BYTES_PER_MS, TRANSFER_SETUP_MS, STAT_TIMEOUT_MS, describe4, secs2, PUT_REPORT_FORMAT, attachFileTool;
|
|
43220
43327
|
var init_attach = __esm({
|
|
@@ -43222,6 +43329,7 @@ var init_attach = __esm({
|
|
|
43222
43329
|
"use strict";
|
|
43223
43330
|
init_contentType();
|
|
43224
43331
|
init_keys();
|
|
43332
|
+
init_prImages();
|
|
43225
43333
|
init_runEvents();
|
|
43226
43334
|
init_bashTimeout();
|
|
43227
43335
|
init_binaryRead();
|
|
@@ -43236,11 +43344,15 @@ var init_attach = __esm({
|
|
|
43236
43344
|
attachFileTool = {
|
|
43237
43345
|
name: "attach_file",
|
|
43238
43346
|
failsInText: true,
|
|
43239
|
-
description: `Post a file from the workspace into the conversation so the person sees it inline \u2014 a screenshot (e.g. from \`playwright screenshot\`), a rendered PDF, a recording, a log. Use it whenever you produce an image worth showing: a link to a file is not a picture. Whole files only: up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) where the artifact store is configured, ${MAX_READ_BYTES} bytes otherwise \u2014 the result says which.`,
|
|
43347
|
+
description: `Post a file from the workspace into the conversation so the person sees it inline \u2014 a screenshot (e.g. from \`playwright screenshot\`), a rendered PDF, a recording, a log. Use it whenever you produce an image worth showing: a link to a file is not a picture. Whole files only: up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) where the artifact store is configured, ${MAX_READ_BYTES} bytes otherwise \u2014 the result says which. For an inspected, non-sensitive PNG screenshot, publishForPr: true also deliberately publishes a public PR image URL after thread delivery (10 MiB max, artifact retention applies). Omit it to keep the file private.`,
|
|
43240
43348
|
inputSchema: {
|
|
43241
43349
|
type: "object",
|
|
43242
43350
|
properties: {
|
|
43243
43351
|
path: { type: "string", description: "Relative path of the file in the workspace" },
|
|
43352
|
+
publishForPr: {
|
|
43353
|
+
type: "boolean",
|
|
43354
|
+
description: "Explicitly make this PNG public for embedding in a PR, after attaching it here. Default false; inspect/redact first. Requires a bound permitted repository and artifact store. The copy expires under the artifact lifecycle."
|
|
43355
|
+
},
|
|
43244
43356
|
comment: {
|
|
43245
43357
|
type: "string",
|
|
43246
43358
|
description: "One line posted with the file saying what it shows (default: the file name)"
|
|
@@ -43253,7 +43365,16 @@ var init_attach = __esm({
|
|
|
43253
43365
|
if (!path) return "error: path is required";
|
|
43254
43366
|
const name = fileNameOf2(path);
|
|
43255
43367
|
const lead = typeof input.comment === "string" && input.comment.trim() ? input.comment.trim() : name;
|
|
43256
|
-
if (
|
|
43368
|
+
if (input.publishForPr !== void 0 && typeof input.publishForPr !== "boolean")
|
|
43369
|
+
return "error: publishForPr must be a boolean";
|
|
43370
|
+
const publishForPr = input.publishForPr === true;
|
|
43371
|
+
if (publishForPr) {
|
|
43372
|
+
if (!ctx.repo) return "error: PR publication requires a bound repository";
|
|
43373
|
+
if (!ctx.github?.canWrite(ctx.repo)) return "error: PR publication requires permission to use this repository";
|
|
43374
|
+
if (!ctx.artifacts) return "error: PR publication requires the artifact store; no public image was published";
|
|
43375
|
+
if (contentTypeFor(name) !== "image/png") return "error: PR publication supports only PNG screenshots";
|
|
43376
|
+
}
|
|
43377
|
+
if (ctx.artifacts) return attachThroughStore(path, name, lead, ctx.artifacts, ctx, publishForPr);
|
|
43257
43378
|
if (!ctx.attach) {
|
|
43258
43379
|
return "attach_file is not available here: this conversation's channel takes no file uploads \u2014 link to the file instead";
|
|
43259
43380
|
}
|
|
@@ -49893,6 +50014,7 @@ function visiblePart(part) {
|
|
|
49893
50014
|
}
|
|
49894
50015
|
function fromPiMessage(message4, provider, tools) {
|
|
49895
50016
|
if (message4.stopReason === "error" || message4.stopReason === "aborted") {
|
|
50017
|
+
if (message4.stopReason === "aborted") throw new DOMException("The model call was aborted", "AbortError");
|
|
49896
50018
|
const raw = message4.errorMessage ?? "the model call failed";
|
|
49897
50019
|
const failure2 = classifyProviderFailure({ error: raw, provider, model: message4.model });
|
|
49898
50020
|
const schemaRejection = failure2.cause === "request-rejected" && failure2.status === 400 ? providerSchemaRejectionOf(raw, tools) : void 0;
|
|
@@ -50024,6 +50146,7 @@ var init_piAi = __esm({
|
|
|
50024
50146
|
const message4 = await this.api$().stream(model, shaped.body, options).result();
|
|
50025
50147
|
return fromPiMessage(message4, this.name, req.tools);
|
|
50026
50148
|
} catch (err2) {
|
|
50149
|
+
if (err2 instanceof Error && err2.name === "AbortError") throw err2;
|
|
50027
50150
|
const failure2 = providerFailureOf(err2);
|
|
50028
50151
|
const schemaRejection = failure2.schemaRejection ?? (failure2.cause === "request-rejected" && failure2.status === 400 ? providerSchemaRejectionOf(err2, req.tools) : void 0);
|
|
50029
50152
|
throw new ProviderFailure(failure2.cause, {
|
|
@@ -56593,7 +56716,7 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
56593
56716
|
// offers no steer line, the executor folds a steer bind like any
|
|
56594
56717
|
// other, and the fold runs on to the seed refusal below, which
|
|
56595
56718
|
// names the unit threads to reply in.
|
|
56596
|
-
pageOwner.run.
|
|
56719
|
+
pageOwner.run.hosted === true ? { kind: "live" } : { kind: "live", runId: pageOwner.run.id }
|
|
56597
56720
|
) : pageOwner?.kind === "unit" ? { kind: "unit", unit: pageOwner.unit.unit } : pageOwner?.kind === "pipeline" ? { kind: "pipeline", unit: pageOwner.unit.unit } : pageOwner?.kind === "pipeline_ambiguous" ? { kind: "pipeline", unit: pageOwner.units.map((unit) => unit.unit).join(", ") } : void 0;
|
|
56598
56721
|
operatorEvent = await root.span(
|
|
56599
56722
|
"dispatch.operator",
|
|
@@ -56680,8 +56803,8 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
56680
56803
|
const threadLive = admission.get(msg.threadKey) !== void 0 || !resume && !restart && deps.threadsElsewhere.get(msg.threadKey) !== void 0;
|
|
56681
56804
|
if (thread && !threadLive && deps.coordinatorInstances !== void 0) {
|
|
56682
56805
|
const owner = pageOwner ?? await ownerOf2(thread, (id) => deps.coordinatorInstances.listUnits(id), msg.threadKey);
|
|
56683
|
-
if (owner.kind === "live" && owner.run.
|
|
56684
|
-
const units = await deps.coordinatorInstances.listUnits(owner.run.instanceId).catch(() => []);
|
|
56806
|
+
if (owner.kind === "live" && owner.run.hosted === true) {
|
|
56807
|
+
const units = owner.run.instanceId === void 0 ? [] : await deps.coordinatorInstances.listUnits(owner.run.instanceId).catch(() => []);
|
|
56685
56808
|
const open2 = units.filter((u) => u.ending === void 0);
|
|
56686
56809
|
await refuse3(
|
|
56687
56810
|
refusalOf(
|
|
@@ -65091,14 +65214,13 @@ async function askModel(input, deps) {
|
|
|
65091
65214
|
);
|
|
65092
65215
|
return { ...seam.value, attempts: seam.attempts };
|
|
65093
65216
|
} catch (err2) {
|
|
65094
|
-
const timedOut = err2 instanceof Error && (err2.name === "TimeoutError" || err2.name === "AbortError");
|
|
65217
|
+
const timedOut = typedProviderFailureOf(err2) === void 0 && err2 instanceof Error && (err2.name === "TimeoutError" || err2.name === "AbortError");
|
|
65095
65218
|
const attempts = attemptsOfThrow(err2);
|
|
65096
65219
|
if (timedOut) {
|
|
65097
65220
|
return {
|
|
65098
65221
|
verdict: "silent",
|
|
65099
|
-
reason:
|
|
65222
|
+
reason: "The request timed out before it could start.",
|
|
65100
65223
|
source: "timeout",
|
|
65101
|
-
providerFailure: "transient",
|
|
65102
65224
|
...attempts ? { attempts } : {}
|
|
65103
65225
|
};
|
|
65104
65226
|
}
|
|
@@ -73870,28 +73992,49 @@ async function steerChild(body, deps) {
|
|
|
73870
73992
|
const steered = await deps.steerChild?.(body.runId, text, instance) ?? false;
|
|
73871
73993
|
return json3(200, { ok: true, outcome: steered ? "steered" : "not_live", at });
|
|
73872
73994
|
}
|
|
73995
|
+
function publicationStepPrefix(row2) {
|
|
73996
|
+
const segment = row2.segments?.reduce((latest, entry) => Math.max(latest, entry.index), 1) ?? 1;
|
|
73997
|
+
if ((row2.segments?.filter((entry) => entry.index === segment).length ?? 0) > 1)
|
|
73998
|
+
throw new PublicationBindingRefusal("publication_facts_mismatch");
|
|
73999
|
+
const base = stepPrefixOf(row2.unit, segment > 1 ? { segment, renewalsSpent: 0, spendUsd: null } : void 0);
|
|
74000
|
+
const resumes = Object.entries(row2.wakes ?? {}).filter(
|
|
74001
|
+
([, answer]) => answer.kind === "segment" && answer.index === segment && answer.leaseMs !== void 0
|
|
74002
|
+
);
|
|
74003
|
+
let prefix = base;
|
|
74004
|
+
for (let attempt = 1; attempt <= resumes.length; attempt += 1) {
|
|
74005
|
+
const idlePrefix = `${prefix}/idle/`;
|
|
74006
|
+
const answers = resumes.filter(
|
|
74007
|
+
([key]) => key.startsWith(idlePrefix) && /^[1-9][0-9]*$/.test(key.slice(idlePrefix.length))
|
|
74008
|
+
);
|
|
74009
|
+
if (answers.length !== 1) throw new PublicationBindingRefusal("publication_facts_mismatch");
|
|
74010
|
+
prefix = `${base}/r${attempt}`;
|
|
74011
|
+
}
|
|
74012
|
+
return prefix;
|
|
74013
|
+
}
|
|
73873
74014
|
async function advanceFindingsPublication(deps, instance, row2, facts2, runId3) {
|
|
73874
74015
|
if (row2.publication === void 0 || facts2.headSha === row2.publication.expectedHeadSha) return row2;
|
|
73875
74016
|
const child = await deps.runs.getRun(runId3, { include: "messages" });
|
|
73876
74017
|
const owner = { instanceId: instance.id, unit: row2.unit };
|
|
73877
74018
|
const latestReview = [...row2.rounds].reverse().find((round2) => round2.agent === "review");
|
|
73878
|
-
const
|
|
73879
|
-
const prefix = row2.recovery !== void 0 ? `${row2.unit}/recovery` : stepPrefixOf(row2.unit, segment > 1 ? { segment, renewalsSpent: 0, spendUsd: null } : void 0);
|
|
74019
|
+
const prefix = row2.recovery !== void 0 ? `${row2.unit}/recovery` : publicationStepPrefix(row2);
|
|
73880
74020
|
const findingsPrefix = `${instance.id}:${prefix}/${latestReview?.index}/findings`;
|
|
73881
74021
|
if (row2.recovery === void 0) {
|
|
73882
74022
|
const tags = child.ok ? child.value.events?.filter((event) => event.type === "coordinator_tag") ?? [] : [];
|
|
73883
74023
|
const tag = tags.length === 1 ? tags[0] : void 0;
|
|
73884
|
-
if (row2.ending !== void 0 || row2.idle !== void 0 || latestReview?.outcome !== "request_changes" || !child.ok || child.value.startedAt < latestReview.at || tag?.parentInstanceId !== instance.id || tag.unit !== row2.unit || tag.base !== instance.base ||
|
|
74024
|
+
if (row2.ending !== void 0 || row2.idle !== void 0 || latestReview?.outcome !== "request_changes" || !child.ok || child.value.startedAt < latestReview.at || tag?.parentInstanceId !== instance.id || tag.unit !== row2.unit || tag.base !== instance.base || !samePublicationBinding(tag.publication, row2.publication))
|
|
73885
74025
|
throw new PublicationBindingRefusal("publication_facts_mismatch");
|
|
73886
74026
|
const listing = await deps.runs.listRuns({
|
|
73887
|
-
status: "
|
|
74027
|
+
status: "all",
|
|
73888
74028
|
visibleTo: EVERY_RUN,
|
|
73889
74029
|
threadKey: row2.threadKey ?? instance.threadKey,
|
|
73890
74030
|
limit: RUN_LIST_MAX_LIMIT
|
|
73891
74031
|
});
|
|
73892
|
-
|
|
74032
|
+
const attempts = listing.runs.filter(
|
|
73893
74033
|
(run2) => run2.parentInstanceId === instance.id && isStepAttempt(run2.idempotencyKey, findingsPrefix)
|
|
73894
|
-
)
|
|
74034
|
+
);
|
|
74035
|
+
if (listing.storeUnavailable || listing.nextBefore !== void 0 || attempts.filter((run2) => run2.id === runId3).length !== 1 || attempts.some(
|
|
74036
|
+
(run2) => run2.id !== runId3 && !(run2.finished && (run2.status === "failed" || run2.status === "interrupted") && run2.agent === "coding" && run2.userId === instance.userId && run2.repo?.toLowerCase() === instance.repo.toLowerCase() && run2.threadKey === (row2.threadKey ?? instance.threadKey) && run2.startedAt >= latestReview.at && run2.finishedAt !== void 0 && run2.finishedAt >= run2.startedAt && run2.finishedAt <= child.value.startedAt && (run2.pushed?.length ?? 0) === 0)
|
|
74037
|
+
))
|
|
73895
74038
|
throw new PublicationBindingRefusal("publication_facts_mismatch");
|
|
73896
74039
|
}
|
|
73897
74040
|
if (!child.ok || child.value.finished !== true || child.value.status !== "completed" || child.value.agent !== "coding" || child.value.parentInstanceId !== instance.id || child.value.userId !== instance.userId || child.value.repo?.toLowerCase() !== instance.repo.toLowerCase() || child.value.threadKey !== (row2.threadKey ?? instance.threadKey) || (row2.recovery !== void 0 ? child.value.idempotencyKey?.startsWith(`${instance.id}:${row2.unit}/recovery/`) !== true || !/\/findings(?:\/a[1-9][0-9]*)?$/.test(child.value.idempotencyKey ?? "") : !isStepAttempt(child.value.idempotencyKey, findingsPrefix)) || !fullHead(facts2.headSha) || child.value.headSha !== facts2.headSha || child.value.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) !== true || facts2.state !== "open" || facts2.sameRepoHead !== true || facts2.headBranchExists !== true || facts2.headRef !== row2.publication.headRef || facts2.baseRef !== row2.publication.baseRef || facts2.verifiedHead?.repo.toLowerCase() !== instance.repo.toLowerCase() || facts2.verifiedHead.ref !== row2.publication.publicationRef || facts2.verifiedHead.sha !== facts2.headSha || row2.publication.repo.toLowerCase() !== instance.repo.toLowerCase() || row2.publication.pr !== row2.pr?.number || row2.publication.baseRef !== instance.base || row2.publication.headRef !== row2.branch || row2.publication.publicationRef !== row2.branch || !samePublicationOwner(row2.publication.owner, owner))
|
|
@@ -74108,13 +74251,13 @@ async function recoverOriginalUnit(body, deps, caller) {
|
|
|
74108
74251
|
const reviewThreadKey = row2.reviewThread?.threadKey ?? row2.threadKey ?? instance.threadKey;
|
|
74109
74252
|
const unitThreadKey = row2.threadKey ?? instance.threadKey;
|
|
74110
74253
|
const listing = await deps.runs.listRuns({
|
|
74111
|
-
status:
|
|
74254
|
+
status: "all",
|
|
74112
74255
|
visibleTo: EVERY_RUN,
|
|
74113
74256
|
threadKey: reviewThreadKey,
|
|
74114
74257
|
limit: RUN_LIST_MAX_LIMIT
|
|
74115
74258
|
});
|
|
74116
74259
|
const unitListing = unitThreadKey === reviewThreadKey ? listing : await deps.runs.listRuns({
|
|
74117
|
-
status:
|
|
74260
|
+
status: "all",
|
|
74118
74261
|
visibleTo: EVERY_RUN,
|
|
74119
74262
|
threadKey: unitThreadKey,
|
|
74120
74263
|
limit: RUN_LIST_MAX_LIMIT
|
|
@@ -74177,17 +74320,28 @@ async function recoverOriginalUnit(body, deps, caller) {
|
|
|
74177
74320
|
const verified2 = facts2.verifiedHead;
|
|
74178
74321
|
if (facts2.state !== "open" || facts2.sameRepoHead !== true || facts2.headBranchExists !== true || facts2.headRef !== row2.branch || facts2.baseRef !== base || !fullHead(facts2.headSha) || verified2 === void 0 || verified2.repo.toLowerCase() !== instance.repo.toLowerCase() || verified2.ref !== row2.branch || verified2.sha !== facts2.headSha)
|
|
74179
74322
|
return json3(409, { ok: false, error: "recovery_facts_mismatch", at });
|
|
74180
|
-
const findingsStep = `${segmentPrefix}/${boundary.index}/findings
|
|
74181
|
-
const findingsPrefix = `${instance.id}:${
|
|
74323
|
+
const findingsStep = `${segmentPrefix}/${boundary.index}/findings`;
|
|
74324
|
+
const findingsPrefix = `${instance.id}:${findingsStep}`;
|
|
74182
74325
|
const headMoved = facts2.headSha !== reviewedHead || expectedHead !== reviewedHead;
|
|
74183
|
-
const
|
|
74184
|
-
|
|
74185
|
-
|
|
74186
|
-
|
|
74187
|
-
|
|
74188
|
-
|
|
74189
|
-
|
|
74190
|
-
|
|
74326
|
+
const failedStep = row2.ending.step;
|
|
74327
|
+
if (row2.ending.cause === "step_threw" && failedStep?.includes("/pr-check") === true) {
|
|
74328
|
+
const checkedStep = failedStep.slice(0, -"/pr-check".length);
|
|
74329
|
+
if (!failedStep.endsWith("/pr-check") || boundary.outcome !== "request_changes" || !isStepAttempt(checkedStep, findingsStep) || row2.ending.round !== boundary.index)
|
|
74330
|
+
return json3(409, { ok: false, error: "recovery_head_moved", at });
|
|
74331
|
+
const attempts = unitListing.runs.filter(
|
|
74332
|
+
(run2) => run2.parentInstanceId === instance.id && isStepAttempt(run2.idempotencyKey, findingsPrefix)
|
|
74333
|
+
);
|
|
74334
|
+
const checkedAttempts = attempts.filter((run2) => run2.idempotencyKey === `${instance.id}:${checkedStep}`);
|
|
74335
|
+
const completed = checkedAttempts.length === 1 ? checkedAttempts[0] : void 0;
|
|
74336
|
+
const reviewFinishedAt = review.finishedAt;
|
|
74337
|
+
if (reviewFinishedAt === void 0 || completed === void 0 || !completed.finished || completed.status !== "completed" || completed.agent !== "coding" || completed.userId !== instance.userId || completed.repo?.toLowerCase() !== instance.repo.toLowerCase() || completed.threadKey !== unitThreadKey || completed.pr !== void 0 && (completed.pr.number !== pr.number || completed.pr.head !== void 0 && completed.pr.head !== row2.branch) || completed.headSha !== facts2.headSha || expectedHead !== reviewedHead && expectedHead !== facts2.headSha || completed.pushed?.some((push) => push.ref !== row2.branch) === true || headMoved && completed.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) !== true || completed.startedAt < reviewFinishedAt || completed.finishedAt === void 0 || completed.finishedAt < completed.startedAt || completed.finishedAt > row2.ending.at || // Inspect every attempt, not just runs matching the current PR head:
|
|
74338
|
+
// any competing success or push remains ambiguous after a later push.
|
|
74339
|
+
attempts.some(
|
|
74340
|
+
(run2) => run2.id !== completed.id && !(run2.finished && (run2.status === "failed" || run2.status === "interrupted") && run2.agent === "coding" && run2.userId === instance.userId && run2.repo?.toLowerCase() === instance.repo.toLowerCase() && run2.threadKey === unitThreadKey && run2.startedAt >= reviewFinishedAt && run2.finishedAt !== void 0 && run2.finishedAt >= run2.startedAt && run2.finishedAt <= completed.startedAt && (run2.pushed?.length ?? 0) === 0)
|
|
74341
|
+
))
|
|
74342
|
+
return json3(409, { ok: false, error: "recovery_head_moved", at });
|
|
74343
|
+
findingsRunId = completed.id;
|
|
74344
|
+
findingsKey = completed.idempotencyKey;
|
|
74191
74345
|
expectedHead = facts2.headSha;
|
|
74192
74346
|
kind = "review";
|
|
74193
74347
|
round2 = boundary.index + 1;
|
|
@@ -74196,7 +74350,10 @@ async function recoverOriginalUnit(body, deps, caller) {
|
|
|
74196
74350
|
lastPush: expectedHead,
|
|
74197
74351
|
publication: { ...publication, expectedHeadSha: expectedHead }
|
|
74198
74352
|
};
|
|
74199
|
-
} else
|
|
74353
|
+
} else {
|
|
74354
|
+
if (headMoved) return json3(409, { ok: false, error: "recovery_head_moved", at });
|
|
74355
|
+
expectedHead = reviewedHead;
|
|
74356
|
+
}
|
|
74200
74357
|
const floor = minutesToMs(leaseMinimum(kind === "findings" ? "fix" : "review"));
|
|
74201
74358
|
if (!Number.isFinite(remainingMs) || remainingMs < floor)
|
|
74202
74359
|
return json3(409, { ok: false, error: "recovery_wall_clock_exhausted", at });
|
|
@@ -75835,7 +75992,7 @@ function createAdminCoordinatorHandler(deps) {
|
|
|
75835
75992
|
})();
|
|
75836
75993
|
};
|
|
75837
75994
|
}
|
|
75838
|
-
var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json3, describe12, isGenerated2, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, githubSecondStart, CHILD_SUPERSESSION_REASONS, PublicationBindingRefusal, samePublicationOwner, fullHead, isStepAttempt, ROUND_OUTCOMES, ENDING_ICON, STEPS;
|
|
75995
|
+
var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json3, describe12, isGenerated2, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, githubSecondStart, CHILD_SUPERSESSION_REASONS, PublicationBindingRefusal, samePublicationOwner, samePublicationBinding, fullHead, isStepAttempt, ROUND_OUTCOMES, ENDING_ICON, STEPS;
|
|
75839
75996
|
var init_adminCoordinator = __esm({
|
|
75840
75997
|
"../../src/channels/adminCoordinator.ts"() {
|
|
75841
75998
|
"use strict";
|
|
@@ -75899,6 +76056,7 @@ var init_adminCoordinator = __esm({
|
|
|
75899
76056
|
reason;
|
|
75900
76057
|
};
|
|
75901
76058
|
samePublicationOwner = (left, right) => left?.instanceId === right.instanceId && left.unit === right.unit;
|
|
76059
|
+
samePublicationBinding = (left, right) => left !== void 0 && left.repo === right.repo && left.pr === right.pr && left.headRef === right.headRef && left.baseRef === right.baseRef && left.expectedHeadSha === right.expectedHeadSha && left.publicationRef === right.publicationRef && samePublicationOwner(left.owner, right.owner);
|
|
75902
76060
|
fullHead = (value) => typeof value === "string" && /^[0-9a-f]{40}$/i.test(value);
|
|
75903
76061
|
isStepAttempt = (key, step) => key === step || key?.startsWith(`${step}/a`) === true && /^[1-9][0-9]*$/.test(key.slice(step.length + 2));
|
|
75904
76062
|
ROUND_OUTCOMES = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coreplane/switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.264.0",
|
|
4
4
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://openswitchboard.dev",
|