@aexhq/sdk 0.40.16 → 0.41.1
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/README.md +25 -25
- package/dist/_contracts/api-key.d.ts +6 -4
- package/dist/_contracts/api-key.js +20 -7
- package/dist/_contracts/connection-ticket.d.ts +5 -5
- package/dist/_contracts/connection-ticket.js +7 -7
- package/dist/_contracts/error-codes.d.ts +1 -1
- package/dist/_contracts/error-codes.js +4 -4
- package/dist/_contracts/error-factory.js +1 -1
- package/dist/_contracts/event-envelope.d.ts +41 -41
- package/dist/_contracts/event-envelope.js +49 -49
- package/dist/_contracts/event-stream-client.d.ts +9 -9
- package/dist/_contracts/event-stream-client.js +5 -5
- package/dist/_contracts/event-view.d.ts +6 -6
- package/dist/_contracts/event-view.js +12 -12
- package/dist/_contracts/http.js +1 -1
- package/dist/_contracts/index.d.ts +10 -10
- package/dist/_contracts/index.js +9 -9
- package/dist/_contracts/models.d.ts +17 -17
- package/dist/_contracts/models.js +14 -14
- package/dist/_contracts/operations.d.ts +79 -82
- package/dist/_contracts/operations.js +233 -243
- package/dist/_contracts/provider-support.d.ts +2 -2
- package/dist/_contracts/runner-event.d.ts +8 -8
- package/dist/_contracts/runner-event.js +9 -9
- package/dist/_contracts/runtime-manifest.d.ts +12 -12
- package/dist/_contracts/runtime-manifest.js +1 -1
- package/dist/_contracts/runtime-sizes.d.ts +12 -12
- package/dist/_contracts/runtime-sizes.js +18 -18
- package/dist/_contracts/runtime-types.d.ts +117 -117
- package/dist/_contracts/runtime-types.js +2 -2
- package/dist/_contracts/sdk-errors.d.ts +3 -3
- package/dist/_contracts/sdk-errors.js +4 -4
- package/dist/_contracts/sdk-secrets.js +10 -10
- package/dist/_contracts/session-artifacts.d.ts +151 -0
- package/dist/_contracts/session-artifacts.js +234 -0
- package/dist/_contracts/{run-config.d.ts → session-config.d.ts} +29 -29
- package/dist/_contracts/{run-config.js → session-config.js} +37 -37
- package/dist/_contracts/session-cost.d.ts +140 -0
- package/dist/_contracts/{run-cost.js → session-cost.js} +94 -94
- package/dist/_contracts/{run-custody.d.ts → session-custody.d.ts} +16 -16
- package/dist/_contracts/{run-custody.js → session-custody.js} +42 -42
- package/dist/_contracts/session-record.d.ts +112 -0
- package/dist/_contracts/{run-record.js → session-record.js} +23 -23
- package/dist/_contracts/session-retention.d.ts +209 -0
- package/dist/_contracts/{run-retention.js → session-retention.js} +116 -116
- package/dist/_contracts/{run-unit.d.ts → session-unit.d.ts} +38 -38
- package/dist/_contracts/{run-unit.js → session-unit.js} +33 -33
- package/dist/_contracts/side-effect-audit.d.ts +14 -14
- package/dist/_contracts/side-effect-audit.js +38 -38
- package/dist/_contracts/sse.d.ts +1 -1
- package/dist/_contracts/sse.js +0 -0
- package/dist/_contracts/stable.d.ts +1 -1
- package/dist/_contracts/stable.js +1 -1
- package/dist/_contracts/status.d.ts +18 -18
- package/dist/_contracts/status.js +19 -19
- package/dist/_contracts/submission.d.ts +101 -99
- package/dist/_contracts/submission.js +96 -96
- package/dist/_contracts/{run-trace.d.ts → turn-trace.d.ts} +1 -1
- package/dist/_contracts/webhook-verify.d.ts +1 -1
- package/dist/_contracts/webhook-verify.js +1 -1
- package/dist/agents-md.d.ts +4 -4
- package/dist/agents-md.js +5 -5
- package/dist/agents-md.js.map +1 -1
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/bundle.js.map +1 -1
- package/dist/cli.mjs +578 -621
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +141 -155
- package/dist/client.js +236 -245
- package/dist/client.js.map +1 -1
- package/dist/fetch-archive.js +3 -3
- package/dist/file.d.ts +7 -7
- package/dist/file.js +8 -8
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts +1 -1
- package/dist/retry.d.ts +1 -1
- package/dist/secret.d.ts +6 -6
- package/dist/secret.js +6 -6
- package/dist/secret.js.map +1 -1
- package/dist/skill.d.ts +2 -2
- package/dist/skill.js +3 -3
- package/dist/skill.js.map +1 -1
- package/dist/tool.js +2 -2
- package/dist/tool.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/authentication.md +11 -11
- package/docs/billing.md +3 -3
- package/docs/cleanup.md +5 -5
- package/docs/concepts/agent-tools.md +5 -5
- package/docs/concepts/composition.md +3 -3
- package/docs/concepts/providers-and-runtimes.md +3 -3
- package/docs/concepts/{runs.md → sessions.md} +8 -8
- package/docs/concepts/subagents.md +16 -16
- package/docs/credentials.md +7 -7
- package/docs/defaults.md +14 -14
- package/docs/errors.md +10 -10
- package/docs/events.md +26 -26
- package/docs/files.md +267 -0
- package/docs/limits-and-quotas.md +35 -35
- package/docs/limits.md +6 -6
- package/docs/mcp.md +3 -3
- package/docs/networking.md +24 -24
- package/docs/provider-runtime-capabilities.md +1 -1
- package/docs/public-surface.json +6 -6
- package/docs/quickstart.md +13 -13
- package/docs/release.md +5 -5
- package/docs/retries.md +3 -3
- package/docs/secrets.md +16 -16
- package/docs/session-config.md +58 -0
- package/docs/{run-record.md → session-record.md} +16 -16
- package/docs/skills.md +9 -9
- package/docs/vision-skills.md +7 -7
- package/docs/webhooks.md +11 -11
- package/examples/feature-tour.ts +17 -17
- package/examples/spike-settle-latency.ts +11 -11
- package/package.json +1 -1
- package/dist/_contracts/run-artifacts.d.ts +0 -49
- package/dist/_contracts/run-artifacts.js +0 -102
- package/dist/_contracts/run-cost.d.ts +0 -140
- package/dist/_contracts/run-record.d.ts +0 -116
- package/dist/_contracts/run-retention.d.ts +0 -209
- package/docs/outputs.md +0 -267
- package/docs/run-config.md +0 -58
- /package/dist/_contracts/{run-trace.js → turn-trace.js} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { strToU8, zipSync } from "fflate";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import { AexNetworkError,
|
|
5
|
-
import {
|
|
3
|
+
import { normalizeSessionUnit } from "./session-unit.js";
|
|
4
|
+
import { AexNetworkError, SessionConfigValidationError, SessionStateError } from "./sdk-errors.js";
|
|
5
|
+
import { assertSessionRecordArchivePublicSafeV1, buildSessionRecordDownloadManifestV1 } from "./session-record.js";
|
|
6
6
|
/**
|
|
7
7
|
* The single source of truth for SDK<->BFF transport. The SDK class
|
|
8
8
|
* AND the CLI subcommands both call these functions; neither
|
|
@@ -14,38 +14,38 @@ import { assertRunRecordArchivePublicSafeV1, buildRunRecordDownloadManifestV1 }
|
|
|
14
14
|
* Workspace identity is derived server-side from the API key on
|
|
15
15
|
* every request; callers do not pass `workspaceId`.
|
|
16
16
|
*/
|
|
17
|
-
export async function
|
|
18
|
-
const result = await http.request(`/api/
|
|
19
|
-
return
|
|
17
|
+
export async function getSessionRecord(http, sessionId) {
|
|
18
|
+
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}`);
|
|
19
|
+
return hasSessionRecord(result) ? result.session : result;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Strongly-typed accessor for the full self-contained
|
|
22
|
+
* Strongly-typed accessor for the full self-contained session unit:
|
|
23
23
|
* parsed submission inputs, attempts, indexed events (with
|
|
24
|
-
* pagination cursor for large
|
|
25
|
-
*
|
|
24
|
+
* pagination cursor for large sessions), raw-event Storage manifest,
|
|
25
|
+
* files, capture failures, and the proxy-call audit.
|
|
26
26
|
*
|
|
27
|
-
* Backed by the same `GET /api/
|
|
28
|
-
* `
|
|
29
|
-
* the documented wire shape. Prefer this for new code; `
|
|
27
|
+
* Backed by the same `GET /api/sessions/:sessionId` endpoint that
|
|
28
|
+
* `getSessionRecord` calls; this variant just narrows the return type to
|
|
29
|
+
* the documented wire shape. Prefer this for new code; `getSessionRecord`
|
|
30
30
|
* stays for callers that only need the loose record.
|
|
31
31
|
*/
|
|
32
|
-
export async function
|
|
33
|
-
// Normalize so the
|
|
32
|
+
export async function getSessionUnit(http, sessionId) {
|
|
33
|
+
// Normalize so the SessionUnit type contract holds at runtime: the managed plane
|
|
34
34
|
// returns a lean record and omits the aggregate collections (F25). The
|
|
35
|
-
// aggregates default to empty (safe array/page access) — read
|
|
36
|
-
// for the authoritative per-
|
|
37
|
-
return
|
|
35
|
+
// aggregates default to empty (safe array/page access) — read files()/events()
|
|
36
|
+
// for the authoritative per-session data on that plane.
|
|
37
|
+
return normalizeSessionUnit(await http.request(`/api/sessions/${encodeURIComponent(sessionId)}`));
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* List the
|
|
41
|
-
* Backed by `GET /api/
|
|
42
|
-
* the
|
|
40
|
+
* List the sessions in the token's workspace, most-recent first, one page at a time.
|
|
41
|
+
* Backed by `GET /api/sessions` (workspace-token gated; the bare collection path, NOT
|
|
42
|
+
* the session-keyed `GET /api/sessions/:sessionId`). The server clamps `limit` to [1, 100] and
|
|
43
43
|
* returns an opaque `nextCursor` for the next page (absent on the last page).
|
|
44
44
|
*
|
|
45
|
-
* Returns public-safe {@link
|
|
46
|
-
* For a single page; callers wanting every
|
|
45
|
+
* Returns public-safe {@link SessionRecordSummary} rows only — never the submission snapshot.
|
|
46
|
+
* For a single page; callers wanting every session loop on `nextCursor` themselves.
|
|
47
47
|
*/
|
|
48
|
-
export async function
|
|
48
|
+
export async function listSessionRecords(http, query) {
|
|
49
49
|
const params = {};
|
|
50
50
|
if (query?.status !== undefined)
|
|
51
51
|
params.status = query.status;
|
|
@@ -55,39 +55,39 @@ export async function listRuns(http, query) {
|
|
|
55
55
|
params.limit = String(query.limit);
|
|
56
56
|
if (query?.cursor !== undefined)
|
|
57
57
|
params.cursor = query.cursor;
|
|
58
|
-
const page = await http.request("/api/
|
|
59
|
-
// Defensive contract enforcement: some deployed planes leak non-
|
|
60
|
-
// rows (settle-time ledger/spendmark items) into the
|
|
58
|
+
const page = await http.request("/api/sessions", {}, params);
|
|
59
|
+
// Defensive contract enforcement: some deployed planes leak non-session marker
|
|
60
|
+
// rows (settle-time ledger/spendmark items) into the session-list index. Those
|
|
61
61
|
// phantoms carry only { id, createdAt } and would surface as duplicate,
|
|
62
|
-
// status-less
|
|
63
|
-
//
|
|
62
|
+
// status-less SessionRecordSummary entries. Drop anything missing the fields
|
|
63
|
+
// SessionRecordSummary declares required, so callers can trust the published type.
|
|
64
64
|
// The same enforcement covers `costUsd`: deployed planes serve `null` for
|
|
65
|
-
//
|
|
65
|
+
// sessions with no settled telemetry, but SessionRecordSummary declares `costUsd?: number`
|
|
66
66
|
// — normalize `null` to absent so typed callers never see it.
|
|
67
67
|
let changed = false;
|
|
68
|
-
const
|
|
69
|
-
for (const
|
|
70
|
-
if (typeof
|
|
71
|
-
typeof
|
|
72
|
-
typeof
|
|
73
|
-
typeof
|
|
68
|
+
const sessions = [];
|
|
69
|
+
for (const session of page.sessions) {
|
|
70
|
+
if (typeof session.id !== "string" ||
|
|
71
|
+
typeof session.status !== "string" ||
|
|
72
|
+
typeof session.createdAt !== "string" ||
|
|
73
|
+
typeof session.updatedAt !== "string") {
|
|
74
74
|
changed = true;
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
|
-
if (typeof
|
|
78
|
-
const { costUsd: _dropped, ...rest } =
|
|
79
|
-
|
|
77
|
+
if (typeof session.costUsd !== "number" && session.costUsd !== undefined) {
|
|
78
|
+
const { costUsd: _dropped, ...rest } = session;
|
|
79
|
+
sessions.push(rest);
|
|
80
80
|
changed = true;
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
sessions.push(session);
|
|
84
84
|
}
|
|
85
|
-
return changed ? { ...page,
|
|
85
|
+
return changed ? { ...page, sessions } : page;
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Resolve a caller-supplied idempotency key to the value that ships on the
|
|
89
89
|
* request. FAIL-FAST: an empty or whitespace-only key THROWS
|
|
90
|
-
* {@link
|
|
90
|
+
* {@link SessionConfigValidationError} — a footgun that silently disabled dedup
|
|
91
91
|
* (`?? generate()` kept `''`, then a downstream truthy header-drop shipped no
|
|
92
92
|
* `Idempotency-Key`). An absent key generates a fresh one; a real key is
|
|
93
93
|
* returned verbatim. The single choke point every send/create/run entry uses.
|
|
@@ -97,7 +97,7 @@ export function resolveIdempotencyKey(key) {
|
|
|
97
97
|
return `aex-idem-${randomUUID()}`;
|
|
98
98
|
}
|
|
99
99
|
if (typeof key !== "string" || key.trim().length === 0) {
|
|
100
|
-
throw new
|
|
100
|
+
throw new SessionConfigValidationError("idempotencyKey must be a non-empty, non-whitespace string", {
|
|
101
101
|
field: "idempotencyKey",
|
|
102
102
|
value: key
|
|
103
103
|
});
|
|
@@ -113,7 +113,7 @@ function idempotencyHeaders(options) {
|
|
|
113
113
|
if (options?.idempotencyKey === undefined)
|
|
114
114
|
return undefined;
|
|
115
115
|
if (typeof options.idempotencyKey !== "string" || options.idempotencyKey.trim().length === 0) {
|
|
116
|
-
throw new
|
|
116
|
+
throw new SessionConfigValidationError("idempotencyKey must be a non-empty, non-whitespace string", {
|
|
117
117
|
field: "idempotencyKey",
|
|
118
118
|
value: options.idempotencyKey
|
|
119
119
|
});
|
|
@@ -131,8 +131,8 @@ export async function createSession(http, request, options) {
|
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
133
|
* Fire-and-forget submit — create the session and post its first turn WITHOUT
|
|
134
|
-
* awaiting the turn to settle (the honest counterpart to await-settle `
|
|
135
|
-
* Returns the `
|
|
134
|
+
* awaiting the turn to settle (the honest counterpart to await-settle `start()`).
|
|
135
|
+
* Returns the `sessionId` immediately; observe the session via a `webhook`, the event
|
|
136
136
|
* stream, or by re-opening the session. Mirrors {@link createSession}'s
|
|
137
137
|
* idempotency handling.
|
|
138
138
|
*/
|
|
@@ -147,13 +147,13 @@ export async function submit(http, request, options) {
|
|
|
147
147
|
const sessionId = created.sessionId ?? created.id;
|
|
148
148
|
const accepted = await sendSessionMessage(http, sessionId, { input }, { idempotencyKey: messageKey });
|
|
149
149
|
const session = accepted.session;
|
|
150
|
-
return {
|
|
150
|
+
return { sessionId: session.sessionId ?? session.id, session };
|
|
151
151
|
}
|
|
152
152
|
function assertSubmitInput(input) {
|
|
153
153
|
const ok = (typeof input === "string" && input.length > 0) ||
|
|
154
154
|
(Array.isArray(input) && input.length > 0 && input.every((segment) => typeof segment === "string" && segment.length > 0));
|
|
155
155
|
if (!ok) {
|
|
156
|
-
throw new
|
|
156
|
+
throw new SessionConfigValidationError("submit: request.input must be a non-empty string or string array", {
|
|
157
157
|
field: "input",
|
|
158
158
|
value: input
|
|
159
159
|
});
|
|
@@ -242,25 +242,25 @@ export async function listSessionEvents(http, sessionId) {
|
|
|
242
242
|
}
|
|
243
243
|
return all;
|
|
244
244
|
}
|
|
245
|
-
export async function
|
|
246
|
-
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/
|
|
247
|
-
return query === undefined ? result.
|
|
245
|
+
export async function listSessionFiles(http, sessionId, query) {
|
|
246
|
+
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/files`);
|
|
247
|
+
return query === undefined ? result.files : filterSessionFiles(result.files, query);
|
|
248
248
|
}
|
|
249
249
|
export async function getSessionCoordinatorTicket(http, sessionId) {
|
|
250
250
|
return http.request(`/api/sessions/${encodeURIComponent(sessionId)}/events/ticket`, { method: "POST" });
|
|
251
251
|
}
|
|
252
252
|
// Bound the transparent pager: the read route caps each page at 1000, so this
|
|
253
|
-
// admits up to ~1e6 events before bailing — past any real
|
|
253
|
+
// admits up to ~1e6 events before bailing — past any real session, but bounded so a
|
|
254
254
|
// server that never clears `nextCursor` can't loop forever.
|
|
255
255
|
const LIST_EVENTS_PAGE_BUDGET = 1000;
|
|
256
256
|
/**
|
|
257
|
-
* List a
|
|
257
|
+
* List a session's events. The read endpoint is PAGED (bounded per response so a
|
|
258
258
|
* long run can't return an unbounded body); this follows `nextCursor` across
|
|
259
259
|
* pages and returns the FULL accumulated list, preserving the prior single-call
|
|
260
260
|
* contract for callers (download/*, CLI, streamEvents polling).
|
|
261
261
|
*/
|
|
262
|
-
export async function
|
|
263
|
-
const path = `/api/
|
|
262
|
+
export async function listSessionRecordEvents(http, sessionId) {
|
|
263
|
+
const path = `/api/sessions/${encodeURIComponent(sessionId)}/events`;
|
|
264
264
|
const all = [];
|
|
265
265
|
let cursor;
|
|
266
266
|
for (let page = 0; page < LIST_EVENTS_PAGE_BUDGET; page++) {
|
|
@@ -275,35 +275,31 @@ export async function listRunEvents(http, runId) {
|
|
|
275
275
|
}
|
|
276
276
|
/**
|
|
277
277
|
* Mint a short-lived coordinator WS ticket via the workspace-token-gated
|
|
278
|
-
* broker (`/api/
|
|
278
|
+
* broker (`/api/sessions/:id/events/ticket`). The returned `wsUrl` + `ticket`
|
|
279
279
|
* open the live event stream directly against the coordinator. Throws if no
|
|
280
280
|
* coordinator is configured for the deployment (HTTP 503).
|
|
281
281
|
*/
|
|
282
|
-
export async function getCoordinatorTicket(http,
|
|
283
|
-
return http.request(`/api/
|
|
284
|
-
}
|
|
285
|
-
export async function listOutputs(http, runId, query) {
|
|
286
|
-
const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/outputs`);
|
|
287
|
-
return query === undefined ? result.outputs : filterOutputs(result.outputs, query);
|
|
282
|
+
export async function getCoordinatorTicket(http, sessionId) {
|
|
283
|
+
return http.request(`/api/sessions/${encodeURIComponent(sessionId)}/events/ticket`, { method: "POST" });
|
|
288
284
|
}
|
|
289
|
-
export async function
|
|
290
|
-
return
|
|
285
|
+
export async function findSessionFiles(http, sessionId, query) {
|
|
286
|
+
return listSessionFiles(http, sessionId, query);
|
|
291
287
|
}
|
|
292
|
-
export async function
|
|
293
|
-
const matches = await
|
|
288
|
+
export async function findSessionFile(http, sessionId, query) {
|
|
289
|
+
const matches = await findSessionFiles(http, sessionId, query);
|
|
294
290
|
if (matches.length === 0)
|
|
295
291
|
return null;
|
|
296
292
|
if (matches.length === 1)
|
|
297
293
|
return matches[0];
|
|
298
|
-
throw new
|
|
299
|
-
|
|
300
|
-
matches: matches.map((
|
|
294
|
+
throw new SessionStateError("files.findOne: file query matched multiple files", {
|
|
295
|
+
sessionId,
|
|
296
|
+
matches: matches.map((file) => file.filename ?? file.id)
|
|
301
297
|
});
|
|
302
298
|
}
|
|
303
|
-
export async function
|
|
304
|
-
const
|
|
305
|
-
const expiresInSeconds =
|
|
306
|
-
const result = await http.request(`/api/
|
|
299
|
+
export async function sessionFileLink(http, sessionId, selectorOrQuery, options) {
|
|
300
|
+
const file = await resolveSessionFileLinkTarget(http, sessionId, selectorOrQuery);
|
|
301
|
+
const expiresInSeconds = normalizeSessionFileLinkExpiresIn(options?.expiresIn);
|
|
302
|
+
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(file.id)}/link`, {
|
|
307
303
|
method: "POST",
|
|
308
304
|
body: JSON.stringify({ expiresInSeconds })
|
|
309
305
|
});
|
|
@@ -312,7 +308,7 @@ export async function outputLink(http, runId, selectorOrQuery, options) {
|
|
|
312
308
|
...result,
|
|
313
309
|
expiresInSeconds: effectiveExpiresIn,
|
|
314
310
|
expiresAt: result.expiresAt ?? syntheticExpiresAt(effectiveExpiresIn),
|
|
315
|
-
|
|
311
|
+
file: result.file ?? file
|
|
316
312
|
};
|
|
317
313
|
}
|
|
318
314
|
/**
|
|
@@ -323,12 +319,12 @@ export async function outputLink(http, runId, selectorOrQuery, options) {
|
|
|
323
319
|
function syntheticExpiresAt(expiresInSeconds) {
|
|
324
320
|
return new Date(Date.now() + expiresInSeconds * 1000).toISOString();
|
|
325
321
|
}
|
|
326
|
-
export async function
|
|
327
|
-
return
|
|
322
|
+
export async function createSessionFileLink(http, sessionId, selectorOrQuery, options) {
|
|
323
|
+
return sessionFileLink(http, sessionId, selectorOrQuery, options);
|
|
328
324
|
}
|
|
329
|
-
export async function eventArchiveLink(http,
|
|
330
|
-
const expiresInSeconds =
|
|
331
|
-
const result = await http.request(`/api/
|
|
325
|
+
export async function eventArchiveLink(http, sessionId, options) {
|
|
326
|
+
const expiresInSeconds = normalizeSessionFileLinkExpiresIn(options?.expiresIn);
|
|
327
|
+
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/events/link`, {
|
|
332
328
|
method: "POST",
|
|
333
329
|
body: JSON.stringify({ expiresInSeconds })
|
|
334
330
|
});
|
|
@@ -339,16 +335,16 @@ export async function eventArchiveLink(http, runId, options) {
|
|
|
339
335
|
expiresAt: result.expiresAt ?? syntheticExpiresAt(effectiveExpiresIn)
|
|
340
336
|
};
|
|
341
337
|
}
|
|
342
|
-
export function
|
|
338
|
+
export function resolveSessionFileSelector(files, selector, sessionId) {
|
|
343
339
|
if (isPathSelector(selector)) {
|
|
344
|
-
const target =
|
|
340
|
+
const target = normalizeSessionFileLookupPath(selector.path);
|
|
345
341
|
if (!target) {
|
|
346
|
-
throw new
|
|
342
|
+
throw new SessionStateError("files.download: file path must be non-empty", { sessionId, path: selector.path });
|
|
347
343
|
}
|
|
348
|
-
const matches =
|
|
349
|
-
if (typeof
|
|
344
|
+
const matches = files.filter((file) => {
|
|
345
|
+
if (typeof file.filename !== "string")
|
|
350
346
|
return false;
|
|
351
|
-
const filename =
|
|
347
|
+
const filename = normalizeSessionFileLookupPath(file.filename);
|
|
352
348
|
if (selector.match === "suffix") {
|
|
353
349
|
return filename === target || filename.endsWith(`/${target}`);
|
|
354
350
|
}
|
|
@@ -357,80 +353,80 @@ export function resolveOutputFileSelector(outputs, selector, runId) {
|
|
|
357
353
|
if (matches.length === 1)
|
|
358
354
|
return matches[0];
|
|
359
355
|
if (matches.length > 1) {
|
|
360
|
-
throw new
|
|
356
|
+
throw new SessionStateError(`files.download: file path "${selector.path}" matched multiple files`, { sessionId, path: selector.path, matches: matches.map((file) => file.filename ?? file.id) });
|
|
361
357
|
}
|
|
362
|
-
throw new
|
|
363
|
-
|
|
358
|
+
throw new SessionStateError(`files.download: file path "${selector.path}" was not found`, {
|
|
359
|
+
sessionId,
|
|
364
360
|
path: selector.path
|
|
365
361
|
});
|
|
366
362
|
}
|
|
367
363
|
if (typeof selector?.id !== "string" || selector.id.length === 0) {
|
|
368
|
-
throw new
|
|
364
|
+
throw new SessionStateError("files.download: selector must include a file id or path", { sessionId });
|
|
369
365
|
}
|
|
370
366
|
return { ...selector, id: selector.id };
|
|
371
367
|
}
|
|
372
|
-
export async function
|
|
373
|
-
const
|
|
374
|
-
?
|
|
375
|
-
:
|
|
376
|
-
const timeoutMs =
|
|
377
|
-
const path = `/api/
|
|
378
|
-
return {
|
|
379
|
-
}
|
|
380
|
-
/** Byte ceiling for {@link
|
|
381
|
-
export const
|
|
382
|
-
/** Default `maxBytes` for {@link
|
|
383
|
-
export const
|
|
384
|
-
/** Default per-attempt timeout while fetching or reading one
|
|
385
|
-
export const
|
|
386
|
-
/** Idempotent
|
|
387
|
-
export const
|
|
368
|
+
export async function downloadSessionFile(http, sessionId, selector, options) {
|
|
369
|
+
const file = isPathSelector(selector)
|
|
370
|
+
? resolveSessionFileSelector(await listSessionFiles(http, sessionId), selector, sessionId)
|
|
371
|
+
: resolveSessionFileSelector([], selector, sessionId);
|
|
372
|
+
const timeoutMs = normalizeSessionFileTransferTimeoutMs(options?.timeoutMs);
|
|
373
|
+
const path = `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(file.id)}/download`;
|
|
374
|
+
return { file, bytes: await downloadSessionFileBytesWithRetry(http, path, timeoutMs) };
|
|
375
|
+
}
|
|
376
|
+
/** Byte ceiling for {@link readSessionFileText} — a hard cap even if a caller asks for more. */
|
|
377
|
+
export const READ_SESSION_FILE_TEXT_MAX_BYTES = 10_000_000;
|
|
378
|
+
/** Default `maxBytes` for {@link readSessionFileText} — a chat-sized preview. */
|
|
379
|
+
export const READ_SESSION_FILE_TEXT_DEFAULT_BYTES = 50_000;
|
|
380
|
+
/** Default per-attempt timeout while fetching or reading one session file body. */
|
|
381
|
+
export const SESSION_FILE_TRANSFER_DEFAULT_TIMEOUT_MS = 30_000;
|
|
382
|
+
/** Idempotent file GETs retry once on a transfer timeout. */
|
|
383
|
+
export const SESSION_FILE_TRANSFER_ATTEMPTS = 2;
|
|
388
384
|
/**
|
|
389
|
-
* Read ONE
|
|
385
|
+
* Read ONE session file as byte-capped, decoded UTF-8 text. Built for handing a session
|
|
390
386
|
* deliverable to an LLM tool: it streams the file body and STOPS at `maxBytes`, so
|
|
391
387
|
* a 200 MB artifact never fully buffers in memory or context. `truncated` is true
|
|
392
388
|
* when the file is larger than the cap. Optionally `grep` keeps only matching lines.
|
|
393
389
|
*
|
|
394
|
-
* Selector is the same `{ path }` / `{ id }` shape as `
|
|
395
|
-
* selector lists the
|
|
390
|
+
* Selector is the same `{ path }` / `{ id }` shape as `downloadSessionFile`. A path
|
|
391
|
+
* selector lists the session's files to resolve the id; an id selector skips that.
|
|
396
392
|
*/
|
|
397
|
-
export async function
|
|
398
|
-
const maxBytes = Math.max(1, Math.min(options?.maxBytes ??
|
|
399
|
-
const
|
|
400
|
-
?
|
|
401
|
-
:
|
|
402
|
-
const timeoutMs =
|
|
403
|
-
const path = `/api/
|
|
404
|
-
const capped = await
|
|
393
|
+
export async function readSessionFileText(http, sessionId, selector, options) {
|
|
394
|
+
const maxBytes = Math.max(1, Math.min(options?.maxBytes ?? READ_SESSION_FILE_TEXT_DEFAULT_BYTES, READ_SESSION_FILE_TEXT_MAX_BYTES));
|
|
395
|
+
const file = isPathSelector(selector)
|
|
396
|
+
? resolveSessionFileSelector(await listSessionFiles(http, sessionId), selector, sessionId)
|
|
397
|
+
: resolveSessionFileSelector([], selector, sessionId);
|
|
398
|
+
const timeoutMs = normalizeSessionFileTransferTimeoutMs(options?.timeoutMs);
|
|
399
|
+
const path = `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(file.id)}/download`;
|
|
400
|
+
const capped = await readSessionFileTextWithRetry(http, path, maxBytes, timeoutMs);
|
|
405
401
|
const text = options?.grep === undefined ? capped.text : grepLines(capped.text, options.grep);
|
|
406
|
-
return {
|
|
402
|
+
return { file, text, truncated: capped.truncated, totalBytes: capped.totalBytes };
|
|
407
403
|
}
|
|
408
|
-
async function
|
|
409
|
-
return
|
|
410
|
-
const response = await
|
|
404
|
+
async function downloadSessionFileBytesWithRetry(http, path, timeoutMs) {
|
|
405
|
+
return sessionFileTransferWithRetry(path, timeoutMs, async () => {
|
|
406
|
+
const response = await downloadSessionFileResponse(http, path, timeoutMs);
|
|
411
407
|
return readResponseBytes(response, timeoutMs);
|
|
412
408
|
});
|
|
413
409
|
}
|
|
414
|
-
async function
|
|
415
|
-
return
|
|
416
|
-
const response = await
|
|
410
|
+
async function readSessionFileTextWithRetry(http, path, maxBytes, timeoutMs) {
|
|
411
|
+
return sessionFileTransferWithRetry(path, timeoutMs, async () => {
|
|
412
|
+
const response = await downloadSessionFileResponse(http, path, timeoutMs);
|
|
417
413
|
return readCappedText(response, maxBytes, timeoutMs);
|
|
418
414
|
});
|
|
419
415
|
}
|
|
420
|
-
async function
|
|
416
|
+
async function downloadSessionFileResponse(http, path, timeoutMs) {
|
|
421
417
|
const controller = new AbortController();
|
|
422
|
-
const { response } = await
|
|
418
|
+
const { response } = await withSessionFileTransferTimeout(http.download(path, { signal: controller.signal }), timeoutMs, () => controller.abort(), "download-open");
|
|
423
419
|
return response;
|
|
424
420
|
}
|
|
425
|
-
async function
|
|
421
|
+
async function sessionFileTransferWithRetry(path, timeoutMs, action) {
|
|
426
422
|
const startedMs = Date.now();
|
|
427
423
|
let lastTimeout;
|
|
428
|
-
for (let attempt = 1; attempt <=
|
|
424
|
+
for (let attempt = 1; attempt <= SESSION_FILE_TRANSFER_ATTEMPTS; attempt += 1) {
|
|
429
425
|
try {
|
|
430
426
|
return await action();
|
|
431
427
|
}
|
|
432
428
|
catch (err) {
|
|
433
|
-
if (!(err instanceof
|
|
429
|
+
if (!(err instanceof SessionFileTransferTimeoutError))
|
|
434
430
|
throw err;
|
|
435
431
|
lastTimeout = err;
|
|
436
432
|
}
|
|
@@ -439,37 +435,37 @@ async function outputTransferWithRetry(path, timeoutMs, action) {
|
|
|
439
435
|
method: "GET",
|
|
440
436
|
host: "",
|
|
441
437
|
path,
|
|
442
|
-
cause: lastTimeout ?? new
|
|
443
|
-
attempts:
|
|
438
|
+
cause: lastTimeout ?? new SessionFileTransferTimeoutError("unknown", timeoutMs),
|
|
439
|
+
attempts: SESSION_FILE_TRANSFER_ATTEMPTS,
|
|
444
440
|
elapsedMs: Date.now() - startedMs
|
|
445
441
|
});
|
|
446
442
|
}
|
|
447
|
-
function
|
|
443
|
+
function normalizeSessionFileTransferTimeoutMs(value) {
|
|
448
444
|
if (value === undefined)
|
|
449
|
-
return
|
|
445
|
+
return SESSION_FILE_TRANSFER_DEFAULT_TIMEOUT_MS;
|
|
450
446
|
if (!Number.isFinite(value) || value <= 0) {
|
|
451
|
-
throw new
|
|
447
|
+
throw new SessionConfigValidationError("files.download: timeoutMs must be a positive finite number", {
|
|
452
448
|
timeoutMs: value
|
|
453
449
|
});
|
|
454
450
|
}
|
|
455
451
|
return Math.max(1, Math.floor(value));
|
|
456
452
|
}
|
|
457
|
-
class
|
|
453
|
+
class SessionFileTransferTimeoutError extends Error {
|
|
458
454
|
code = "ETIMEDOUT";
|
|
459
455
|
phase;
|
|
460
456
|
constructor(phase, timeoutMs) {
|
|
461
|
-
super(`
|
|
462
|
-
this.name = "
|
|
457
|
+
super(`file transfer phase=${phase} timed out after ${timeoutMs}ms`);
|
|
458
|
+
this.name = "SessionFileTransferTimeoutError";
|
|
463
459
|
this.phase = phase;
|
|
464
460
|
}
|
|
465
461
|
}
|
|
466
|
-
async function
|
|
462
|
+
async function withSessionFileTransferTimeout(promise, timeoutMs, abort, phase) {
|
|
467
463
|
let timedOut = false;
|
|
468
464
|
let timeout;
|
|
469
465
|
const timeoutPromise = new Promise((_, reject) => {
|
|
470
466
|
timeout = setTimeout(() => {
|
|
471
467
|
timedOut = true;
|
|
472
|
-
reject(new
|
|
468
|
+
reject(new SessionFileTransferTimeoutError(phase, timeoutMs));
|
|
473
469
|
queueMicrotask(() => {
|
|
474
470
|
try {
|
|
475
471
|
abort();
|
|
@@ -485,7 +481,7 @@ async function withOutputTransferTimeout(promise, timeoutMs, abort, phase) {
|
|
|
485
481
|
}
|
|
486
482
|
catch (err) {
|
|
487
483
|
if (timedOut && isAbortLikeError(err))
|
|
488
|
-
throw new
|
|
484
|
+
throw new SessionFileTransferTimeoutError(phase, timeoutMs);
|
|
489
485
|
throw err;
|
|
490
486
|
}
|
|
491
487
|
finally {
|
|
@@ -500,14 +496,14 @@ function isAbortLikeError(err) {
|
|
|
500
496
|
async function readResponseBytes(response, timeoutMs) {
|
|
501
497
|
const body = response.body;
|
|
502
498
|
if (!body) {
|
|
503
|
-
const buffer = await
|
|
499
|
+
const buffer = await withSessionFileTransferTimeout(response.arrayBuffer(), timeoutMs, () => { }, "body-read");
|
|
504
500
|
return new Uint8Array(buffer);
|
|
505
501
|
}
|
|
506
502
|
const reader = body.getReader();
|
|
507
503
|
const chunks = [];
|
|
508
504
|
try {
|
|
509
505
|
while (true) {
|
|
510
|
-
const { done, value } = await
|
|
506
|
+
const { done, value } = await withSessionFileTransferTimeout(reader.read(), timeoutMs, () => {
|
|
511
507
|
void reader.cancel().catch(() => { });
|
|
512
508
|
}, "body-read");
|
|
513
509
|
if (done)
|
|
@@ -534,7 +530,7 @@ async function readCappedText(response, maxBytes, timeoutMs) {
|
|
|
534
530
|
const body = response.body;
|
|
535
531
|
if (!body) {
|
|
536
532
|
// No streaming body (some fetch polyfills) — buffer, then slice to the cap.
|
|
537
|
-
const buf = new Uint8Array(await
|
|
533
|
+
const buf = new Uint8Array(await withSessionFileTransferTimeout(response.arrayBuffer(), timeoutMs, () => { }, "body-read"));
|
|
538
534
|
const total = declared ?? buf.byteLength;
|
|
539
535
|
return {
|
|
540
536
|
text: decoder.decode(buf.subarray(0, maxBytes)),
|
|
@@ -548,7 +544,7 @@ async function readCappedText(response, maxBytes, timeoutMs) {
|
|
|
548
544
|
let sawMore = false;
|
|
549
545
|
try {
|
|
550
546
|
while (read < maxBytes) {
|
|
551
|
-
const { done, value } = await
|
|
547
|
+
const { done, value } = await withSessionFileTransferTimeout(reader.read(), timeoutMs, () => {
|
|
552
548
|
void reader.cancel().catch(() => { });
|
|
553
549
|
}, "body-read");
|
|
554
550
|
if (done)
|
|
@@ -560,7 +556,7 @@ async function readCappedText(response, maxBytes, timeoutMs) {
|
|
|
560
556
|
}
|
|
561
557
|
if (read >= maxBytes) {
|
|
562
558
|
// We hit the cap; peek once more to learn whether bytes remain, then stop.
|
|
563
|
-
const next = await
|
|
559
|
+
const next = await withSessionFileTransferTimeout(reader.read(), timeoutMs, () => {
|
|
564
560
|
void reader.cancel().catch(() => { });
|
|
565
561
|
}, "body-read");
|
|
566
562
|
if (!next.done && next.value && next.value.byteLength > 0)
|
|
@@ -597,47 +593,41 @@ function grepLines(text, pattern) {
|
|
|
597
593
|
.join("\n");
|
|
598
594
|
}
|
|
599
595
|
/**
|
|
600
|
-
* List a
|
|
601
|
-
* {@link
|
|
602
|
-
* events /
|
|
603
|
-
* empty array means the
|
|
596
|
+
* List a session's subagent child sessions (`GET /sessions/:id/children`). Each row is a
|
|
597
|
+
* {@link ChildSessionRef} whose `id` resolves through the session record facade (getSessionRecord /
|
|
598
|
+
* events / files) — so every child the platform hands you is resolvable. An
|
|
599
|
+
* empty array means the session spawned no children.
|
|
604
600
|
*/
|
|
605
|
-
export async function
|
|
606
|
-
const result = await http.request(`/api/
|
|
601
|
+
export async function listSessionChildren(http, sessionId) {
|
|
602
|
+
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/children`);
|
|
607
603
|
return Array.isArray(result)
|
|
608
604
|
? result
|
|
609
605
|
: result.children;
|
|
610
606
|
}
|
|
611
|
-
export async function cancelRun(http, runId) {
|
|
612
|
-
await http.request(`/api/runs/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
|
|
613
|
-
}
|
|
614
|
-
export async function deleteRun(http, runId) {
|
|
615
|
-
await http.request(`/api/runs/${encodeURIComponent(runId)}`, { method: "DELETE" });
|
|
616
|
-
}
|
|
617
607
|
/**
|
|
618
|
-
* List a
|
|
619
|
-
* the rows surfaced by `GET /api/
|
|
620
|
-
* means the
|
|
608
|
+
* List a session's webhook delivery attempts (the per-session delivery ledger). Returns
|
|
609
|
+
* the rows surfaced by `GET /api/sessions/:id/webhook-deliveries`; an empty array
|
|
610
|
+
* means the session carried no `webhook` or has not reached a terminal state yet.
|
|
621
611
|
*/
|
|
622
|
-
export async function
|
|
623
|
-
const result = await http.request(`/api/
|
|
612
|
+
export async function getSessionWebhookDeliveries(http, sessionId) {
|
|
613
|
+
const result = await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/webhook-deliveries`);
|
|
624
614
|
return Array.isArray(result)
|
|
625
615
|
? result
|
|
626
616
|
: result.deliveries;
|
|
627
617
|
}
|
|
628
618
|
/**
|
|
629
|
-
* Manually re-trigger a
|
|
619
|
+
* Manually re-trigger a session's webhook delivery: resets the row to `pending` and
|
|
630
620
|
* re-sends the frozen payload with the SAME `webhook-id` so the consumer
|
|
631
621
|
* dedupes. Idempotent from the caller's view.
|
|
632
622
|
*/
|
|
633
|
-
export async function
|
|
634
|
-
await http.request(`/api/
|
|
623
|
+
export async function redeliverSessionWebhook(http, sessionId, deliveryId) {
|
|
624
|
+
await http.request(`/api/sessions/${encodeURIComponent(sessionId)}/webhook-deliveries/${encodeURIComponent(deliveryId)}/redeliver`, { method: "POST" });
|
|
635
625
|
}
|
|
636
626
|
/**
|
|
637
627
|
* Delete a workspace asset cache entry. Accepts an `asset_<id>` value,
|
|
638
628
|
* `sha256:<hex>`, or a bare 64-hex digest. Workspace is derived server-side
|
|
639
629
|
* from the token; idempotent.
|
|
640
|
-
* Does NOT affect
|
|
630
|
+
* Does NOT affect sessions that already snapshotted the asset.
|
|
641
631
|
*/
|
|
642
632
|
export async function deleteWorkspaceAsset(http, hash) {
|
|
643
633
|
const assetId = hash.startsWith("asset_")
|
|
@@ -698,23 +688,23 @@ export async function getWebhookSigningSecret(http) {
|
|
|
698
688
|
}
|
|
699
689
|
/**
|
|
700
690
|
* Download each artifact's bytes into a zip-file map keyed by
|
|
701
|
-
* `<zipPrefix><relative-path>`, fetched from the `
|
|
691
|
+
* `<zipPrefix><relative-path>`, fetched from the `files`
|
|
702
692
|
* download route. Best-effort: a per-artifact fetch failure records an
|
|
703
693
|
* `errors[]` entry rather than aborting the rest, so the failure is
|
|
704
694
|
* surfaced (never silent) while a partially-available run still yields a
|
|
705
695
|
* usable zip.
|
|
706
696
|
*/
|
|
707
|
-
async function collectArtifactBytes(http,
|
|
697
|
+
async function collectArtifactBytes(http, sessionId, items, zipPrefix, namespace, timeoutMs = SESSION_FILE_TRANSFER_DEFAULT_TIMEOUT_MS) {
|
|
708
698
|
const entries = [];
|
|
709
699
|
const captured = [];
|
|
710
700
|
const errors = [];
|
|
711
701
|
for (const item of items) {
|
|
712
702
|
const rel = item.filename ?? item.id;
|
|
713
703
|
try {
|
|
714
|
-
const path = `/api/
|
|
704
|
+
const path = `/api/sessions/${encodeURIComponent(sessionId)}/${namespace}/${encodeURIComponent(item.id)}/download`;
|
|
715
705
|
entries.push({
|
|
716
706
|
path: `${zipPrefix}${rel}`,
|
|
717
|
-
bytes: await
|
|
707
|
+
bytes: await downloadSessionFileBytesWithRetry(http, path, timeoutMs),
|
|
718
708
|
...(item.contentType !== undefined ? { contentType: item.contentType } : {}),
|
|
719
709
|
customerContent: true
|
|
720
710
|
});
|
|
@@ -737,17 +727,17 @@ function eventsJsonl(events) {
|
|
|
737
727
|
function isPathSelector(selector) {
|
|
738
728
|
return Boolean(selector && typeof selector === "object" && "path" in selector);
|
|
739
729
|
}
|
|
740
|
-
function
|
|
730
|
+
function normalizeSessionFileLookupPath(path) {
|
|
741
731
|
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
742
732
|
}
|
|
743
|
-
export function
|
|
744
|
-
return
|
|
733
|
+
export function filterSessionFiles(files, query) {
|
|
734
|
+
return files.filter((file) => sessionFileMatchesQuery(file, query));
|
|
745
735
|
}
|
|
746
736
|
/**
|
|
747
|
-
* The single filename-matcher for cross-
|
|
737
|
+
* The single filename-matcher for cross-session / per-session file SEARCH. A
|
|
748
738
|
* string is a case-insensitive SUBSTRING match; a RegExp is tested as given (and
|
|
749
739
|
* reset to `lastIndex = 0` so a reused `/g` regex is safe). Sharing this SSoT is
|
|
750
|
-
* what closes the T16 crash class:
|
|
740
|
+
* what closes the T16 crash class: session-file search no longer assumes `filename`
|
|
751
741
|
* is a string and passes a RegExp into `escapeRegExp(...).replace(...)`.
|
|
752
742
|
*/
|
|
753
743
|
export function toFilenameMatcher(filename) {
|
|
@@ -760,8 +750,8 @@ export function toFilenameMatcher(filename) {
|
|
|
760
750
|
return filename.test(name);
|
|
761
751
|
};
|
|
762
752
|
}
|
|
763
|
-
export function
|
|
764
|
-
const contentType = normalizeContentType(
|
|
753
|
+
export function classifySessionFile(file) {
|
|
754
|
+
const contentType = normalizeContentType(file.contentType);
|
|
765
755
|
if (contentType) {
|
|
766
756
|
if (contentType === "application/json" || contentType.endsWith("+json") || contentType.includes("json")) {
|
|
767
757
|
return "json";
|
|
@@ -789,7 +779,7 @@ export function classifyOutput(output) {
|
|
|
789
779
|
return "binary";
|
|
790
780
|
return "unknown";
|
|
791
781
|
}
|
|
792
|
-
const extension = extensionOf(
|
|
782
|
+
const extension = extensionOf(file.filename);
|
|
793
783
|
if (!extension)
|
|
794
784
|
return "unknown";
|
|
795
785
|
if (["json", "jsonl", "ndjson"].includes(extension))
|
|
@@ -812,10 +802,10 @@ export function classifyOutput(output) {
|
|
|
812
802
|
return "binary";
|
|
813
803
|
return "unknown";
|
|
814
804
|
}
|
|
815
|
-
export function
|
|
805
|
+
export function normalizeSessionFileLinkExpiresIn(input = "1h") {
|
|
816
806
|
if (typeof input === "number") {
|
|
817
807
|
if (!Number.isFinite(input) || input <= 0) {
|
|
818
|
-
throw new
|
|
808
|
+
throw new SessionStateError("sessionFileLink: expiresIn must be a positive number of seconds", {
|
|
819
809
|
expiresIn: input
|
|
820
810
|
});
|
|
821
811
|
}
|
|
@@ -827,34 +817,34 @@ export function normalizeOutputLinkExpiresIn(input = "1h") {
|
|
|
827
817
|
return 60 * 60;
|
|
828
818
|
if (input === "1d")
|
|
829
819
|
return 24 * 60 * 60;
|
|
830
|
-
throw new
|
|
820
|
+
throw new SessionStateError("sessionFileLink: expiresIn must be seconds, \"15m\", \"1h\", or \"1d\"", {
|
|
831
821
|
expiresIn: input
|
|
832
822
|
});
|
|
833
823
|
}
|
|
834
|
-
async function
|
|
824
|
+
async function resolveSessionFileLinkTarget(http, sessionId, selectorOrQuery) {
|
|
835
825
|
if (typeof selectorOrQuery === "string") {
|
|
836
826
|
if (selectorOrQuery.length === 0) {
|
|
837
|
-
throw new
|
|
827
|
+
throw new SessionStateError("sessionFileLink: selector must include a file id or query", { sessionId });
|
|
838
828
|
}
|
|
839
829
|
return { id: selectorOrQuery };
|
|
840
830
|
}
|
|
841
|
-
if (
|
|
831
|
+
if (hasSessionFileId(selectorOrQuery)) {
|
|
842
832
|
if (selectorOrQuery.id.length === 0) {
|
|
843
|
-
throw new
|
|
833
|
+
throw new SessionStateError("sessionFileLink: selector must include a file id or query", { sessionId });
|
|
844
834
|
}
|
|
845
835
|
return selectorOrQuery;
|
|
846
836
|
}
|
|
847
837
|
if (isPathSelector(selectorOrQuery) && selectorOrQuery.match === "suffix") {
|
|
848
|
-
return
|
|
838
|
+
return resolveSessionFileSelector(await listSessionFiles(http, sessionId), selectorOrQuery, sessionId);
|
|
849
839
|
}
|
|
850
|
-
const match = await
|
|
840
|
+
const match = await findSessionFile(http, sessionId, selectorOrQuery);
|
|
851
841
|
if (match)
|
|
852
842
|
return match;
|
|
853
|
-
throw new
|
|
843
|
+
throw new SessionStateError("sessionFileLink: file query matched no files", { sessionId });
|
|
854
844
|
}
|
|
855
|
-
function
|
|
856
|
-
const normalizedPath = typeof
|
|
857
|
-
if (query.path !== undefined && normalizedPath !==
|
|
845
|
+
function sessionFileMatchesQuery(file, query) {
|
|
846
|
+
const normalizedPath = typeof file.filename === "string" ? normalizeSessionFileQueryPath(file.filename) : "";
|
|
847
|
+
if (query.path !== undefined && normalizedPath !== normalizeSessionFileQueryPath(query.path)) {
|
|
858
848
|
return false;
|
|
859
849
|
}
|
|
860
850
|
if (query.filename !== undefined) {
|
|
@@ -875,21 +865,21 @@ function outputMatchesQuery(output, query) {
|
|
|
875
865
|
if (query.extension !== undefined && extensionOf(normalizedPath) !== normalizeExtension(query.extension)) {
|
|
876
866
|
return false;
|
|
877
867
|
}
|
|
878
|
-
if (query.contentType !== undefined && !contentTypeMatches(
|
|
868
|
+
if (query.contentType !== undefined && !contentTypeMatches(file.contentType, query.contentType)) {
|
|
879
869
|
return false;
|
|
880
870
|
}
|
|
881
|
-
if (query.type !== undefined &&
|
|
871
|
+
if (query.type !== undefined && classifySessionFile(file) !== query.type) {
|
|
882
872
|
return false;
|
|
883
873
|
}
|
|
884
874
|
return true;
|
|
885
875
|
}
|
|
886
|
-
function
|
|
876
|
+
function hasSessionFileId(value) {
|
|
887
877
|
return Boolean(value && typeof value === "object" && "id" in value && typeof value.id === "string");
|
|
888
878
|
}
|
|
889
|
-
function
|
|
879
|
+
function normalizeSessionFileQueryPath(path) {
|
|
890
880
|
let normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
891
|
-
while (normalized === "
|
|
892
|
-
normalized = normalized === "
|
|
881
|
+
while (normalized === "files" || normalized.startsWith("files/")) {
|
|
882
|
+
normalized = normalized === "files" ? "" : normalized.slice("files/".length);
|
|
893
883
|
}
|
|
894
884
|
return normalized.replace(/\/+$/, "");
|
|
895
885
|
}
|
|
@@ -897,7 +887,7 @@ function basenameOf(path) {
|
|
|
897
887
|
return path.split("/").filter(Boolean).pop() ?? "";
|
|
898
888
|
}
|
|
899
889
|
function directoryMatches(path, dir, recursive) {
|
|
900
|
-
const normalizedDir =
|
|
890
|
+
const normalizedDir = normalizeSessionFileQueryPath(dir);
|
|
901
891
|
if (normalizedDir.length === 0)
|
|
902
892
|
return true;
|
|
903
893
|
const prefix = `${normalizedDir}/`;
|
|
@@ -912,7 +902,7 @@ function normalizeExtension(extension) {
|
|
|
912
902
|
function extensionOf(path) {
|
|
913
903
|
if (!path)
|
|
914
904
|
return "";
|
|
915
|
-
const basename = basenameOf(
|
|
905
|
+
const basename = basenameOf(normalizeSessionFileQueryPath(path));
|
|
916
906
|
const index = basename.lastIndexOf(".");
|
|
917
907
|
return index > 0 && index < basename.length - 1 ? basename.slice(index + 1).toLowerCase() : "";
|
|
918
908
|
}
|
|
@@ -930,64 +920,64 @@ function contentTypeMatches(actual, expected) {
|
|
|
930
920
|
return normalizedActual === normalizedExpected;
|
|
931
921
|
}
|
|
932
922
|
/**
|
|
933
|
-
* Download EVERYTHING public about a
|
|
923
|
+
* Download EVERYTHING public about a session as one zip, organised into the three
|
|
934
924
|
* namespace folders:
|
|
935
925
|
*
|
|
936
|
-
* metadata/
|
|
926
|
+
* metadata/session.json — the session record.
|
|
937
927
|
* events/events.jsonl — typed event-channel records.
|
|
938
|
-
*
|
|
939
|
-
* manifest.json — `
|
|
928
|
+
* files/<rel> — the session's captured files.
|
|
929
|
+
* manifest.json — `SessionRecordManifestV1`.
|
|
940
930
|
*/
|
|
941
|
-
export async function download(http,
|
|
942
|
-
const [
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
931
|
+
export async function download(http, sessionId) {
|
|
932
|
+
const [sessionRecord, events, files] = await Promise.all([
|
|
933
|
+
getSessionRecord(http, sessionId),
|
|
934
|
+
listSessionRecordEvents(http, sessionId),
|
|
935
|
+
listSessionFiles(http, sessionId)
|
|
946
936
|
]);
|
|
947
|
-
const
|
|
948
|
-
const submissionSnapshot = extractSubmissionSnapshot(
|
|
949
|
-
const costTelemetry = extractCostTelemetry(
|
|
950
|
-
const manifest =
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
errors:
|
|
937
|
+
const collectedFiles = await collectArtifactBytes(http, sessionId, files, "files/", "files");
|
|
938
|
+
const submissionSnapshot = extractSubmissionSnapshot(sessionRecord);
|
|
939
|
+
const costTelemetry = extractCostTelemetry(sessionRecord);
|
|
940
|
+
const manifest = buildSessionRecordDownloadManifestV1({
|
|
941
|
+
sessionId,
|
|
942
|
+
sessionFiles: collectedFiles.captured,
|
|
943
|
+
errors: collectedFiles.errors,
|
|
954
944
|
typedEventCount: events.length,
|
|
955
945
|
...(submissionSnapshot ? { submission: { status: "present" } } : {}),
|
|
956
946
|
...(costTelemetry ? { cost: { status: "present" } } : {})
|
|
957
947
|
});
|
|
958
948
|
return zipEntries([
|
|
959
|
-
jsonEntry("metadata/
|
|
949
|
+
jsonEntry("metadata/session.json", sessionRecord),
|
|
960
950
|
...(submissionSnapshot ? [jsonEntry("metadata/submission.json", submissionSnapshot)] : []),
|
|
961
951
|
...(costTelemetry ? [jsonEntry("metadata/cost.json", costTelemetry)] : []),
|
|
962
952
|
jsonlEntry("events/events.jsonl", events),
|
|
963
|
-
...
|
|
953
|
+
...collectedFiles.entries,
|
|
964
954
|
jsonEntry("manifest.json", manifest)
|
|
965
955
|
]);
|
|
966
956
|
}
|
|
967
957
|
/**
|
|
968
|
-
* Download only the
|
|
958
|
+
* Download only the session's captured files (the `files` namespace). Zip
|
|
969
959
|
* layout: `<rel>` per file plus a `manifest.json`
|
|
970
|
-
* (`{
|
|
960
|
+
* (`{ sessionId, namespace: "files", files[], errors[] }`).
|
|
971
961
|
*/
|
|
972
|
-
export async function
|
|
973
|
-
const
|
|
974
|
-
const timeoutMs =
|
|
975
|
-
const { entries, captured, errors } = await collectArtifactBytes(http,
|
|
962
|
+
export async function downloadSessionFiles(http, sessionId, options) {
|
|
963
|
+
const files = await listSessionFiles(http, sessionId);
|
|
964
|
+
const timeoutMs = normalizeSessionFileTransferTimeoutMs(options?.timeoutMs);
|
|
965
|
+
const { entries, captured, errors } = await collectArtifactBytes(http, sessionId, files, "", "files", timeoutMs);
|
|
976
966
|
return zipEntries([
|
|
977
967
|
...entries,
|
|
978
|
-
jsonEntry("manifest.json", {
|
|
968
|
+
jsonEntry("manifest.json", { sessionId, namespace: "files", files: captured, errors })
|
|
979
969
|
]);
|
|
980
970
|
}
|
|
981
971
|
/**
|
|
982
972
|
* Download only the event archive (the `events` namespace). Always includes
|
|
983
973
|
* typed `events.jsonl` plus `manifest.json`.
|
|
984
974
|
*/
|
|
985
|
-
export async function downloadEvents(http,
|
|
986
|
-
const events = await
|
|
975
|
+
export async function downloadEvents(http, sessionId) {
|
|
976
|
+
const events = await listSessionRecordEvents(http, sessionId);
|
|
987
977
|
return zipEntries([
|
|
988
978
|
jsonlEntry("events.jsonl", events),
|
|
989
979
|
jsonEntry("manifest.json", {
|
|
990
|
-
|
|
980
|
+
sessionId,
|
|
991
981
|
namespace: "events",
|
|
992
982
|
files: [{ path: "events.jsonl", role: "typed_events", status: "present", recordCount: events.length }],
|
|
993
983
|
errors: []
|
|
@@ -995,23 +985,23 @@ export async function downloadEvents(http, runId) {
|
|
|
995
985
|
]);
|
|
996
986
|
}
|
|
997
987
|
/**
|
|
998
|
-
* Download only the
|
|
999
|
-
* containing `
|
|
988
|
+
* Download only the session record (the `metadata` namespace) as a zip
|
|
989
|
+
* containing `session.json` plus `manifest.json`.
|
|
1000
990
|
*/
|
|
1001
|
-
export async function downloadMetadata(http,
|
|
1002
|
-
const
|
|
991
|
+
export async function downloadMetadata(http, sessionId) {
|
|
992
|
+
const sessionRecord = await getSessionRecord(http, sessionId);
|
|
1003
993
|
return zipEntries([
|
|
1004
|
-
jsonEntry("
|
|
994
|
+
jsonEntry("session.json", sessionRecord),
|
|
1005
995
|
jsonEntry("manifest.json", {
|
|
1006
|
-
|
|
996
|
+
sessionId,
|
|
1007
997
|
namespace: "metadata",
|
|
1008
|
-
files: [{ path: "
|
|
998
|
+
files: [{ path: "session.json", role: "session_metadata", status: "present" }],
|
|
1009
999
|
errors: []
|
|
1010
1000
|
})
|
|
1011
1001
|
]);
|
|
1012
1002
|
}
|
|
1013
1003
|
function zipEntries(entries) {
|
|
1014
|
-
|
|
1004
|
+
assertSessionRecordArchivePublicSafeV1(entries);
|
|
1015
1005
|
const files = {};
|
|
1016
1006
|
for (const entry of entries) {
|
|
1017
1007
|
files[entry.path] = entry.bytes;
|
|
@@ -1032,8 +1022,8 @@ function jsonlEntry(path, events) {
|
|
|
1032
1022
|
contentType: "application/jsonl; charset=utf-8"
|
|
1033
1023
|
};
|
|
1034
1024
|
}
|
|
1035
|
-
function extractSubmissionSnapshot(
|
|
1036
|
-
const raw =
|
|
1025
|
+
function extractSubmissionSnapshot(sessionRecord) {
|
|
1026
|
+
const raw = sessionRecord.submission;
|
|
1037
1027
|
if (!isRecord(raw) || raw.kind !== "submission" || !isRecord(raw.submission)) {
|
|
1038
1028
|
return undefined;
|
|
1039
1029
|
}
|
|
@@ -1041,18 +1031,18 @@ function extractSubmissionSnapshot(run) {
|
|
|
1041
1031
|
submission: raw.submission
|
|
1042
1032
|
};
|
|
1043
1033
|
}
|
|
1044
|
-
function extractCostTelemetry(
|
|
1045
|
-
const raw =
|
|
1034
|
+
function extractCostTelemetry(sessionRecord) {
|
|
1035
|
+
const raw = sessionRecord.costTelemetry;
|
|
1046
1036
|
return isRecord(raw) ? raw : undefined;
|
|
1047
1037
|
}
|
|
1048
1038
|
function isRecord(value) {
|
|
1049
1039
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1050
1040
|
}
|
|
1051
1041
|
// ===========================================================================
|
|
1052
|
-
//
|
|
1042
|
+
// SessionRecord submission operations (McpServer / session config composition)
|
|
1053
1043
|
// ===========================================================================
|
|
1054
|
-
export async function
|
|
1055
|
-
return http.request("/api/
|
|
1044
|
+
export async function startSessionRecord(http, request) {
|
|
1045
|
+
return http.request("/api/sessions", {
|
|
1056
1046
|
method: "POST",
|
|
1057
1047
|
body: JSON.stringify(request)
|
|
1058
1048
|
});
|
|
@@ -1200,8 +1190,8 @@ function unwrapSkill(result) {
|
|
|
1200
1190
|
}
|
|
1201
1191
|
return result;
|
|
1202
1192
|
}
|
|
1203
|
-
function
|
|
1204
|
-
return Boolean(value && typeof value === "object" && "
|
|
1193
|
+
function hasSessionRecord(value) {
|
|
1194
|
+
return Boolean(value && typeof value === "object" && "session" in value);
|
|
1205
1195
|
}
|
|
1206
1196
|
function unwrapSession(result) {
|
|
1207
1197
|
if (result && typeof result === "object" && "session" in result) {
|
|
@@ -1212,7 +1202,7 @@ function unwrapSession(result) {
|
|
|
1212
1202
|
/**
|
|
1213
1203
|
* Upload bytes to the hosted API's content-addressable asset endpoint.
|
|
1214
1204
|
* Returns a storage-neutral asset id suitable for `kind:"asset"` refs in a
|
|
1215
|
-
* later
|
|
1205
|
+
* later session submission.
|
|
1216
1206
|
*/
|
|
1217
1207
|
export async function uploadWorkspaceAsset(http, input) {
|
|
1218
1208
|
return http.request("/assets", {
|