@opengeni/api-router 0.5.7 → 0.9.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/app.d.ts +21 -3
- package/dist/app.js +3 -1
- package/dist/{chunk-HBEJMWD3.js → chunk-QOYQBYHM.js} +4302 -926
- package/dist/chunk-QOYQBYHM.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/app.ts +83 -18
- package/src/codex-redemption-security.ts +96 -0
- package/src/github-access.ts +46 -0
- package/src/github-browser-flow.ts +83 -0
- package/src/http/auth.ts +12 -4
- package/src/http/sse.ts +526 -92
- package/src/index.ts +2 -1
- package/src/integrations/oauth-client.ts +147 -61
- package/src/mcp/server.ts +828 -146
- package/src/mcp/session-view.ts +721 -201
- package/src/mcp/toolspace.ts +482 -132
- package/src/model-catalog.ts +337 -0
- package/src/routes/codex.ts +883 -28
- package/src/routes/enrollments.ts +2 -2
- package/src/routes/files.ts +153 -0
- package/src/routes/github.ts +63 -202
- package/src/routes/install.ts +1 -1
- package/src/routes/machines.ts +2 -2
- package/src/routes/sessions.ts +789 -81
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +66 -13
- package/src/sandbox/access.ts +1 -1
- package/src/sandbox/auth-callout.ts +1 -1
- package/src/sandbox/channel-a.ts +14 -1
- package/src/sandbox/enrollment.ts +4 -4
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +1 -1
- package/src/sandbox/viewer.ts +8 -2
- package/dist/chunk-HBEJMWD3.js.map +0 -1
package/src/mcp/session-view.ts
CHANGED
|
@@ -1,89 +1,133 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `session_get`) exposed to manager-style agents over MCP.
|
|
2
|
+
* Model-facing projections for cross-session monitoring tools.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* tens of thousands of characters, and a manager that pages a busy worker piles
|
|
12
|
-
* hundreds of thousands of characters into its own context in one monitoring
|
|
13
|
-
* turn — the exact "parent ingests child" blow-up that bricks the manager.
|
|
14
|
-
*
|
|
15
|
-
* The manager rarely needs a worker's full message deltas / tool outputs
|
|
16
|
-
* verbatim; it needs status + recent progress. So these tools cap what they
|
|
17
|
-
* hand back in two stages, both pure and exhaustively testable here:
|
|
18
|
-
*
|
|
19
|
-
* 1. PER-EVENT FIELD TRIM (`capEventPayload` / `capPayloadValue`): walk each
|
|
20
|
-
* event's payload and clamp any over-long string (and any over-large nested
|
|
21
|
-
* object, by serializing then clamping) to a per-field budget, leaving an
|
|
22
|
-
* explicit `…N chars truncated…` marker. Type-agnostic: it targets whatever
|
|
23
|
-
* field is fat (`text`, `output`, `arguments`, `raw`, `delta`, …) without
|
|
24
|
-
* enumerating event types, so a new fat event type is capped automatically.
|
|
25
|
-
*
|
|
26
|
-
* 2. HEAD+TAIL PAGE BUDGET (`capEventPage`): after per-event trim, if the page
|
|
27
|
-
* still exceeds the total token budget, keep a HEAD (oldest, for entry
|
|
28
|
-
* context) and a TAIL (newest, for recent progress) of events and drop the
|
|
29
|
-
* middle, inserting one synthetic marker event that says how many were
|
|
30
|
-
* dropped and how to get them (page with `after`/`limit`, or read the
|
|
31
|
-
* notebook). Pagination semantics are preserved: `nextAfter` is still the
|
|
32
|
-
* real highest `sequence` returned, so the next page starts exactly where
|
|
33
|
-
* this one ended.
|
|
34
|
-
*
|
|
35
|
-
* Worker-side and UI consumers never go through here — they call the DB
|
|
36
|
-
* functions or the REST routes directly. This module only shapes the MCP tool
|
|
37
|
-
* result a manager model reads, and it is intentionally dependency-free (no DB)
|
|
38
|
-
* so the cap logic can be unit-tested in isolation.
|
|
4
|
+
* `session_events` reads are selected tail/forward and filtered in PostgreSQL;
|
|
5
|
+
* this module is the independent final guard before JSON enters a manager
|
|
6
|
+
* model's context. It measures the exact pretty-printed MCP text, trims fat
|
|
7
|
+
* payload fields, and—only if still required—removes rows from the pagination
|
|
8
|
+
* edge while preserving a usable cursor. It never manufactures an event or
|
|
9
|
+
* advances across an event it did not return.
|
|
39
10
|
*/
|
|
40
11
|
|
|
41
|
-
import type {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
12
|
+
import type {
|
|
13
|
+
Rig,
|
|
14
|
+
Session,
|
|
15
|
+
SessionEvent,
|
|
16
|
+
SessionEventCompactResult,
|
|
17
|
+
SessionEventPayloadMode,
|
|
18
|
+
SessionEventReadDirection,
|
|
19
|
+
SessionEventReadMode,
|
|
20
|
+
} from "@opengeni/contracts";
|
|
21
|
+
import {
|
|
22
|
+
boundSessionEventPayload,
|
|
23
|
+
measureSessionEventJson,
|
|
24
|
+
sessionEventJsonBytes,
|
|
25
|
+
} from "@opengeni/contracts";
|
|
50
26
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
27
|
+
export const SESSION_EVENT_MCP_MAX_BYTES = 64 * 1024;
|
|
28
|
+
export const SESSION_EVENT_MCP_FIELD_MAX_CHARS = 4_000;
|
|
29
|
+
export const DEFAULT_SESSION_DETAIL_CHARS = 6_000;
|
|
30
|
+
export const SESSION_DETAIL_MCP_MAX_BYTES = 64 * 1024;
|
|
31
|
+
export const RIG_DETAIL_MCP_MAX_BYTES = 64 * 1024;
|
|
54
32
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
tailEvents: number;
|
|
67
|
-
};
|
|
33
|
+
/**
|
|
34
|
+
* Keep the single-result MCP response below the same pretty-JSON envelope as
|
|
35
|
+
* event pages. The contracts projection bounds each value independently for
|
|
36
|
+
* HTTP/SDK use; this second boundary accounts for the result identity,
|
|
37
|
+
* failure/truncation metadata, and MCP's pretty-printing overhead.
|
|
38
|
+
*/
|
|
39
|
+
export function boundSessionEventCompactResult(
|
|
40
|
+
result: SessionEventCompactResult,
|
|
41
|
+
maxBytes = SESSION_EVENT_MCP_MAX_BYTES,
|
|
42
|
+
): SessionEventCompactResult {
|
|
43
|
+
const envelopeMaxBytes = Math.max(8 * 1024, maxBytes);
|
|
68
44
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
45
|
+
const project = (budget: number): SessionEventCompactResult => {
|
|
46
|
+
const noValues = budget <= 0;
|
|
47
|
+
const text =
|
|
48
|
+
noValues || result.text === null ? null : clampString(result.text, Math.max(128, budget));
|
|
49
|
+
const boundValue = (value: unknown): unknown =>
|
|
50
|
+
noValues || value === null
|
|
51
|
+
? null
|
|
52
|
+
: boundSessionEventPayload(value, {
|
|
53
|
+
surface: "http_projection",
|
|
54
|
+
maxBytes: Math.max(1_024, budget),
|
|
55
|
+
});
|
|
56
|
+
const output = boundValue(result.output);
|
|
57
|
+
const resultValue = boundValue(result.result);
|
|
58
|
+
const checkpoint = boundValue(result.checkpoint);
|
|
59
|
+
const receipt = boundValue(result.receipt);
|
|
60
|
+
const failure =
|
|
61
|
+
noValues || result.failure === null
|
|
62
|
+
? null
|
|
63
|
+
: {
|
|
64
|
+
error:
|
|
65
|
+
clampString(result.failure.error ?? "", Math.max(128, Math.floor(budget / 3))) ||
|
|
66
|
+
null,
|
|
67
|
+
code:
|
|
68
|
+
clampString(result.failure.code ?? "", Math.max(128, Math.floor(budget / 6))) || null,
|
|
69
|
+
retryable: result.failure.retryable,
|
|
70
|
+
recovery:
|
|
71
|
+
clampString(result.failure.recovery ?? "", Math.max(128, Math.floor(budget / 3))) ||
|
|
72
|
+
null,
|
|
73
|
+
};
|
|
74
|
+
const changed =
|
|
75
|
+
text !== result.text ||
|
|
76
|
+
output !== result.output ||
|
|
77
|
+
resultValue !== result.result ||
|
|
78
|
+
checkpoint !== result.checkpoint ||
|
|
79
|
+
receipt !== result.receipt ||
|
|
80
|
+
JSON.stringify(failure) !== JSON.stringify(result.failure);
|
|
81
|
+
// A compact result can inherit a source-payload boundary without any of
|
|
82
|
+
// its already-bounded slots changing at the MCP boundary. Keep that loss
|
|
83
|
+
// visible on the model-facing result, but do not manufacture a new byte
|
|
84
|
+
// count: the source projection owns the original/delivered accounting.
|
|
85
|
+
const inheritedSourceBoundary = result.truncation.fields.includes("payload");
|
|
86
|
+
const mcpBoundaryRecorded = changed || inheritedSourceBoundary;
|
|
87
|
+
const deliveredBytes = sessionEventJsonBytes({
|
|
88
|
+
text,
|
|
89
|
+
output,
|
|
90
|
+
result: resultValue,
|
|
91
|
+
failure,
|
|
92
|
+
checkpoint,
|
|
93
|
+
receipt,
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
...result,
|
|
97
|
+
text,
|
|
98
|
+
output,
|
|
99
|
+
result: resultValue,
|
|
100
|
+
failure,
|
|
101
|
+
checkpoint,
|
|
102
|
+
receipt,
|
|
103
|
+
truncation: {
|
|
104
|
+
...result.truncation,
|
|
105
|
+
truncated: result.truncation.truncated || changed,
|
|
106
|
+
fields: mcpBoundaryRecorded
|
|
107
|
+
? [...new Set([...result.truncation.fields, "mcp_envelope"])]
|
|
108
|
+
: result.truncation.fields,
|
|
109
|
+
originalBytes: changed
|
|
110
|
+
? (result.truncation.originalBytes ?? result.truncation.deliveredBytes)
|
|
111
|
+
: result.truncation.originalBytes,
|
|
112
|
+
deliveredBytes,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
};
|
|
78
116
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
117
|
+
// Start with a generous budget and tighten only if the full compact result
|
|
118
|
+
// would exceed MCP's envelope. This preserves as much result-bearing data
|
|
119
|
+
// as possible while guaranteeing a truthful bounded response.
|
|
120
|
+
for (const budget of [12_000, 8_000, 4_000, 2_000, 1_000, 0]) {
|
|
121
|
+
const candidate = project(budget);
|
|
122
|
+
if (prettyJsonBytes(candidate) <= envelopeMaxBytes) return candidate;
|
|
123
|
+
}
|
|
124
|
+
throw new RangeError(
|
|
125
|
+
`Session-event compact result exceeds its ${envelopeMaxBytes}-byte envelope`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
82
128
|
|
|
83
129
|
function safeStringify(value: unknown): string {
|
|
84
|
-
if (typeof value === "string")
|
|
85
|
-
return value;
|
|
86
|
-
}
|
|
130
|
+
if (typeof value === "string") return value;
|
|
87
131
|
try {
|
|
88
132
|
return JSON.stringify(value) ?? String(value);
|
|
89
133
|
} catch {
|
|
@@ -92,177 +136,488 @@ function safeStringify(value: unknown): string {
|
|
|
92
136
|
}
|
|
93
137
|
|
|
94
138
|
function truncationMarker(droppedChars: number): string {
|
|
95
|
-
return `…[${droppedChars} chars
|
|
139
|
+
return `…[${droppedChars} chars omitted from this model monitoring projection; request explicit forensic full mode for any retained audit preview; original source output may not have been retained]`;
|
|
96
140
|
}
|
|
97
141
|
|
|
98
142
|
function clampString(value: string, maxChars: number): string {
|
|
99
|
-
if (value.length <= maxChars)
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
|
-
// Keep a head and a small tail of the field so both the start and the end
|
|
103
|
-
// (often the most diagnostic part of a tool output / error) survive.
|
|
143
|
+
if (value.length <= maxChars) return value;
|
|
104
144
|
const dropped = value.length - maxChars;
|
|
105
145
|
const headChars = Math.max(0, Math.floor(maxChars * 0.7));
|
|
106
146
|
const tailChars = Math.max(0, maxChars - headChars);
|
|
107
|
-
const head = value.slice(0, headChars);
|
|
108
147
|
const tail = tailChars > 0 ? value.slice(value.length - tailChars) : "";
|
|
109
|
-
return `${
|
|
148
|
+
return `${value.slice(0, headChars)}${truncationMarker(dropped)}${tail}`;
|
|
110
149
|
}
|
|
111
150
|
|
|
112
|
-
/**
|
|
113
|
-
* Recursively clamp any over-budget string or nested value inside a payload.
|
|
114
|
-
* Strings longer than `perFieldChars` are head+tail clamped. Nested objects /
|
|
115
|
-
* arrays whose serialized form exceeds `perFieldChars` are recursed into so the
|
|
116
|
-
* clamp lands on the actual fat leaf; if recursion cannot shrink them enough
|
|
117
|
-
* (e.g. thousands of tiny fields), the whole branch is replaced by its clamped
|
|
118
|
-
* serialization. Plain scalars pass through untouched. A depth guard makes the
|
|
119
|
-
* walk safe against pathological / cyclic structures.
|
|
120
|
-
*/
|
|
151
|
+
/** Recursively clamp fat leaves and collapse pathological containers. */
|
|
121
152
|
export function capPayloadValue(value: unknown, perFieldChars: number, depth = 0): unknown {
|
|
122
|
-
if (typeof value === "string")
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (value === null || typeof value !== "object") {
|
|
126
|
-
return value;
|
|
127
|
-
}
|
|
128
|
-
// Guard against pathological / cyclic structures: past a reasonable depth,
|
|
129
|
-
// collapse to a clamped serialization.
|
|
130
|
-
if (depth >= 8) {
|
|
131
|
-
return clampString(safeStringify(value), perFieldChars);
|
|
132
|
-
}
|
|
153
|
+
if (typeof value === "string") return clampString(value, perFieldChars);
|
|
154
|
+
if (value === null || typeof value !== "object") return value;
|
|
155
|
+
if (depth >= 8) return clampString(safeStringify(value), perFieldChars);
|
|
133
156
|
const serializedLength = safeStringify(value).length;
|
|
134
|
-
if (serializedLength <= perFieldChars)
|
|
135
|
-
return value;
|
|
136
|
-
}
|
|
157
|
+
if (serializedLength <= perFieldChars) return value;
|
|
137
158
|
if (Array.isArray(value)) {
|
|
138
159
|
const mapped = value.map((entry) => capPayloadValue(entry, perFieldChars, depth + 1));
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return clampString(safeStringify(value), perFieldChars);
|
|
160
|
+
return safeStringify(mapped).length <= perFieldChars * 2
|
|
161
|
+
? mapped
|
|
162
|
+
: clampString(safeStringify(value), perFieldChars);
|
|
143
163
|
}
|
|
144
164
|
const out: Record<string, unknown> = {};
|
|
145
165
|
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
146
166
|
out[key] = capPayloadValue(entry, perFieldChars, depth + 1);
|
|
147
167
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
return clampString(safeStringify(value), perFieldChars);
|
|
168
|
+
return safeStringify(out).length <= perFieldChars * 4
|
|
169
|
+
? out
|
|
170
|
+
: clampString(safeStringify(value), perFieldChars);
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
export function capEventPayload(event: SessionEvent, perFieldChars: number): SessionEvent {
|
|
157
174
|
const cappedPayload = capPayloadValue(event.payload, perFieldChars);
|
|
158
|
-
|
|
159
|
-
return event;
|
|
160
|
-
}
|
|
161
|
-
return { ...event, payload: cappedPayload };
|
|
175
|
+
return cappedPayload === event.payload ? event : { ...event, payload: cappedPayload };
|
|
162
176
|
}
|
|
163
177
|
|
|
164
|
-
export type
|
|
178
|
+
export type SessionEventMcpPageInput = {
|
|
179
|
+
events: readonly SessionEvent[];
|
|
180
|
+
mode: SessionEventReadMode;
|
|
181
|
+
payloadMode: SessionEventPayloadMode;
|
|
182
|
+
direction: SessionEventReadDirection;
|
|
183
|
+
sourceHasMore: boolean;
|
|
184
|
+
sourceTruncatedBy: "count" | "bytes" | null;
|
|
185
|
+
after: number;
|
|
186
|
+
before: number | null;
|
|
187
|
+
maxBytes?: number | undefined;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type SessionEventMcpPage = {
|
|
191
|
+
mode: SessionEventReadMode;
|
|
192
|
+
payloadMode: SessionEventPayloadMode;
|
|
193
|
+
direction: SessionEventReadDirection;
|
|
165
194
|
events: SessionEvent[];
|
|
166
|
-
|
|
167
|
-
// can advance the cursor correctly even when the middle was dropped. Null
|
|
168
|
-
// when the page was empty.
|
|
195
|
+
coveredSequence: { first: number; last: number } | null;
|
|
169
196
|
nextAfter: number | null;
|
|
197
|
+
nextBefore: number | null;
|
|
198
|
+
hasMore: boolean;
|
|
170
199
|
truncated: boolean;
|
|
200
|
+
truncation?: {
|
|
201
|
+
reasons: Array<"source_count" | "source_bytes" | "model_payload" | "model_bytes">;
|
|
202
|
+
omittedSide: "before" | "after";
|
|
203
|
+
resumeCursor: number | null;
|
|
204
|
+
};
|
|
205
|
+
bytes: number;
|
|
206
|
+
maxBytes: number;
|
|
171
207
|
};
|
|
172
208
|
|
|
209
|
+
function prettyJsonBytes(value: unknown): number {
|
|
210
|
+
return Buffer.byteLength(JSON.stringify(value, null, 2), "utf8");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function setMeasuredBytes(page: SessionEventMcpPage): number {
|
|
214
|
+
let measured = page.bytes;
|
|
215
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
216
|
+
page.bytes = measured;
|
|
217
|
+
const next = prettyJsonBytes(page);
|
|
218
|
+
if (next === measured) return next;
|
|
219
|
+
measured = next;
|
|
220
|
+
}
|
|
221
|
+
page.bytes = measured;
|
|
222
|
+
return prettyJsonBytes(page);
|
|
223
|
+
}
|
|
224
|
+
|
|
173
225
|
/**
|
|
174
|
-
* Build
|
|
175
|
-
*
|
|
176
|
-
* between the kept head and tail so ordering by sequence stays monotonic. It
|
|
177
|
-
* never participates in pagination (the caller derives `nextAfter` from the
|
|
178
|
-
* real events, not this marker). Typed `session.status.changed` so the
|
|
179
|
-
* synthetic event still validates against the `SessionEvent` contract.
|
|
226
|
+
* Build the exact model-visible page. Returned `bytes` includes all metadata
|
|
227
|
+
* and pretty-printing used by the MCP JSON adapter.
|
|
180
228
|
*/
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
229
|
+
export function boundSessionEventMcpPage(input: SessionEventMcpPageInput): SessionEventMcpPage {
|
|
230
|
+
const maxBytes = Math.max(8 * 1024, input.maxBytes ?? SESSION_EVENT_MCP_MAX_BYTES);
|
|
231
|
+
let payloadTrimmed = false;
|
|
232
|
+
const events = input.events.map((event) => {
|
|
233
|
+
const capped = capEventPayload(event, SESSION_EVENT_MCP_FIELD_MAX_CHARS);
|
|
234
|
+
if (capped !== event) payloadTrimmed = true;
|
|
235
|
+
return capped;
|
|
236
|
+
});
|
|
237
|
+
let modelRowsDropped = false;
|
|
238
|
+
|
|
239
|
+
const build = (): SessionEventMcpPage => {
|
|
240
|
+
const first = events[0]?.sequence ?? null;
|
|
241
|
+
const last = events.at(-1)?.sequence ?? null;
|
|
242
|
+
const reasons: NonNullable<SessionEventMcpPage["truncation"]>["reasons"] = [];
|
|
243
|
+
if (input.sourceHasMore) {
|
|
244
|
+
reasons.push(input.sourceTruncatedBy === "bytes" ? "source_bytes" : "source_count");
|
|
245
|
+
}
|
|
246
|
+
if (payloadTrimmed) reasons.push("model_payload");
|
|
247
|
+
if (modelRowsDropped) reasons.push("model_bytes");
|
|
248
|
+
const nextAfter = input.direction === "after" ? (last ?? input.after) : null;
|
|
249
|
+
const nextBefore = input.direction === "before" ? (first ?? input.before) : null;
|
|
250
|
+
const page: SessionEventMcpPage = {
|
|
251
|
+
mode: input.mode,
|
|
252
|
+
payloadMode: input.payloadMode,
|
|
253
|
+
direction: input.direction,
|
|
254
|
+
events: [...events],
|
|
255
|
+
coveredSequence: first === null || last === null ? null : { first, last },
|
|
256
|
+
nextAfter,
|
|
257
|
+
nextBefore,
|
|
258
|
+
hasMore: input.sourceHasMore || modelRowsDropped,
|
|
259
|
+
truncated: reasons.length > 0,
|
|
260
|
+
...(reasons.length > 0
|
|
261
|
+
? {
|
|
262
|
+
truncation: {
|
|
263
|
+
reasons,
|
|
264
|
+
omittedSide: input.direction,
|
|
265
|
+
resumeCursor: input.direction === "after" ? nextAfter : nextBefore,
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
269
|
+
bytes: 0,
|
|
270
|
+
maxBytes,
|
|
271
|
+
};
|
|
272
|
+
setMeasuredBytes(page);
|
|
273
|
+
return page;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
let page = build();
|
|
277
|
+
while (page.bytes > maxBytes && events.length > 0) {
|
|
278
|
+
if (input.direction === "before") events.shift();
|
|
279
|
+
else events.pop();
|
|
280
|
+
modelRowsDropped = true;
|
|
281
|
+
page = build();
|
|
282
|
+
}
|
|
283
|
+
if (page.bytes > maxBytes) {
|
|
284
|
+
throw new RangeError(`Session-event MCP metadata exceeds its ${maxBytes}-byte envelope`);
|
|
285
|
+
}
|
|
286
|
+
return page;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
type MonitoringPreviewState = {
|
|
290
|
+
remainingStringBytes: number;
|
|
291
|
+
remainingNodes: number;
|
|
292
|
+
truncated: boolean;
|
|
293
|
+
details: string[];
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
type SessionDetailFieldFact = {
|
|
297
|
+
truncated: boolean;
|
|
298
|
+
originalBytes: number | null;
|
|
299
|
+
deliveredBytes: number;
|
|
300
|
+
originalCount?: number;
|
|
301
|
+
deliveredCount?: number;
|
|
302
|
+
measurementBounded?: boolean;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
function modelStringProjection(
|
|
306
|
+
value: string,
|
|
307
|
+
maxBytes: number,
|
|
308
|
+
): {
|
|
309
|
+
value: string;
|
|
310
|
+
fact: SessionDetailFieldFact & { originalChars: number };
|
|
311
|
+
} {
|
|
312
|
+
const originalBytes = Buffer.byteLength(value, "utf8");
|
|
313
|
+
if (originalBytes <= maxBytes) {
|
|
314
|
+
return {
|
|
315
|
+
value,
|
|
316
|
+
fact: {
|
|
317
|
+
truncated: false,
|
|
318
|
+
originalBytes,
|
|
319
|
+
deliveredBytes: originalBytes,
|
|
320
|
+
originalChars: value.length,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
let omittedBytes = originalBytes - maxBytes;
|
|
325
|
+
let head = "";
|
|
326
|
+
let tail = "";
|
|
327
|
+
let marker = "";
|
|
328
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
329
|
+
marker = `…[${omittedBytes} UTF-8 bytes omitted from model monitoring projection]…`;
|
|
330
|
+
const contentBudget = Math.max(0, maxBytes - Buffer.byteLength(marker, "utf8"));
|
|
331
|
+
head = utf8Prefix(value, Math.floor(contentBudget * 0.7));
|
|
332
|
+
tail = utf8Suffix(value, contentBudget - Buffer.byteLength(head, "utf8"));
|
|
333
|
+
const exact = Math.max(
|
|
334
|
+
0,
|
|
335
|
+
originalBytes - Buffer.byteLength(head, "utf8") - Buffer.byteLength(tail, "utf8"),
|
|
336
|
+
);
|
|
337
|
+
if (exact === omittedBytes) break;
|
|
338
|
+
omittedBytes = exact;
|
|
339
|
+
}
|
|
340
|
+
const projected = `${head}${marker}${tail}`;
|
|
188
341
|
return {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
_truncated: true,
|
|
196
|
-
note: `${droppedCount} event(s) (sequence ${firstDroppedSequence}–${lastDroppedSequence}) omitted from this monitoring view to keep the response bounded. Page the gap with session_events after=${firstDroppedSequence - 1} limit=… if you need them verbatim, or read the worker's session notebook.`,
|
|
197
|
-
droppedCount,
|
|
198
|
-
omittedSequenceRange: [firstDroppedSequence, lastDroppedSequence],
|
|
342
|
+
value: projected,
|
|
343
|
+
fact: {
|
|
344
|
+
truncated: true,
|
|
345
|
+
originalBytes,
|
|
346
|
+
deliveredBytes: Buffer.byteLength(projected, "utf8"),
|
|
347
|
+
originalChars: value.length,
|
|
199
348
|
},
|
|
200
|
-
occurredAt: template.occurredAt,
|
|
201
|
-
clientEventId: null,
|
|
202
|
-
turnId: null,
|
|
203
349
|
};
|
|
204
350
|
}
|
|
205
351
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
352
|
+
function utf8Prefix(value: string, maxBytes: number): string {
|
|
353
|
+
let index = 0;
|
|
354
|
+
let bytes = 0;
|
|
355
|
+
while (index < value.length) {
|
|
356
|
+
const codePoint = value.codePointAt(index)!;
|
|
357
|
+
const character = String.fromCodePoint(codePoint);
|
|
358
|
+
const nextBytes = Buffer.byteLength(character, "utf8");
|
|
359
|
+
if (bytes + nextBytes > maxBytes) break;
|
|
360
|
+
bytes += nextBytes;
|
|
361
|
+
index += character.length;
|
|
362
|
+
}
|
|
363
|
+
return value.slice(0, index);
|
|
364
|
+
}
|
|
217
365
|
|
|
218
|
-
|
|
366
|
+
function utf8Suffix(value: string, maxBytes: number): string {
|
|
367
|
+
let index = value.length;
|
|
368
|
+
let bytes = 0;
|
|
369
|
+
while (index > 0) {
|
|
370
|
+
const last = value.charCodeAt(index - 1);
|
|
371
|
+
const width = last >= 0xdc00 && last <= 0xdfff && index > 1 ? 2 : 1;
|
|
372
|
+
const character = value.slice(index - width, index);
|
|
373
|
+
const nextBytes = Buffer.byteLength(character, "utf8");
|
|
374
|
+
if (bytes + nextBytes > maxBytes) break;
|
|
375
|
+
bytes += nextBytes;
|
|
376
|
+
index -= width;
|
|
377
|
+
}
|
|
378
|
+
return value.slice(index);
|
|
379
|
+
}
|
|
219
380
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
381
|
+
function previewMonitoringValue(
|
|
382
|
+
value: unknown,
|
|
383
|
+
state: MonitoringPreviewState,
|
|
384
|
+
path = "$",
|
|
385
|
+
depth = 0,
|
|
386
|
+
): unknown {
|
|
387
|
+
if (state.remainingNodes <= 0 || depth >= 8) {
|
|
388
|
+
state.truncated = true;
|
|
389
|
+
if (state.details.length < 24) state.details.push(`${path}: traversal boundary`);
|
|
390
|
+
return "[nested value omitted from model monitoring projection]";
|
|
391
|
+
}
|
|
392
|
+
state.remainingNodes -= 1;
|
|
393
|
+
if (typeof value === "string") {
|
|
394
|
+
const projected = modelStringProjection(value, Math.min(1_000, state.remainingStringBytes));
|
|
395
|
+
state.remainingStringBytes = Math.max(
|
|
396
|
+
0,
|
|
397
|
+
state.remainingStringBytes - projected.fact.deliveredBytes,
|
|
398
|
+
);
|
|
399
|
+
if (projected.fact.truncated) {
|
|
400
|
+
state.truncated = true;
|
|
401
|
+
if (state.details.length < 24) state.details.push(`${path}: string truncated`);
|
|
227
402
|
}
|
|
403
|
+
return projected.value;
|
|
228
404
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
405
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return value;
|
|
406
|
+
if (typeof value !== "object") {
|
|
407
|
+
state.truncated = true;
|
|
408
|
+
if (state.details.length < 24) state.details.push(`${path}: non-JSON value omitted`);
|
|
409
|
+
return `[${typeof value} value omitted from model monitoring projection]`;
|
|
233
410
|
}
|
|
411
|
+
if (Array.isArray(value)) {
|
|
412
|
+
const keep = Math.min(24, value.length);
|
|
413
|
+
const out = value
|
|
414
|
+
.slice(0, keep)
|
|
415
|
+
.map((entry, index) => previewMonitoringValue(entry, state, `${path}[${index}]`, depth + 1));
|
|
416
|
+
if (keep < value.length) {
|
|
417
|
+
state.truncated = true;
|
|
418
|
+
if (state.details.length < 24) {
|
|
419
|
+
state.details.push(`${path}: ${value.length - keep} array entries omitted`);
|
|
420
|
+
}
|
|
421
|
+
out.push({ omittedEntries: value.length - keep });
|
|
422
|
+
}
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
const out: Record<string, unknown> = {};
|
|
426
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
427
|
+
const keep = Math.min(24, entries.length);
|
|
428
|
+
for (let index = 0; index < keep; index += 1) {
|
|
429
|
+
const [rawKey, entry] = entries[index]!;
|
|
430
|
+
const keyProjection = modelStringProjection(rawKey, 128).value;
|
|
431
|
+
const key = Object.prototype.hasOwnProperty.call(out, keyProjection)
|
|
432
|
+
? `${keyProjection}#${index}`
|
|
433
|
+
: keyProjection;
|
|
434
|
+
out[key] = previewMonitoringValue(entry, state, `${path}.${keyProjection}`, depth + 1);
|
|
435
|
+
}
|
|
436
|
+
if (keep < entries.length) {
|
|
437
|
+
state.truncated = true;
|
|
438
|
+
if (state.details.length < 24) {
|
|
439
|
+
state.details.push(`${path}: ${entries.length - keep} object fields omitted`);
|
|
440
|
+
}
|
|
441
|
+
out.omittedFields = entries.length - keep;
|
|
442
|
+
}
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
234
445
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
446
|
+
function projectMonitoringContainer(
|
|
447
|
+
value: unknown,
|
|
448
|
+
stringBytes: number,
|
|
449
|
+
): { value: unknown; fact: SessionDetailFieldFact; details: string[] } {
|
|
450
|
+
const measurement = measureSessionEventJson(value);
|
|
451
|
+
const state: MonitoringPreviewState = {
|
|
452
|
+
remainingStringBytes: stringBytes,
|
|
453
|
+
remainingNodes: 128,
|
|
454
|
+
truncated: false,
|
|
455
|
+
details: [],
|
|
456
|
+
};
|
|
457
|
+
const preview = previewMonitoringValue(value, state);
|
|
458
|
+
const deliveredBytes = Buffer.byteLength(JSON.stringify(preview), "utf8");
|
|
459
|
+
const originalCount = Array.isArray(value)
|
|
460
|
+
? value.length
|
|
461
|
+
: value !== null && typeof value === "object"
|
|
462
|
+
? Object.keys(value).length
|
|
463
|
+
: undefined;
|
|
464
|
+
const deliveredCount = originalCount === undefined ? undefined : Math.min(24, originalCount);
|
|
465
|
+
const originalBytes = measurement.bytes;
|
|
466
|
+
const truncated =
|
|
467
|
+
state.truncated || originalBytes === null || (originalBytes ?? 0) !== deliveredBytes;
|
|
253
468
|
return {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
469
|
+
value: preview,
|
|
470
|
+
fact: {
|
|
471
|
+
truncated,
|
|
472
|
+
originalBytes,
|
|
473
|
+
deliveredBytes,
|
|
474
|
+
...(originalCount === undefined ? {} : { originalCount }),
|
|
475
|
+
...(deliveredCount === undefined ? {} : { deliveredCount }),
|
|
476
|
+
...(measurement.bytes === null ? { measurementBounded: true } : {}),
|
|
477
|
+
},
|
|
478
|
+
details: state.details,
|
|
257
479
|
};
|
|
258
480
|
}
|
|
259
481
|
|
|
260
|
-
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
482
|
+
/** Purpose-built, flat, model-facing detail projection for `session_get`. */
|
|
483
|
+
export function boundSessionDetailMcp(
|
|
484
|
+
session: Session,
|
|
485
|
+
effectiveControl: unknown = session.effectiveControl,
|
|
486
|
+
maxBytes = SESSION_DETAIL_MCP_MAX_BYTES,
|
|
487
|
+
) {
|
|
488
|
+
const title = session.title === null ? null : modelStringProjection(session.title, 512);
|
|
489
|
+
const initialMessage = modelStringProjection(session.initialMessage, 4_000);
|
|
490
|
+
const instructions =
|
|
491
|
+
session.instructions === null ? null : modelStringProjection(session.instructions, 4_000);
|
|
492
|
+
const metadata = projectMonitoringContainer(session.metadata, 3_000);
|
|
493
|
+
const resources = projectMonitoringContainer(session.resources, 3_000);
|
|
494
|
+
const tools = projectMonitoringContainer(session.tools, 4_000);
|
|
495
|
+
const mcpServers = projectMonitoringContainer(session.mcpServers, 3_000);
|
|
496
|
+
const permissions = projectMonitoringContainer(session.firstPartyMcpPermissions, 1_500);
|
|
497
|
+
const control = projectMonitoringContainer(effectiveControl, 2_000);
|
|
498
|
+
const fieldFacts: Record<string, SessionDetailFieldFact> = {
|
|
499
|
+
title: title?.fact ?? {
|
|
500
|
+
truncated: false,
|
|
501
|
+
originalBytes: 0,
|
|
502
|
+
deliveredBytes: 0,
|
|
503
|
+
},
|
|
504
|
+
initialMessage: initialMessage.fact,
|
|
505
|
+
instructions: instructions?.fact ?? {
|
|
506
|
+
truncated: false,
|
|
507
|
+
originalBytes: 0,
|
|
508
|
+
deliveredBytes: 0,
|
|
509
|
+
},
|
|
510
|
+
metadata: metadata.fact,
|
|
511
|
+
resources: resources.fact,
|
|
512
|
+
tools: tools.fact,
|
|
513
|
+
mcpServers: mcpServers.fact,
|
|
514
|
+
firstPartyMcpPermissions: permissions.fact,
|
|
515
|
+
effectiveControl: control.fact,
|
|
516
|
+
};
|
|
517
|
+
const details = [
|
|
518
|
+
...metadata.details,
|
|
519
|
+
...resources.details,
|
|
520
|
+
...tools.details,
|
|
521
|
+
...mcpServers.details,
|
|
522
|
+
...permissions.details,
|
|
523
|
+
...control.details,
|
|
524
|
+
].slice(0, 32);
|
|
525
|
+
const result = {
|
|
526
|
+
id: session.id,
|
|
527
|
+
workspaceId: session.workspaceId,
|
|
528
|
+
accountId: session.accountId,
|
|
529
|
+
status: session.status,
|
|
530
|
+
title: title?.value ?? null,
|
|
531
|
+
titleSource: session.titleSource,
|
|
532
|
+
initialMessage: initialMessage.value,
|
|
533
|
+
instructions: instructions?.value ?? null,
|
|
534
|
+
resources: resources.value,
|
|
535
|
+
tools: tools.value,
|
|
536
|
+
metadata: metadata.value,
|
|
537
|
+
model: modelStringProjection(session.model, 512).value,
|
|
538
|
+
sandboxBackend: modelStringProjection(session.sandboxBackend, 128).value,
|
|
539
|
+
sandboxOs: session.sandboxOs,
|
|
540
|
+
sandboxGroupId: session.sandboxGroupId,
|
|
541
|
+
activeSandboxId: session.activeSandboxId,
|
|
542
|
+
activeEpoch: session.activeEpoch,
|
|
543
|
+
variableSetId: session.variableSetId,
|
|
544
|
+
environmentId: session.environmentId,
|
|
545
|
+
rigId: session.rigId,
|
|
546
|
+
rigVersionId: session.rigVersionId,
|
|
547
|
+
firstPartyMcpPermissions: permissions.value,
|
|
548
|
+
mcpServers: mcpServers.value,
|
|
549
|
+
parentSessionId: session.parentSessionId,
|
|
550
|
+
createIdempotencyKey:
|
|
551
|
+
session.createIdempotencyKey === null
|
|
552
|
+
? null
|
|
553
|
+
: modelStringProjection(session.createIdempotencyKey, 512).value,
|
|
554
|
+
temporalWorkflowId:
|
|
555
|
+
session.temporalWorkflowId === null
|
|
556
|
+
? null
|
|
557
|
+
: modelStringProjection(session.temporalWorkflowId, 512).value,
|
|
558
|
+
activeTurnId: session.activeTurnId,
|
|
559
|
+
lastInputTokens: session.lastInputTokens,
|
|
560
|
+
queueVersion: session.queueVersion,
|
|
561
|
+
queueHeadPosition: session.queueHeadPosition,
|
|
562
|
+
queueTailPosition: session.queueTailPosition,
|
|
563
|
+
effectiveControl: control.value,
|
|
564
|
+
lastSequence: session.lastSequence,
|
|
565
|
+
codexPinnedCredentialId: session.codexPinnedCredentialId,
|
|
566
|
+
codexLastCredentialId: session.codexLastCredentialId,
|
|
567
|
+
pinned: session.pinned,
|
|
568
|
+
pinnedAt: session.pinnedAt,
|
|
569
|
+
pinVersion: session.pinVersion,
|
|
570
|
+
...(session.treeStats === undefined ? {} : { treeStats: session.treeStats }),
|
|
571
|
+
createdAt: session.createdAt,
|
|
572
|
+
updatedAt: session.updatedAt,
|
|
573
|
+
projection: {
|
|
574
|
+
truncated: Object.values(fieldFacts).some((fact) => fact.truncated),
|
|
575
|
+
fields: fieldFacts,
|
|
576
|
+
details,
|
|
577
|
+
bytes: 0,
|
|
578
|
+
maxBytes,
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
582
|
+
const measured = prettyJsonBytes(result);
|
|
583
|
+
if (result.projection.bytes === measured) break;
|
|
584
|
+
result.projection.bytes = measured;
|
|
585
|
+
}
|
|
586
|
+
const mutable = result as Record<string, any> & {
|
|
587
|
+
projection: typeof result.projection;
|
|
588
|
+
};
|
|
589
|
+
const fallbackContainers: Array<[string, SessionDetailFieldFact]> = [
|
|
590
|
+
["tools", fieldFacts.tools!],
|
|
591
|
+
["resources", fieldFacts.resources!],
|
|
592
|
+
["metadata", fieldFacts.metadata!],
|
|
593
|
+
["mcpServers", fieldFacts.mcpServers!],
|
|
594
|
+
["effectiveControl", fieldFacts.effectiveControl!],
|
|
595
|
+
["firstPartyMcpPermissions", fieldFacts.firstPartyMcpPermissions!],
|
|
596
|
+
];
|
|
597
|
+
for (const [field, fact] of fallbackContainers) {
|
|
598
|
+
if (result.projection.bytes <= maxBytes) break;
|
|
599
|
+
const omission = {
|
|
600
|
+
preview: `[${field} preview omitted at final session_get byte boundary]`,
|
|
601
|
+
...(fact.originalCount === undefined ? {} : { originalCount: fact.originalCount }),
|
|
602
|
+
};
|
|
603
|
+
mutable[field] = omission;
|
|
604
|
+
fact.truncated = true;
|
|
605
|
+
fact.deliveredBytes = Buffer.byteLength(JSON.stringify(omission), "utf8");
|
|
606
|
+
fact.deliveredCount = 0;
|
|
607
|
+
result.projection.truncated = true;
|
|
608
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
609
|
+
const measured = prettyJsonBytes(result);
|
|
610
|
+
if (result.projection.bytes === measured) break;
|
|
611
|
+
result.projection.bytes = measured;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (result.projection.bytes > maxBytes) {
|
|
615
|
+
throw new RangeError(`session_get projection exceeds its ${maxBytes}-byte envelope`);
|
|
616
|
+
}
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** @deprecated use boundSessionDetailMcp for the actual MCP response. */
|
|
266
621
|
export function capSessionDetail<T extends { metadata?: unknown; initialMessage?: unknown }>(
|
|
267
622
|
session: T,
|
|
268
623
|
perFieldChars: number = DEFAULT_SESSION_DETAIL_CHARS,
|
|
@@ -285,3 +640,168 @@ export function capSessionDetail<T extends { metadata?: unknown; initialMessage?
|
|
|
285
640
|
}
|
|
286
641
|
return changed ? out : session;
|
|
287
642
|
}
|
|
643
|
+
|
|
644
|
+
/** Bounded current definition plus compact-by-query historical rig summaries. */
|
|
645
|
+
export function boundRigDetailMcp(
|
|
646
|
+
rig: Rig,
|
|
647
|
+
versionsPage: { versions: unknown[]; total: number; hasMore: boolean },
|
|
648
|
+
changesPage: { changes: unknown[]; total: number; hasMore: boolean },
|
|
649
|
+
maxBytes = RIG_DETAIL_MCP_MAX_BYTES,
|
|
650
|
+
) {
|
|
651
|
+
const name = modelStringProjection(rig.name, 512);
|
|
652
|
+
const description =
|
|
653
|
+
rig.description === null ? null : modelStringProjection(rig.description, 2_000);
|
|
654
|
+
const active = rig.activeVersion;
|
|
655
|
+
const activeSetup =
|
|
656
|
+
active?.setupScript === null || active?.setupScript === undefined
|
|
657
|
+
? null
|
|
658
|
+
: modelStringProjection(active.setupScript, 8_000);
|
|
659
|
+
const activeImage =
|
|
660
|
+
active?.image === null || active?.image === undefined
|
|
661
|
+
? null
|
|
662
|
+
: modelStringProjection(active.image, 1_000);
|
|
663
|
+
const activeChangelog =
|
|
664
|
+
active?.changelog === null || active?.changelog === undefined
|
|
665
|
+
? null
|
|
666
|
+
: modelStringProjection(active.changelog, 2_000);
|
|
667
|
+
const activeChecks = projectMonitoringContainer(active?.checks ?? [], 5_000);
|
|
668
|
+
const activeHooks = projectMonitoringContainer(active?.credentialHooks ?? [], 1_500);
|
|
669
|
+
const activeVariableSets = projectMonitoringContainer(active?.defaultVariableSetIds ?? [], 1_500);
|
|
670
|
+
const versions = projectMonitoringContainer(versionsPage.versions, 4_000);
|
|
671
|
+
const changes = projectMonitoringContainer(changesPage.changes, 4_000);
|
|
672
|
+
const fieldFacts: Record<string, SessionDetailFieldFact> = {
|
|
673
|
+
name: name.fact,
|
|
674
|
+
description: description?.fact ?? {
|
|
675
|
+
truncated: false,
|
|
676
|
+
originalBytes: 0,
|
|
677
|
+
deliveredBytes: 0,
|
|
678
|
+
},
|
|
679
|
+
activeSetupScript: activeSetup?.fact ?? {
|
|
680
|
+
truncated: false,
|
|
681
|
+
originalBytes: 0,
|
|
682
|
+
deliveredBytes: 0,
|
|
683
|
+
},
|
|
684
|
+
activeImage: activeImage?.fact ?? {
|
|
685
|
+
truncated: false,
|
|
686
|
+
originalBytes: 0,
|
|
687
|
+
deliveredBytes: 0,
|
|
688
|
+
},
|
|
689
|
+
activeChangelog: activeChangelog?.fact ?? {
|
|
690
|
+
truncated: false,
|
|
691
|
+
originalBytes: 0,
|
|
692
|
+
deliveredBytes: 0,
|
|
693
|
+
},
|
|
694
|
+
activeChecks: activeChecks.fact,
|
|
695
|
+
activeCredentialHooks: activeHooks.fact,
|
|
696
|
+
activeDefaultVariableSetIds: activeVariableSets.fact,
|
|
697
|
+
versions: versions.fact,
|
|
698
|
+
changes: changes.fact,
|
|
699
|
+
};
|
|
700
|
+
const result = {
|
|
701
|
+
rig: {
|
|
702
|
+
id: rig.id,
|
|
703
|
+
accountId: rig.accountId,
|
|
704
|
+
workspaceId: rig.workspaceId,
|
|
705
|
+
name: name.value,
|
|
706
|
+
description: description?.value ?? null,
|
|
707
|
+
createdBy: rig.createdBy === null ? null : modelStringProjection(rig.createdBy, 512).value,
|
|
708
|
+
activeVersion: active
|
|
709
|
+
? {
|
|
710
|
+
id: active.id,
|
|
711
|
+
rigId: active.rigId,
|
|
712
|
+
version: active.version,
|
|
713
|
+
image: activeImage?.value ?? null,
|
|
714
|
+
setupScript: activeSetup?.value ?? null,
|
|
715
|
+
checks: activeChecks.value,
|
|
716
|
+
credentialHooks: activeHooks.value,
|
|
717
|
+
defaultVariableSetIds: activeVariableSets.value,
|
|
718
|
+
changelog: activeChangelog?.value ?? null,
|
|
719
|
+
createdBy:
|
|
720
|
+
active.createdBy === null ? null : modelStringProjection(active.createdBy, 512).value,
|
|
721
|
+
active: active.active,
|
|
722
|
+
createdAt: active.createdAt,
|
|
723
|
+
}
|
|
724
|
+
: null,
|
|
725
|
+
activeVersionHealth: rig.activeVersionHealth,
|
|
726
|
+
versionCount: rig.versionCount,
|
|
727
|
+
createdAt: rig.createdAt,
|
|
728
|
+
updatedAt: rig.updatedAt,
|
|
729
|
+
},
|
|
730
|
+
versions: versions.value,
|
|
731
|
+
versionsTotal: versionsPage.total,
|
|
732
|
+
versionsTruncated: versionsPage.hasMore || versions.fact.truncated,
|
|
733
|
+
changes: changes.value,
|
|
734
|
+
changesTotal: changesPage.total,
|
|
735
|
+
changesTruncated: changesPage.hasMore || changes.fact.truncated,
|
|
736
|
+
projection: {
|
|
737
|
+
truncated:
|
|
738
|
+
versionsPage.hasMore ||
|
|
739
|
+
changesPage.hasMore ||
|
|
740
|
+
Object.values(fieldFacts).some((fact) => fact.truncated),
|
|
741
|
+
fields: fieldFacts,
|
|
742
|
+
details: [
|
|
743
|
+
...activeChecks.details,
|
|
744
|
+
...activeHooks.details,
|
|
745
|
+
...activeVariableSets.details,
|
|
746
|
+
...versions.details,
|
|
747
|
+
...changes.details,
|
|
748
|
+
].slice(0, 32),
|
|
749
|
+
bytes: 0,
|
|
750
|
+
maxBytes,
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
754
|
+
const measured = prettyJsonBytes(result);
|
|
755
|
+
if (result.projection.bytes === measured) break;
|
|
756
|
+
result.projection.bytes = measured;
|
|
757
|
+
}
|
|
758
|
+
const mutable = result as Record<string, any> & {
|
|
759
|
+
projection: typeof result.projection;
|
|
760
|
+
};
|
|
761
|
+
const rigFallbacks: Array<{
|
|
762
|
+
target: Record<string, unknown>;
|
|
763
|
+
field: string;
|
|
764
|
+
fact: SessionDetailFieldFact;
|
|
765
|
+
}> = [
|
|
766
|
+
{
|
|
767
|
+
target: mutable.rig.activeVersion ?? {},
|
|
768
|
+
field: "checks",
|
|
769
|
+
fact: fieldFacts.activeChecks!,
|
|
770
|
+
},
|
|
771
|
+
{ target: mutable, field: "versions", fact: fieldFacts.versions! },
|
|
772
|
+
{ target: mutable, field: "changes", fact: fieldFacts.changes! },
|
|
773
|
+
{
|
|
774
|
+
target: mutable.rig.activeVersion ?? {},
|
|
775
|
+
field: "credentialHooks",
|
|
776
|
+
fact: fieldFacts.activeCredentialHooks!,
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
target: mutable.rig.activeVersion ?? {},
|
|
780
|
+
field: "defaultVariableSetIds",
|
|
781
|
+
fact: fieldFacts.activeDefaultVariableSetIds!,
|
|
782
|
+
},
|
|
783
|
+
];
|
|
784
|
+
for (const fallback of rigFallbacks) {
|
|
785
|
+
if (result.projection.bytes <= maxBytes) break;
|
|
786
|
+
const omission = {
|
|
787
|
+
preview: `[${fallback.field} preview omitted at final rig_get byte boundary]`,
|
|
788
|
+
...(fallback.fact.originalCount === undefined
|
|
789
|
+
? {}
|
|
790
|
+
: { originalCount: fallback.fact.originalCount }),
|
|
791
|
+
};
|
|
792
|
+
fallback.target[fallback.field] = omission;
|
|
793
|
+
fallback.fact.truncated = true;
|
|
794
|
+
fallback.fact.deliveredBytes = Buffer.byteLength(JSON.stringify(omission), "utf8");
|
|
795
|
+
fallback.fact.deliveredCount = 0;
|
|
796
|
+
result.projection.truncated = true;
|
|
797
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
798
|
+
const measured = prettyJsonBytes(result);
|
|
799
|
+
if (result.projection.bytes === measured) break;
|
|
800
|
+
result.projection.bytes = measured;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (result.projection.bytes > maxBytes) {
|
|
804
|
+
throw new RangeError(`rig_get projection exceeds its ${maxBytes}-byte envelope`);
|
|
805
|
+
}
|
|
806
|
+
return result;
|
|
807
|
+
}
|