grokbot-control 0.0.0-stage → 0.1.3
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 +160 -2
- package/dist/auth/lock.d.ts +14 -0
- package/dist/auth/lock.js +71 -0
- package/dist/auth/provider.d.ts +89 -0
- package/dist/auth/provider.js +884 -0
- package/dist/auth/store.d.ts +25 -0
- package/dist/auth/store.js +106 -0
- package/dist/auth/types.d.ts +138 -0
- package/dist/auth/types.js +1 -0
- package/dist/cli/config.d.ts +26 -0
- package/dist/cli/config.js +98 -0
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +83 -0
- package/dist/cli/io.d.ts +6 -0
- package/dist/cli/io.js +61 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +275 -0
- package/dist/cli/sync.d.ts +4 -0
- package/dist/cli/sync.js +105 -0
- package/dist/connect/client.d.ts +32 -0
- package/dist/connect/client.js +257 -0
- package/dist/dto/result.d.ts +33 -0
- package/dist/dto/result.js +29 -0
- package/dist/dto/types.d.ts +63 -0
- package/dist/dto/types.js +2 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +64 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +11 -0
- package/dist/mcp/main.d.ts +2 -0
- package/dist/mcp/main.js +82 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +408 -0
- package/dist/mcp/zod-json.d.ts +2 -0
- package/dist/mcp/zod-json.js +4 -0
- package/dist/ops/auth.d.ts +48 -0
- package/dist/ops/auth.js +89 -0
- package/dist/ops/bot.d.ts +52 -0
- package/dist/ops/bot.js +211 -0
- package/dist/ops/limits.d.ts +10 -0
- package/dist/ops/limits.js +31 -0
- package/dist/ops/map.d.ts +7 -0
- package/dist/ops/map.js +57 -0
- package/dist/ops/message.d.ts +33 -0
- package/dist/ops/message.js +118 -0
- package/dist/ops/plane.d.ts +26 -0
- package/dist/ops/plane.js +33 -0
- package/dist/ops/progress.d.ts +34 -0
- package/dist/ops/progress.js +92 -0
- package/dist/ops/session.d.ts +13 -0
- package/dist/ops/session.js +34 -0
- package/dist/ops/sidebar.d.ts +70 -0
- package/dist/ops/sidebar.js +205 -0
- package/dist/sync/attachments.d.ts +76 -0
- package/dist/sync/attachments.js +274 -0
- package/dist/sync/core.d.ts +92 -0
- package/dist/sync/core.js +1027 -0
- package/dist/sync/decode.d.ts +83 -0
- package/dist/sync/decode.js +229 -0
- package/dist/sync/download-policy.d.ts +3 -0
- package/dist/sync/download-policy.js +103 -0
- package/dist/sync/fs.d.ts +24 -0
- package/dist/sync/fs.js +252 -0
- package/dist/sync/hydrate.d.ts +43 -0
- package/dist/sync/hydrate.js +472 -0
- package/dist/sync/render.d.ts +5 -0
- package/dist/sync/render.js +100 -0
- package/dist/testing/mock-persist.d.ts +3 -0
- package/dist/testing/mock-persist.js +45 -0
- package/dist/testing/mock-transport.d.ts +18 -0
- package/dist/testing/mock-transport.js +202 -0
- package/docs/acceptance.md +19 -0
- package/docs/architecture.md +128 -0
- package/docs/auth-lock.md +87 -0
- package/docs/implementation-plan.md +14 -0
- package/docs/implementation-status.md +92 -0
- package/docs/proto-drift.md +9 -0
- package/docs/protocol/README.md +34 -0
- package/docs/protocol/attachment-pagination-0.79.3.md +32 -0
- package/docs/protocol/auth-flow.md +51 -0
- package/docs/protocol/baseline-manifest.md +72 -0
- package/docs/protocol/baselines/full-schema-diff-0.78.4-to-0.79.2.txt +239 -0
- package/docs/protocol/baselines/full-schema-diff-0.78.4-to-0.79.3.txt +240 -0
- package/docs/protocol/baselines/full-schema-diff-0.79.2-to-0.79.3.txt +23 -0
- package/docs/protocol/baselines/schema-0.79.3/README.md +7 -0
- package/docs/protocol/baselines/schema-0.79.3/agent.v1.proto +5184 -0
- package/docs/protocol/baselines/schema-0.79.3/aiserver.v1.proto +18182 -0
- package/docs/protocol/baselines/schema-0.79.3/aiserver.v1_2.proto +54 -0
- package/docs/protocol/baselines/schema-0.79.3/anyrun.v1.proto +37 -0
- package/docs/protocol/baselines/schema-0.79.3/internapi.v1.proto +35 -0
- package/docs/protocol/baselines/schema-0.79.3/snapshot.json.gz +0 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.AgentStoreService.proto +34 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.AiService.proto +17 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.AnalyticsService.proto +17 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.BackgroundComposerService.proto +19 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.DashboardService.proto +101 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.GrokBotService.proto +473 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.SandBoxService.proto +64 -0
- package/docs/protocol/baselines/used-surface-0.78.4.json +3694 -0
- package/docs/protocol/baselines/used-surface-0.79.2.json +3835 -0
- package/docs/protocol/baselines/used-surface-0.79.3.json +3835 -0
- package/docs/protocol/baselines/used-surface-diff-0.78.4-to-0.79.2.md +34 -0
- package/docs/protocol/baselines/used-surface-diff-0.78.4-to-0.79.3.md +33 -0
- package/docs/protocol/compat-matrix.md +20 -0
- package/docs/protocol/static-restore.md +64 -0
- package/docs/protocol/surface-map.md +39 -0
- package/docs/protocol/transcript-body-0.79.3.md +98 -0
- package/docs/protocol/update-runbook.md +30 -0
- package/docs/protocol-map.md +106 -0
- package/docs/references.md +55 -0
- package/docs/requirements.md +26 -0
- package/docs/review.md +36 -0
- package/docs/sync-format.md +98 -0
- package/package.json +60 -4
- package/vendor/proto-gen/NOTICE.md +23 -0
- package/vendor/proto-gen/agent.v1_pb.js +9503 -0
- package/vendor/proto-gen/aiserver.v1_2_pb.js +105 -0
- package/vendor/proto-gen/aiserver.v1_pb.js +33163 -0
- package/vendor/proto-gen/anyrun.v1_pb.js +64 -0
- package/vendor/proto-gen/internapi.v1_pb.js +60 -0
- package/vendor/proto-gen/package.json +10 -0
- package/vendor/proto-gen/svc_aiserver.v1.GrokBotService_connect.js +4193 -0
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
import { hydrateEntries, hydrationCoverage } from "./hydrate.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { publicHttpsFetch } from "./download-policy.js";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { decodeStored, decodeBody, DECODER, } from "./decode.js";
|
|
6
|
+
import { renderEntry, dataBlock } from "./render.js";
|
|
7
|
+
import { GbcError } from "../errors.js";
|
|
8
|
+
import { okResult, failResult } from "../dto/result.js";
|
|
9
|
+
import { Archive, component, hash, sessionComponent } from "./fs.js";
|
|
10
|
+
import { ATTACHMENT_DOWNLOADER, DEFAULT_ATTACH, HARD_ATTACH, acceptStoreRead, assertRemotePath, downloadToStage, objectRel, redactAttachmentError, refRel, } from "./attachments.js";
|
|
11
|
+
export const DEFAULTS = {
|
|
12
|
+
maxPages: 100,
|
|
13
|
+
maxBytes: 32 * 1024 * 1024,
|
|
14
|
+
timeoutMs: 30000,
|
|
15
|
+
};
|
|
16
|
+
export function jsonSafe(value) {
|
|
17
|
+
if (value?.toJson)
|
|
18
|
+
return value.toJson();
|
|
19
|
+
return JSON.parse(JSON.stringify(value, (_k, v) => typeof v === "bigint"
|
|
20
|
+
? String(v)
|
|
21
|
+
: v instanceof Uint8Array
|
|
22
|
+
? Buffer.from(v).toString("base64")
|
|
23
|
+
: v));
|
|
24
|
+
}
|
|
25
|
+
function uint(v) {
|
|
26
|
+
if ((typeof v !== "bigint" && typeof v !== "string" && typeof v !== "number") ||
|
|
27
|
+
(typeof v === "number" && !Number.isSafeInteger(v)) ||
|
|
28
|
+
!/^\d+$/.test(String(v)))
|
|
29
|
+
throw new GbcError("invalid_argument", "invalid/unsafe uint64 in transcript");
|
|
30
|
+
const n = BigInt(v);
|
|
31
|
+
if (n > 18446744073709551615n)
|
|
32
|
+
throw new GbcError("invalid_argument", "uint64 out of range");
|
|
33
|
+
return String(n);
|
|
34
|
+
}
|
|
35
|
+
function generation(v) {
|
|
36
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v > 0xffffffff)
|
|
37
|
+
throw new GbcError("invalid_argument", "invalid generation");
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
function bounded(v, def, max) {
|
|
41
|
+
const n = v ?? def;
|
|
42
|
+
if (!Number.isInteger(n) || n < 1 || n > max)
|
|
43
|
+
throw new GbcError("invalid_argument", `sync bound must be 1..${max}`);
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
function scope(backend, i) {
|
|
47
|
+
if (!i.acknowledgeUnverifiedScope || !i.accountId || !i.teamId)
|
|
48
|
+
throw new GbcError("denied", "sync requires explicit stable account-id, team-id (use none if no team), and acknowledgement of unverified identity; auth API has no verified stable account ID");
|
|
49
|
+
component(i.accountId);
|
|
50
|
+
component(i.teamId);
|
|
51
|
+
if (i.accountId.includes("@"))
|
|
52
|
+
throw new GbcError("invalid_argument", "account-id must be a stable provider ID, not an email address");
|
|
53
|
+
const u = new URL(backend);
|
|
54
|
+
if (u.username || u.password || u.search || u.hash)
|
|
55
|
+
throw new GbcError("invalid_argument", "backend must not contain credentials/query/fragment");
|
|
56
|
+
return {
|
|
57
|
+
backend: backend.replace(/\/$/, ""),
|
|
58
|
+
account_id: i.accountId,
|
|
59
|
+
team_id: i.teamId,
|
|
60
|
+
assurance: "operator_asserted_unverified",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Canonical manifest binding; never a token/email-derived identity claim. */
|
|
64
|
+
function scopeKey(s) {
|
|
65
|
+
if (!s ||
|
|
66
|
+
![s.backend, s.account_id, s.team_id].every((v) => typeof v === "string" && v.length > 0) ||
|
|
67
|
+
s.assurance !== "operator_asserted_unverified")
|
|
68
|
+
throw new GbcError("failed_precondition", "archive has invalid scope metadata");
|
|
69
|
+
return hash(JSON.stringify([s.backend, s.account_id, s.team_id, s.assurance]));
|
|
70
|
+
}
|
|
71
|
+
function botDir(id) {
|
|
72
|
+
return `bots/${component(id)}`;
|
|
73
|
+
}
|
|
74
|
+
function sessionDir(a, s) {
|
|
75
|
+
return `${botDir(a)}/sessions/${sessionComponent(s)}`;
|
|
76
|
+
}
|
|
77
|
+
function genDir(a, s, g) {
|
|
78
|
+
return `${sessionDir(a, s)}/generations/${generation(g)}`;
|
|
79
|
+
}
|
|
80
|
+
function manifest(a, expected, create = false) {
|
|
81
|
+
const m = a.json("manifest.json");
|
|
82
|
+
if (!m && create) {
|
|
83
|
+
a.writeJson("manifest.json", {
|
|
84
|
+
format_version: 1,
|
|
85
|
+
scope: expected,
|
|
86
|
+
pagination_live_verified: false,
|
|
87
|
+
attachment_policy: "references_only",
|
|
88
|
+
sessions: [],
|
|
89
|
+
});
|
|
90
|
+
return a.json("manifest.json");
|
|
91
|
+
}
|
|
92
|
+
if (!m || m.format_version !== 1 || !Array.isArray(m.sessions))
|
|
93
|
+
throw new GbcError("failed_precondition", "not a supported sync archive");
|
|
94
|
+
if (m.sessions.length > 10000)
|
|
95
|
+
throw new GbcError("resource_exhausted", "archive session index bound exceeded");
|
|
96
|
+
for (const x of m.sessions) {
|
|
97
|
+
component(x.agent_id);
|
|
98
|
+
sessionComponent(x.session_id);
|
|
99
|
+
}
|
|
100
|
+
if (expected &&
|
|
101
|
+
["backend", "account_id", "team_id", "assurance"].some((k) => m.scope?.[k] !== expected[k]))
|
|
102
|
+
throw new GbcError("denied", "archive scope mismatch (backend/account/team); choose a separate root");
|
|
103
|
+
return m;
|
|
104
|
+
}
|
|
105
|
+
export function entryRecord(e, agent, session, g, archiveScope) {
|
|
106
|
+
const body = e.body;
|
|
107
|
+
if (body != null && !(body instanceof Uint8Array))
|
|
108
|
+
throw new GbcError("invalid_argument", "expected protobuf bytes, not guessed UTF-8");
|
|
109
|
+
return {
|
|
110
|
+
archive_scope_sha256: archiveScope,
|
|
111
|
+
agent_id: agent,
|
|
112
|
+
session_id: session,
|
|
113
|
+
generation: g,
|
|
114
|
+
seq: uint(e.seq),
|
|
115
|
+
updated_seq: uint(e.updatedSeq),
|
|
116
|
+
entry_id: e.entryId || undefined,
|
|
117
|
+
entry_kind: String(e.entryKind ?? ""),
|
|
118
|
+
body_base64: body == null ? undefined : Buffer.from(body).toString("base64"),
|
|
119
|
+
body_omitted: Boolean(e.bodyOmitted),
|
|
120
|
+
blob_hash: e.blobHash || undefined,
|
|
121
|
+
raw: jsonSafe(e),
|
|
122
|
+
wire_base64: e.toBinary
|
|
123
|
+
? Buffer.from(e.toBinary()).toString("base64")
|
|
124
|
+
: undefined,
|
|
125
|
+
untrusted: true,
|
|
126
|
+
...decodeBody(body, body == null && Boolean(e.bodyOmitted), String(e.entryKind ?? "")),
|
|
127
|
+
...(body != null ? { body_source: "observed_inline" } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function parseEntries(text, agent, session, g, archiveScope) {
|
|
131
|
+
const out = new Map();
|
|
132
|
+
for (const line of (text ?? "").split("\n")) {
|
|
133
|
+
if (!line)
|
|
134
|
+
continue;
|
|
135
|
+
const e = {
|
|
136
|
+
...JSON.parse(line),
|
|
137
|
+
archive_scope_sha256: archiveScope,
|
|
138
|
+
};
|
|
139
|
+
if (e.agent_id !== agent ||
|
|
140
|
+
e.session_id !== session ||
|
|
141
|
+
e.generation !== g ||
|
|
142
|
+
uint(e.seq) !== e.seq ||
|
|
143
|
+
uint(e.updated_seq) !== e.updated_seq ||
|
|
144
|
+
out.has(e.seq))
|
|
145
|
+
throw new GbcError("failed_precondition", "corrupt or wrong-scope local entries");
|
|
146
|
+
const { decoded: _decoded, decode_status: _status, decode_reason: _reason, encryption_status: _encryption, body_source: _bodySource, ...original } = e;
|
|
147
|
+
out.set(e.seq, { ...original, ...decodeStored(e) });
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
function merge(out, e) {
|
|
152
|
+
const old = out.get(e.seq);
|
|
153
|
+
if (!old || BigInt(e.updated_seq) > BigInt(old.updated_seq)) {
|
|
154
|
+
if (old && (e.body_omitted || e.body_base64 === undefined)) {
|
|
155
|
+
// An omitted newer observation must not erase a previously available body.
|
|
156
|
+
e.last_available_body =
|
|
157
|
+
old.body_base64 !== undefined && !old.body_omitted
|
|
158
|
+
? { updated_seq: old.updated_seq, body_base64: old.body_base64 }
|
|
159
|
+
: old.last_available_body;
|
|
160
|
+
}
|
|
161
|
+
if (old?.attachment_local) {
|
|
162
|
+
const a = decodeStored(old).decoded?.attachment?.file_path, b = decodeStored(e).decoded?.attachment?.file_path;
|
|
163
|
+
if (a !== undefined && a === b)
|
|
164
|
+
e.attachment_local = old.attachment_local;
|
|
165
|
+
}
|
|
166
|
+
out.set(e.seq, e);
|
|
167
|
+
}
|
|
168
|
+
else if (e.updated_seq === old.updated_seq &&
|
|
169
|
+
old.body_base64 === undefined &&
|
|
170
|
+
e.body_base64 !== undefined)
|
|
171
|
+
out.set(e.seq, e);
|
|
172
|
+
// Missing/older entries never delete or overwrite newer local versions.
|
|
173
|
+
}
|
|
174
|
+
function fail(e) {
|
|
175
|
+
// Never echo arbitrary RPC/error text (may contain signed URLs or secrets).
|
|
176
|
+
const rpcCode = e?.code;
|
|
177
|
+
const rpcCodes = {
|
|
178
|
+
1: "canceled",
|
|
179
|
+
3: "invalid_argument",
|
|
180
|
+
4: "deadline_exceeded",
|
|
181
|
+
5: "not_found",
|
|
182
|
+
7: "permission_denied",
|
|
183
|
+
8: "resource_exhausted",
|
|
184
|
+
14: "unavailable",
|
|
185
|
+
16: "unauthenticated",
|
|
186
|
+
};
|
|
187
|
+
if (!(e instanceof GbcError) &&
|
|
188
|
+
typeof rpcCode === "number" &&
|
|
189
|
+
rpcCodes[rpcCode])
|
|
190
|
+
return failResult(rpcCodes[rpcCode], "sync read RPC failed; original error text suppressed to avoid leaking credentials/URLs", { retryable: rpcCode === 14 || rpcCode === 8 });
|
|
191
|
+
const known = e instanceof GbcError;
|
|
192
|
+
return failResult(known ? e.code : "sync_failed", known
|
|
193
|
+
? e.message
|
|
194
|
+
: "sync/local IO failed; archive preserved; rerun from history head after resolving cause", { retryable: false, details: known ? e.details : undefined });
|
|
195
|
+
}
|
|
196
|
+
/** Shared CLI/MCP core. Normal read RPCs only, including opt-in store presign. No Watch or server writes. */
|
|
197
|
+
export class SyncOps {
|
|
198
|
+
client;
|
|
199
|
+
backend;
|
|
200
|
+
simulated;
|
|
201
|
+
fetchImpl;
|
|
202
|
+
constructor(client, backend, simulated = false, fetchImpl = publicHttpsFetch) {
|
|
203
|
+
this.client = client;
|
|
204
|
+
this.backend = backend;
|
|
205
|
+
this.simulated = simulated;
|
|
206
|
+
this.fetchImpl = fetchImpl;
|
|
207
|
+
}
|
|
208
|
+
async result(fn, code) {
|
|
209
|
+
try {
|
|
210
|
+
const data = await fn();
|
|
211
|
+
const result = okResult(data, { simulated: this.simulated, code });
|
|
212
|
+
if (data?.partial === true)
|
|
213
|
+
return {
|
|
214
|
+
...result,
|
|
215
|
+
ok: false,
|
|
216
|
+
outcome: "known_failure",
|
|
217
|
+
code: "partial",
|
|
218
|
+
error: {
|
|
219
|
+
message: "Archive retained, but requested body hydration is partial; inspect per-entry hydration reasons.",
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
return { ...fail(e), simulated: this.simulated };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
pull(i) {
|
|
229
|
+
return this.result(async () => {
|
|
230
|
+
const identity = scope(this.backend, i);
|
|
231
|
+
component(i.agentId);
|
|
232
|
+
if (i.sessionId !== undefined)
|
|
233
|
+
component(i.sessionId);
|
|
234
|
+
if (!path.isAbsolute(i.dir))
|
|
235
|
+
throw new GbcError("invalid_argument", "sync root must be absolute");
|
|
236
|
+
const maxPages = bounded(i.maxPages, DEFAULTS.maxPages, 1000), maxBytes = bounded(i.maxBytes, DEFAULTS.maxBytes, 256 * 1024 * 1024), timeoutMs = bounded(i.timeoutMs, DEFAULTS.timeoutMs, 300000);
|
|
237
|
+
const hydrate = i.hydrate ?? "off";
|
|
238
|
+
if (hydrate !== "off" && hydrate !== "missing")
|
|
239
|
+
throw new GbcError("invalid_argument", "hydrate must be off or missing");
|
|
240
|
+
const maxHydrateRequests = bounded(i.maxHydrateRequests, 32, 256), hydrateMaxBytes = bounded(i.hydrateMaxBytes, 8 * 1024 * 1024, 64 * 1024 * 1024), hydrateMaxBodyBytes = bounded(i.hydrateMaxBodyBytes, 4 * 1024 * 1024, 4 * 1024 * 1024);
|
|
241
|
+
if (i.dryRun)
|
|
242
|
+
return {
|
|
243
|
+
dry_run: true,
|
|
244
|
+
scope: identity,
|
|
245
|
+
would_pull_agent: i.agentId,
|
|
246
|
+
session_id: i.sessionId ?? null,
|
|
247
|
+
no_rpc_or_files: true,
|
|
248
|
+
hydrate,
|
|
249
|
+
};
|
|
250
|
+
const a = new Archive(i.dir, true), release = a.lock();
|
|
251
|
+
try {
|
|
252
|
+
const m = manifest(a, identity, true);
|
|
253
|
+
const deadline = Date.now() + timeoutMs;
|
|
254
|
+
let pages = 0, bytes = 0;
|
|
255
|
+
let apiRequests = 0;
|
|
256
|
+
const call = async (method, req) => {
|
|
257
|
+
const ms = deadline - Date.now();
|
|
258
|
+
if (ms <= 0)
|
|
259
|
+
throw new GbcError("deadline_exceeded", "sync time budget exhausted; rerun from head");
|
|
260
|
+
const ac = new AbortController();
|
|
261
|
+
let timer;
|
|
262
|
+
try {
|
|
263
|
+
apiRequests++;
|
|
264
|
+
return await Promise.race([
|
|
265
|
+
this.client[method](req, { timeoutMs: ms, signal: ac.signal }),
|
|
266
|
+
new Promise((_, reject) => {
|
|
267
|
+
timer = setTimeout(() => {
|
|
268
|
+
ac.abort();
|
|
269
|
+
reject(new GbcError("deadline_exceeded", "sync time budget exhausted; rerun from head"));
|
|
270
|
+
}, ms);
|
|
271
|
+
}),
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
let hydrateRequests = 0, hydrateSpent = 0, partial = false;
|
|
279
|
+
const botRes = await call("listGrokBotAgents", {});
|
|
280
|
+
const bot = (botRes.agents ?? []).find((b) => b.agentId === i.agentId || b.id === i.agentId);
|
|
281
|
+
if (!bot)
|
|
282
|
+
throw new GbcError(botRes.truncated ? "lookup_incomplete" : "not_found", botRes.truncated
|
|
283
|
+
? "agent list truncated; absence cannot establish nonexistence"
|
|
284
|
+
: "agent not returned by list RPC");
|
|
285
|
+
const sessionsRes = await call("listGrokBotAgentSessions", {
|
|
286
|
+
agentId: i.agentId,
|
|
287
|
+
});
|
|
288
|
+
let sessions = sessionsRes.sessions ?? [];
|
|
289
|
+
if (i.sessionId !== undefined)
|
|
290
|
+
sessions = sessions.filter((s) => s.sessionId === i.sessionId);
|
|
291
|
+
if (i.sessionId !== undefined && sessions.length !== 1)
|
|
292
|
+
throw new GbcError("not_found", "explicit session not returned uniquely by session RPC");
|
|
293
|
+
// The default conversation has empty session_id and is not returned by
|
|
294
|
+
// ListGrokBotAgentSessions (live 2026-11-07); include it for agent-wide pulls.
|
|
295
|
+
if (i.sessionId === undefined &&
|
|
296
|
+
!sessions.some((s) => s.sessionId === ""))
|
|
297
|
+
sessions.push({
|
|
298
|
+
agentId: i.agentId,
|
|
299
|
+
sessionId: "",
|
|
300
|
+
default_unlisted: true,
|
|
301
|
+
});
|
|
302
|
+
if (sessions.length > 50)
|
|
303
|
+
throw new GbcError("resource_exhausted", "more than 50 sessions; select --session-id");
|
|
304
|
+
const seenSessions = new Set();
|
|
305
|
+
for (const s of sessions) {
|
|
306
|
+
sessionComponent(s.sessionId);
|
|
307
|
+
if (s.agentId !== i.agentId || seenSessions.has(s.sessionId))
|
|
308
|
+
throw new GbcError("failed_precondition", "invalid/duplicate session scope");
|
|
309
|
+
seenSessions.add(s.sessionId);
|
|
310
|
+
}
|
|
311
|
+
const botJson = JSON.stringify({
|
|
312
|
+
agent_id: i.agentId,
|
|
313
|
+
raw: jsonSafe(bot),
|
|
314
|
+
untrusted: true,
|
|
315
|
+
}) + "\n";
|
|
316
|
+
if (Buffer.byteLength(botJson) > maxBytes)
|
|
317
|
+
throw new GbcError("resource_exhausted", "bot metadata exceeds byte bound");
|
|
318
|
+
a.atomic(`${botDir(i.agentId)}/bot.json`, botJson);
|
|
319
|
+
const summaries = [];
|
|
320
|
+
sessions.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
321
|
+
const rotation = Number.isInteger(m.next_session_index)
|
|
322
|
+
? m.next_session_index % Math.max(1, sessions.length)
|
|
323
|
+
: 0;
|
|
324
|
+
sessions = [
|
|
325
|
+
...sessions.slice(rotation),
|
|
326
|
+
...sessions.slice(0, rotation),
|
|
327
|
+
];
|
|
328
|
+
const pageSize = bounded(i.pageSize, 100, 500);
|
|
329
|
+
for (const [sessionIndex, s] of sessions.entries()) {
|
|
330
|
+
const sid = s.sessionId;
|
|
331
|
+
if (pages >= maxPages ||
|
|
332
|
+
bytes >= maxBytes ||
|
|
333
|
+
Date.now() >= deadline) {
|
|
334
|
+
summaries.push({
|
|
335
|
+
session_id: sid,
|
|
336
|
+
published: false,
|
|
337
|
+
pages: 0,
|
|
338
|
+
bytes: 0,
|
|
339
|
+
stop_reason: "command_budget_before_session",
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const priorSession = a.json(`${sessionDir(i.agentId, sid)}/session.json`);
|
|
344
|
+
const priorG = priorSession?.current_generation;
|
|
345
|
+
let previous, priorText;
|
|
346
|
+
if (Number.isInteger(priorG)) {
|
|
347
|
+
const pg = genDir(i.agentId, sid, priorG);
|
|
348
|
+
previous = a.json(`${pg}/state.json`);
|
|
349
|
+
priorText = a.read(`${pg}/entries.jsonl`);
|
|
350
|
+
if (priorText === undefined ||
|
|
351
|
+
previous?.entries_sha256 !== hash(priorText))
|
|
352
|
+
previous = undefined;
|
|
353
|
+
}
|
|
354
|
+
const startPages = pages, startBytes = bytes;
|
|
355
|
+
// Budget-one calls alternate HEAD refresh and continuation BACKFILL.
|
|
356
|
+
const resumeOnly = previous?.next_phase === "backfill" &&
|
|
357
|
+
previous?.backfill_before_seq != null;
|
|
358
|
+
let backfillCursor = previous?.backfill_before_seq ?? undefined;
|
|
359
|
+
let reachedOldest = previous?.backfill_end_observed === true;
|
|
360
|
+
let isHead = !resumeOnly, headObserved = false, pageAccepted = false;
|
|
361
|
+
let g = resumeOnly ? priorG : undefined, before = resumeOnly
|
|
362
|
+
? backfillCursor
|
|
363
|
+
: undefined, stop = "page_budget", fetched = new Map();
|
|
364
|
+
const pageMeta = [];
|
|
365
|
+
while (pages < maxPages &&
|
|
366
|
+
bytes < maxBytes &&
|
|
367
|
+
Date.now() < deadline) {
|
|
368
|
+
const res = await call("listGrokBotTranscriptEntries", {
|
|
369
|
+
agentId: i.agentId,
|
|
370
|
+
sessionId: sid,
|
|
371
|
+
limit: pageSize,
|
|
372
|
+
...(g === undefined || g === 0 ? {} : { generation: g }),
|
|
373
|
+
...(before === undefined ? {} : { beforeSeq: BigInt(before) }),
|
|
374
|
+
});
|
|
375
|
+
pages++;
|
|
376
|
+
const nextG = generation(res.generation);
|
|
377
|
+
if (g !== undefined && nextG !== g) {
|
|
378
|
+
stop = "generation_changed";
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
g = nextG;
|
|
382
|
+
if (isHead && priorG !== g) {
|
|
383
|
+
backfillCursor = undefined;
|
|
384
|
+
reachedOldest = false;
|
|
385
|
+
}
|
|
386
|
+
const responseSize = Buffer.byteLength(JSON.stringify(jsonSafe(res)));
|
|
387
|
+
if (bytes + responseSize > maxBytes) {
|
|
388
|
+
stop = "byte_budget";
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
const rows = (res.entries ?? []).map((e) => entryRecord(e, i.agentId, sid, g, scopeKey(m.scope)));
|
|
392
|
+
const size = Buffer.byteLength(rows.map((e) => JSON.stringify(e)).join("\n"));
|
|
393
|
+
if (bytes + Math.max(size, responseSize) > maxBytes) {
|
|
394
|
+
stop = "byte_budget";
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
bytes += Math.max(size, responseSize);
|
|
398
|
+
const { entries: _entries, ...meta } = jsonSafe(res);
|
|
399
|
+
pageMeta.push({ before_seq: before ?? null, response: meta });
|
|
400
|
+
if (!rows.length) {
|
|
401
|
+
pageAccepted = true;
|
|
402
|
+
if (isHead)
|
|
403
|
+
headObserved = true;
|
|
404
|
+
if (!isHead || backfillCursor === undefined)
|
|
405
|
+
reachedOldest = true;
|
|
406
|
+
stop = "empty_page_unverified";
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
let min;
|
|
410
|
+
for (const e of rows) {
|
|
411
|
+
const n = BigInt(e.seq);
|
|
412
|
+
min = min === undefined || n < min ? n : min;
|
|
413
|
+
}
|
|
414
|
+
if (before !== undefined &&
|
|
415
|
+
(min >= BigInt(before) ||
|
|
416
|
+
rows.some((e) => BigInt(e.seq) >= BigInt(before)))) {
|
|
417
|
+
stop = "cursor_no_progress_or_boundary_overlap";
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
pageAccepted = true;
|
|
421
|
+
if (isHead)
|
|
422
|
+
headObserved = true;
|
|
423
|
+
for (const e of rows)
|
|
424
|
+
merge(fetched, e);
|
|
425
|
+
if (isHead) {
|
|
426
|
+
if (backfillCursor === undefined ||
|
|
427
|
+
BigInt(backfillCursor) > min)
|
|
428
|
+
backfillCursor = String(min);
|
|
429
|
+
before = backfillCursor;
|
|
430
|
+
isHead = false;
|
|
431
|
+
if (reachedOldest) {
|
|
432
|
+
stop = "head_refreshed_history_previously_observed";
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
before = String(min);
|
|
438
|
+
backfillCursor = before;
|
|
439
|
+
}
|
|
440
|
+
if (min === 0n) {
|
|
441
|
+
stop = "seq_zero_unverified";
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
stop = "page_budget";
|
|
445
|
+
}
|
|
446
|
+
if (Date.now() >= deadline)
|
|
447
|
+
stop = "time_budget";
|
|
448
|
+
else if (bytes >= maxBytes)
|
|
449
|
+
stop = "byte_budget";
|
|
450
|
+
if (g === undefined) {
|
|
451
|
+
summaries.push({
|
|
452
|
+
session_id: sid,
|
|
453
|
+
stop_reason: stop,
|
|
454
|
+
published: false,
|
|
455
|
+
});
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
// Don't activate a generation which changed during the scan.
|
|
459
|
+
if (stop === "generation_changed") {
|
|
460
|
+
if (hydrate === "missing")
|
|
461
|
+
partial = true;
|
|
462
|
+
a.writeJson(`${sessionDir(i.agentId, sid)}/session.json`, {
|
|
463
|
+
agent_id: i.agentId,
|
|
464
|
+
session_id: sid,
|
|
465
|
+
current_generation: null,
|
|
466
|
+
raw: jsonSafe(s),
|
|
467
|
+
sync_status: "generation_changed; repull required",
|
|
468
|
+
untrusted: true,
|
|
469
|
+
});
|
|
470
|
+
summaries.push({
|
|
471
|
+
session_id: sid,
|
|
472
|
+
stop_reason: stop,
|
|
473
|
+
published: false,
|
|
474
|
+
});
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const gd = genDir(i.agentId, sid, g), oldText = a.read(`${gd}/entries.jsonl`);
|
|
478
|
+
const out = parseEntries(oldText, i.agentId, sid, g, scopeKey(m.scope));
|
|
479
|
+
const oldState = a.json(`${gd}/state.json`);
|
|
480
|
+
const recovered = oldText !== undefined &&
|
|
481
|
+
oldState?.entries_sha256 !== hash(oldText);
|
|
482
|
+
for (const e of fetched.values())
|
|
483
|
+
merge(out, e);
|
|
484
|
+
let hydration;
|
|
485
|
+
if (hydrate === "missing") {
|
|
486
|
+
const budget = {
|
|
487
|
+
bytes: 0,
|
|
488
|
+
maxBytes: Math.max(0, Math.min(maxBytes - bytes, hydrateMaxBytes - hydrateSpent)),
|
|
489
|
+
deadline,
|
|
490
|
+
};
|
|
491
|
+
hydration = await hydrateEntries([...out.values()], {
|
|
492
|
+
archive: a,
|
|
493
|
+
fetchImpl: this.fetchImpl,
|
|
494
|
+
call,
|
|
495
|
+
maxRequests: Math.max(0, maxHydrateRequests - hydrateRequests),
|
|
496
|
+
maxBodyBytes: hydrateMaxBodyBytes,
|
|
497
|
+
budget,
|
|
498
|
+
prioritySeqs: new Set(fetched.keys()),
|
|
499
|
+
});
|
|
500
|
+
hydrateRequests += hydration.requests;
|
|
501
|
+
hydrateSpent += budget.bytes;
|
|
502
|
+
bytes += budget.bytes;
|
|
503
|
+
// Decoder unsupported is counted separately and does NOT force exit 9.
|
|
504
|
+
if (hydration.failed > 0)
|
|
505
|
+
partial = true;
|
|
506
|
+
if (hydration.generation_changed) {
|
|
507
|
+
partial = true;
|
|
508
|
+
a.writeJson(`${sessionDir(i.agentId, sid)}/session.json`, {
|
|
509
|
+
agent_id: i.agentId,
|
|
510
|
+
session_id: sid,
|
|
511
|
+
current_generation: null,
|
|
512
|
+
sync_status: "generation_changed_during_body_read; repull required",
|
|
513
|
+
raw: jsonSafe(s),
|
|
514
|
+
untrusted: true,
|
|
515
|
+
});
|
|
516
|
+
summaries.push({
|
|
517
|
+
session_id: sid,
|
|
518
|
+
published: false,
|
|
519
|
+
stop_reason: "generation_changed_during_body_read",
|
|
520
|
+
hydration,
|
|
521
|
+
});
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const readObject = (rel) => {
|
|
526
|
+
try {
|
|
527
|
+
const fd = fs.openSync(a.file(rel), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
528
|
+
try {
|
|
529
|
+
const st = fs.fstatSync(fd);
|
|
530
|
+
if (st.nlink !== 1)
|
|
531
|
+
return;
|
|
532
|
+
return fs.readFileSync(fd);
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
fs.closeSync(fd);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
const bodyCoverage = hydrationCoverage([...out.values()], readObject);
|
|
543
|
+
const text = [...out.values()]
|
|
544
|
+
.sort((a, b) => (BigInt(a.seq) < BigInt(b.seq) ? -1 : 1))
|
|
545
|
+
.map((e) => JSON.stringify(e))
|
|
546
|
+
.join("\n") + (out.size ? "\n" : "");
|
|
547
|
+
if (Buffer.byteLength(text) > 64 * 1024 * 1024)
|
|
548
|
+
throw new GbcError("resource_exhausted", "session archive exceeds 64MiB snapshot bound; sharding not implemented");
|
|
549
|
+
const state = {
|
|
550
|
+
generation: g,
|
|
551
|
+
entries_sha256: hash(text),
|
|
552
|
+
entries: out.size,
|
|
553
|
+
fetched_entries: fetched.size,
|
|
554
|
+
observed_at_ms: String(Date.now()),
|
|
555
|
+
completeness: "unknown",
|
|
556
|
+
pagination_live_verified: false,
|
|
557
|
+
stop_reason: stop,
|
|
558
|
+
history_before_seq: before ?? null,
|
|
559
|
+
restart_policy: "alternating_head_and_backfill",
|
|
560
|
+
view: "retained_observations_not_current_mirror",
|
|
561
|
+
current_membership: "unknown; deletes not consumed",
|
|
562
|
+
head_refreshed_at_ms: headObserved
|
|
563
|
+
? String(Date.now())
|
|
564
|
+
: (previous?.head_refreshed_at_ms ?? null),
|
|
565
|
+
backfill_before_seq: backfillCursor ?? null,
|
|
566
|
+
backfill_end_observed: reachedOldest,
|
|
567
|
+
next_phase: !pageAccepted
|
|
568
|
+
? (previous?.next_phase ?? "head")
|
|
569
|
+
: reachedOldest
|
|
570
|
+
? "head"
|
|
571
|
+
: resumeOnly
|
|
572
|
+
? "head"
|
|
573
|
+
: "backfill",
|
|
574
|
+
session_pages: pages - startPages,
|
|
575
|
+
session_bytes: bytes - startBytes,
|
|
576
|
+
recovered_data_state_mismatch: recovered,
|
|
577
|
+
pages: pageMeta,
|
|
578
|
+
attachments: "references_only",
|
|
579
|
+
body_hydration_format: 1,
|
|
580
|
+
hydration_policy: hydrate,
|
|
581
|
+
body_coverage: bodyCoverage,
|
|
582
|
+
hydration,
|
|
583
|
+
body_partial: !!hydration &&
|
|
584
|
+
(hydration.failed > 0 || hydration.generation_changed),
|
|
585
|
+
decoder: DECODER,
|
|
586
|
+
decoded_entries: [...out.values()].filter((e) => e.decode_status === "decoded").length,
|
|
587
|
+
encryption: "per_entry; unknown unless validated plaintext JSON",
|
|
588
|
+
omitted_entries: [...out.values()].filter((e) => e.body_omitted)
|
|
589
|
+
.length,
|
|
590
|
+
};
|
|
591
|
+
if (Buffer.byteLength(JSON.stringify(state)) > 64 * 1024 * 1024)
|
|
592
|
+
throw new GbcError("resource_exhausted", "session state exceeds 64MiB bound");
|
|
593
|
+
// Entries are committed before state; checksum detects the inter-file crash window.
|
|
594
|
+
a.atomic(`${gd}/entries.jsonl`, text);
|
|
595
|
+
a.writeJson(`${gd}/state.json`, state);
|
|
596
|
+
a.writeJson(`${sessionDir(i.agentId, sid)}/session.json`, {
|
|
597
|
+
agent_id: i.agentId,
|
|
598
|
+
session_id: sid,
|
|
599
|
+
current_generation: g,
|
|
600
|
+
raw: jsonSafe(s),
|
|
601
|
+
untrusted: true,
|
|
602
|
+
});
|
|
603
|
+
if (!m.sessions.some((x) => x.agent_id === i.agentId && x.session_id === sid))
|
|
604
|
+
m.sessions.push({ agent_id: i.agentId, session_id: sid });
|
|
605
|
+
if (m.sessions.length > 10000)
|
|
606
|
+
throw new GbcError("resource_exhausted", "archive session index bound exceeded");
|
|
607
|
+
m.next_session_index =
|
|
608
|
+
(rotation + sessionIndex + 1) % Math.max(1, sessions.length);
|
|
609
|
+
a.writeJson("manifest.json", m);
|
|
610
|
+
summaries.push({
|
|
611
|
+
session_id: sid,
|
|
612
|
+
generation: g,
|
|
613
|
+
entries: out.size,
|
|
614
|
+
fetched_entries: fetched.size,
|
|
615
|
+
completeness: "unknown",
|
|
616
|
+
phase: resumeOnly ? "backfill" : "head_and_backfill",
|
|
617
|
+
backfill_before_seq: backfillCursor ?? null,
|
|
618
|
+
backfill_end_observed: reachedOldest,
|
|
619
|
+
pages: pages - startPages,
|
|
620
|
+
bytes: bytes - startBytes,
|
|
621
|
+
stop_reason: stop,
|
|
622
|
+
recovered,
|
|
623
|
+
body_coverage: bodyCoverage,
|
|
624
|
+
hydration,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
scope: identity,
|
|
629
|
+
pages,
|
|
630
|
+
bytes,
|
|
631
|
+
partial,
|
|
632
|
+
hydration_policy: hydrate,
|
|
633
|
+
hydrate_requests: hydrateRequests,
|
|
634
|
+
api_requests: apiRequests,
|
|
635
|
+
hydrate_bytes: hydrateSpent,
|
|
636
|
+
sessions: summaries,
|
|
637
|
+
note: "Retained observations, not a current or complete mirror. Body hydration is opt-in and version-bound; line coverage and body coverage are independent. No deletion, decryption or Watch. Attachments are separate. Pagination completeness remains unknown.",
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
finally {
|
|
641
|
+
release();
|
|
642
|
+
}
|
|
643
|
+
}, i.dryRun ? "dry_run" : undefined);
|
|
644
|
+
}
|
|
645
|
+
status(i) {
|
|
646
|
+
return this.result(async () => {
|
|
647
|
+
const a = new Archive(i.dir), m = manifest(a);
|
|
648
|
+
const selected = this.selected(m, i);
|
|
649
|
+
return {
|
|
650
|
+
scope: m.scope,
|
|
651
|
+
locked: a.read(".sync/lock") !== undefined,
|
|
652
|
+
sessions: selected.map((x) => {
|
|
653
|
+
const local = a.json(`${sessionDir(x.agent_id, x.session_id)}/session.json`);
|
|
654
|
+
if (local?.current_generation === null)
|
|
655
|
+
return {
|
|
656
|
+
...x,
|
|
657
|
+
generation: null,
|
|
658
|
+
consistent: false,
|
|
659
|
+
completeness: "unknown",
|
|
660
|
+
stop_reason: "generation_changed; repull required",
|
|
661
|
+
};
|
|
662
|
+
const gd = genDir(x.agent_id, x.session_id, generation(local?.current_generation));
|
|
663
|
+
const state = a.json(`${gd}/state.json`), text = a.read(`${gd}/entries.jsonl`);
|
|
664
|
+
return {
|
|
665
|
+
...x,
|
|
666
|
+
generation: local.current_generation,
|
|
667
|
+
entries: state?.entries,
|
|
668
|
+
body_coverage: text !== undefined
|
|
669
|
+
? hydrationCoverage([
|
|
670
|
+
...parseEntries(text, x.agent_id, x.session_id, generation(local.current_generation), scopeKey(m.scope)).values(),
|
|
671
|
+
], (rel) => {
|
|
672
|
+
try {
|
|
673
|
+
const fd = fs.openSync(a.file(rel), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
674
|
+
try {
|
|
675
|
+
if (fs.fstatSync(fd).nlink !== 1)
|
|
676
|
+
return;
|
|
677
|
+
return fs.readFileSync(fd);
|
|
678
|
+
}
|
|
679
|
+
finally {
|
|
680
|
+
fs.closeSync(fd);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
})
|
|
687
|
+
: null,
|
|
688
|
+
body_hydration_format: state?.body_hydration_format ?? null,
|
|
689
|
+
body_partial: state?.body_partial ?? null,
|
|
690
|
+
stop_reason: state?.stop_reason,
|
|
691
|
+
completeness: "unknown",
|
|
692
|
+
consistent: text !== undefined && state?.entries_sha256 === hash(text),
|
|
693
|
+
};
|
|
694
|
+
}),
|
|
695
|
+
selected_sessions: selected.length,
|
|
696
|
+
indexed_sessions: m.sessions.length,
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
selected(m, i) {
|
|
701
|
+
if (i.sessionId && !i.agentId)
|
|
702
|
+
throw new GbcError("invalid_argument", "session selector requires agent ID");
|
|
703
|
+
if (i.agentId)
|
|
704
|
+
component(i.agentId);
|
|
705
|
+
if (i.sessionId)
|
|
706
|
+
component(i.sessionId);
|
|
707
|
+
const rows = m.sessions.filter((x) => (!i.agentId || x.agent_id === i.agentId) &&
|
|
708
|
+
(!i.sessionId || x.session_id === i.sessionId));
|
|
709
|
+
if (rows.length > 50)
|
|
710
|
+
throw new GbcError("resource_exhausted", "select agent/session; local summary limited to 50 sessions");
|
|
711
|
+
return rows;
|
|
712
|
+
}
|
|
713
|
+
render(i) {
|
|
714
|
+
return this.result(async () => {
|
|
715
|
+
if (i.dryRun) {
|
|
716
|
+
if (!path.isAbsolute(i.dir))
|
|
717
|
+
throw new GbcError("invalid_argument", "sync root must be absolute");
|
|
718
|
+
return { dry_run: true, no_rpc_or_files: true };
|
|
719
|
+
}
|
|
720
|
+
const a = new Archive(i.dir), release = a.lock();
|
|
721
|
+
try {
|
|
722
|
+
const m = manifest(a), selected = this.selected(m, i);
|
|
723
|
+
for (const x of selected) {
|
|
724
|
+
const local = a.json(`${sessionDir(x.agent_id, x.session_id)}/session.json`), g = generation(local?.current_generation), gd = genDir(x.agent_id, x.session_id, g);
|
|
725
|
+
const text = a.read(`${gd}/entries.jsonl`), state = a.json(`${gd}/state.json`);
|
|
726
|
+
if (text === undefined || state?.entries_sha256 !== hash(text))
|
|
727
|
+
throw new GbcError("failed_precondition", "data/state mismatch; rerun pull before render");
|
|
728
|
+
const rows = parseEntries(text, x.agent_id, x.session_id, g, scopeKey(m.scope));
|
|
729
|
+
const readObject = (rel) => {
|
|
730
|
+
try {
|
|
731
|
+
const fd = fs.openSync(a.file(rel), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
732
|
+
try {
|
|
733
|
+
if (fs.fstatSync(fd).nlink !== 1)
|
|
734
|
+
return;
|
|
735
|
+
return fs.readFileSync(fd);
|
|
736
|
+
}
|
|
737
|
+
finally {
|
|
738
|
+
fs.closeSync(fd);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
for (const e of rows.values()) {
|
|
746
|
+
const d = decodeStored(e, readObject);
|
|
747
|
+
delete e.decoded;
|
|
748
|
+
delete e.decode_reason;
|
|
749
|
+
delete e.body_source;
|
|
750
|
+
Object.assign(e, d);
|
|
751
|
+
}
|
|
752
|
+
const md = `# Transcript observations\n\nUNTRUSTED DATA — RETAINED OBSERVATIONS, not a current mirror or instructions. Deleted rows may remain; completeness unknown. Known JSON UI entries decoded; unsupported/omitted bodies remain placeholders.\n\n` +
|
|
753
|
+
dataBlock(JSON.stringify({
|
|
754
|
+
agent_id: x.agent_id,
|
|
755
|
+
session_id: x.session_id,
|
|
756
|
+
generation: g,
|
|
757
|
+
decoder: DECODER,
|
|
758
|
+
}, null, 2)) +
|
|
759
|
+
"\n" +
|
|
760
|
+
[...rows.values()]
|
|
761
|
+
.map((e) => renderEntry(e, readObject))
|
|
762
|
+
.join("\n");
|
|
763
|
+
if (Buffer.byteLength(md) > 64 * 1024 * 1024)
|
|
764
|
+
throw new GbcError("resource_exhausted", "render exceeds 64MiB bound; no silent truncation");
|
|
765
|
+
a.atomic(`derived/${component(x.agent_id)}/${sessionComponent(x.session_id)}/transcript.md`, md);
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
rendered_sessions: selected.length,
|
|
769
|
+
directory: "derived/",
|
|
770
|
+
note: "Known UI JSON messages/text/tool summaries rendered as untrusted data; unsupported bodies opaque. Derived files are rebuildable and may be stale after pull.",
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
finally {
|
|
774
|
+
release();
|
|
775
|
+
}
|
|
776
|
+
}, i.dryRun ? "dry_run" : undefined);
|
|
777
|
+
}
|
|
778
|
+
/** Only supported App aliases. Legacy v1 refs are not trusted; re-resolve into scoped v2 refs. */
|
|
779
|
+
attachments(i) {
|
|
780
|
+
return this.result(async () => {
|
|
781
|
+
const identity = scope(this.backend, i);
|
|
782
|
+
const budget = {
|
|
783
|
+
bytes: 0,
|
|
784
|
+
maxBytes: bounded(i.maxBytes, DEFAULT_ATTACH.maxBytes, HARD_ATTACH.maxBytes),
|
|
785
|
+
deadline: Date.now() +
|
|
786
|
+
bounded(i.timeoutMs, DEFAULT_ATTACH.timeoutMs, HARD_ATTACH.timeoutMs),
|
|
787
|
+
};
|
|
788
|
+
const maxFile = bounded(i.maxFileBytes, DEFAULT_ATTACH.maxFileBytes, HARD_ATTACH.maxFileBytes);
|
|
789
|
+
if (!path.isAbsolute(i.dir))
|
|
790
|
+
throw new GbcError("invalid_argument", "sync root must be absolute");
|
|
791
|
+
const filter = i.path === undefined ? undefined : assertRemotePath(i.path);
|
|
792
|
+
if (i.dryRun)
|
|
793
|
+
return { dry_run: true, no_rpc_or_files: true };
|
|
794
|
+
const a = new Archive(i.dir);
|
|
795
|
+
manifest(a, identity);
|
|
796
|
+
const release = a.lock();
|
|
797
|
+
try {
|
|
798
|
+
const m = manifest(a, identity), selected = this.selected(m, i);
|
|
799
|
+
if (!selected.length)
|
|
800
|
+
throw new GbcError("not_found", "no archived sessions selected");
|
|
801
|
+
const results = [];
|
|
802
|
+
let updated = 0;
|
|
803
|
+
for (const x of selected) {
|
|
804
|
+
const g = generation(a.json(`${sessionDir(x.agent_id, x.session_id)}/session.json`)
|
|
805
|
+
?.current_generation);
|
|
806
|
+
const gd = genDir(x.agent_id, x.session_id, g), state = a.json(`${gd}/state.json`), old = a.read(`${gd}/entries.jsonl`);
|
|
807
|
+
if (old === undefined || state?.entries_sha256 !== hash(old))
|
|
808
|
+
throw new GbcError("failed_precondition", "data/state mismatch; repull first");
|
|
809
|
+
const rows = parseEntries(old, x.agent_id, x.session_id, g, scopeKey(m.scope));
|
|
810
|
+
const refs = new Map();
|
|
811
|
+
for (const e of rows.values()) {
|
|
812
|
+
const d = decodeStored(e).decoded;
|
|
813
|
+
if (!d?.attachment)
|
|
814
|
+
continue;
|
|
815
|
+
let rp;
|
|
816
|
+
try {
|
|
817
|
+
rp = assertRemotePath(d.attachment.file_path);
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
results.push({
|
|
821
|
+
agent_id: x.agent_id,
|
|
822
|
+
session_id: x.session_id,
|
|
823
|
+
seq: e.seq,
|
|
824
|
+
status: "unsupported",
|
|
825
|
+
reason: "unsupported_attachment_reference",
|
|
826
|
+
});
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (filter !== undefined && rp !== filter)
|
|
830
|
+
continue;
|
|
831
|
+
refs.set(rp, [...(refs.get(rp) ?? []), e]);
|
|
832
|
+
}
|
|
833
|
+
for (const [rp, entries] of refs) {
|
|
834
|
+
const rr = refRel(x.agent_id, rp, x.session_id);
|
|
835
|
+
let previous = a.json(rr), checked;
|
|
836
|
+
if (previous?.status === "ok" &&
|
|
837
|
+
/^[a-f0-9]{64}$/.test(previous.content_sha256 ?? "") &&
|
|
838
|
+
previous.relative_path ===
|
|
839
|
+
objectRel(x.agent_id, previous.content_sha256)) {
|
|
840
|
+
try {
|
|
841
|
+
const fd = fs.openSync(a.file(previous.relative_path), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
842
|
+
try {
|
|
843
|
+
const st = fs.fstatSync(fd);
|
|
844
|
+
if (st.size <= maxFile && st.nlink === 1) {
|
|
845
|
+
const h = (await import("node:crypto")).createHash("sha256"), buf = Buffer.alloc(65536);
|
|
846
|
+
let n;
|
|
847
|
+
while ((n = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
|
|
848
|
+
if (Date.now() >= budget.deadline)
|
|
849
|
+
throw new Error();
|
|
850
|
+
h.update(buf.subarray(0, n));
|
|
851
|
+
}
|
|
852
|
+
if (h.digest("hex") === previous.content_sha256 &&
|
|
853
|
+
st.size === previous.size_bytes)
|
|
854
|
+
checked = previous;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
finally {
|
|
858
|
+
fs.closeSync(fd);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
checked = undefined;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
let local;
|
|
866
|
+
try {
|
|
867
|
+
let complete = false;
|
|
868
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
869
|
+
const ms = budget.deadline - Date.now();
|
|
870
|
+
if (ms <= 0)
|
|
871
|
+
throw new GbcError("deadline_exceeded", "attachment deadline");
|
|
872
|
+
const ac = new AbortController(), timer = setTimeout(() => ac.abort(), ms);
|
|
873
|
+
let res;
|
|
874
|
+
try {
|
|
875
|
+
res = await Promise.race([
|
|
876
|
+
this.client.resolveGrokBotAttachmentReads({
|
|
877
|
+
agentId: x.agent_id,
|
|
878
|
+
paths: [rp],
|
|
879
|
+
...(x.session_id ? { sessionId: x.session_id } : {}),
|
|
880
|
+
}, { signal: ac.signal, timeoutMs: ms }),
|
|
881
|
+
new Promise((_, reject) => ac.signal.addEventListener("abort", () => reject(new GbcError("deadline_exceeded", "attachment deadline")), { once: true })),
|
|
882
|
+
]);
|
|
883
|
+
}
|
|
884
|
+
finally {
|
|
885
|
+
clearTimeout(timer);
|
|
886
|
+
}
|
|
887
|
+
if (!Array.isArray(res.reads) ||
|
|
888
|
+
res.reads.length !== 1 ||
|
|
889
|
+
res.reads[0].path !== rp)
|
|
890
|
+
throw new GbcError("failed_precondition", "conflicting attachment resolution");
|
|
891
|
+
const accepted = acceptStoreRead(rp, res.reads[0], Date.now());
|
|
892
|
+
if (!accepted.ok) {
|
|
893
|
+
local = {
|
|
894
|
+
remote_path: rp,
|
|
895
|
+
status: accepted.status,
|
|
896
|
+
reason: accepted.reason,
|
|
897
|
+
observed_at_ms: String(Date.now()),
|
|
898
|
+
live_verified: false,
|
|
899
|
+
};
|
|
900
|
+
complete = true;
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
const store = accepted.store;
|
|
904
|
+
const sink = a.stage();
|
|
905
|
+
try {
|
|
906
|
+
const transfer = await downloadToStage(store.url, {
|
|
907
|
+
maxBytes: maxFile,
|
|
908
|
+
expectedSize: store.size_bytes,
|
|
909
|
+
budget,
|
|
910
|
+
fetchImpl: this.fetchImpl,
|
|
911
|
+
etag: checked?.etag,
|
|
912
|
+
sink,
|
|
913
|
+
});
|
|
914
|
+
if (transfer.status === 403) {
|
|
915
|
+
if (attempt === 0)
|
|
916
|
+
continue;
|
|
917
|
+
throw new GbcError("permission_denied", "attachment URL rejected after refresh");
|
|
918
|
+
}
|
|
919
|
+
if (transfer.status === 304) {
|
|
920
|
+
if (!checked ||
|
|
921
|
+
checked.etag !== store.etag ||
|
|
922
|
+
checked.size_bytes !== store.size_bytes)
|
|
923
|
+
throw new GbcError("failed_precondition", "304 metadata mismatch");
|
|
924
|
+
local = {
|
|
925
|
+
...checked,
|
|
926
|
+
reason: "not_modified",
|
|
927
|
+
observed_at_ms: String(Date.now()),
|
|
928
|
+
live_verified: false,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
const rel = objectRel(x.agent_id, transfer.digest);
|
|
933
|
+
sink.commit(rel);
|
|
934
|
+
local = {
|
|
935
|
+
remote_path: rp,
|
|
936
|
+
status: "ok",
|
|
937
|
+
relative_path: rel,
|
|
938
|
+
content_sha256: transfer.digest,
|
|
939
|
+
size_bytes: transfer.size,
|
|
940
|
+
etag: store.etag,
|
|
941
|
+
content_type: store.content_type,
|
|
942
|
+
observed_at_ms: String(Date.now()),
|
|
943
|
+
live_verified: false,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
complete = true;
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
finally {
|
|
950
|
+
sink.discard();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (!complete)
|
|
954
|
+
throw new GbcError("unavailable", "attachment transfer incomplete");
|
|
955
|
+
}
|
|
956
|
+
catch (e) {
|
|
957
|
+
const red = redactAttachmentError(e);
|
|
958
|
+
local = {
|
|
959
|
+
remote_path: rp,
|
|
960
|
+
...red,
|
|
961
|
+
observed_at_ms: String(Date.now()),
|
|
962
|
+
live_verified: false,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
// Preserve old content address as historical, never claim it is the new current ref.
|
|
966
|
+
const historical = previous?.status === "ok" &&
|
|
967
|
+
previous.content_sha256 !== local.content_sha256
|
|
968
|
+
? {
|
|
969
|
+
content_sha256: previous.content_sha256,
|
|
970
|
+
relative_path: previous.relative_path,
|
|
971
|
+
}
|
|
972
|
+
: previous?.historical;
|
|
973
|
+
a.writeJson(rr, {
|
|
974
|
+
...local,
|
|
975
|
+
historical,
|
|
976
|
+
scope: identity,
|
|
977
|
+
agent_id: x.agent_id,
|
|
978
|
+
session_id: x.session_id,
|
|
979
|
+
downloader: ATTACHMENT_DOWNLOADER,
|
|
980
|
+
});
|
|
981
|
+
for (const e of entries)
|
|
982
|
+
e.attachment_local = local;
|
|
983
|
+
results.push({
|
|
984
|
+
agent_id: x.agent_id,
|
|
985
|
+
session_id: x.session_id,
|
|
986
|
+
...local,
|
|
987
|
+
etag: undefined,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
const text = [...rows.values()]
|
|
991
|
+
.sort((a, b) => (BigInt(a.seq) < BigInt(b.seq) ? -1 : 1))
|
|
992
|
+
.map((e) => JSON.stringify(e))
|
|
993
|
+
.join("\n") + (rows.size ? "\n" : "");
|
|
994
|
+
if (Buffer.byteLength(text) > 64 * 1024 * 1024)
|
|
995
|
+
throw new GbcError("resource_exhausted", "attachment archive exceeds 64MiB");
|
|
996
|
+
a.atomic(`${gd}/entries.jsonl`, text);
|
|
997
|
+
a.writeJson(`${gd}/state.json`, {
|
|
998
|
+
...state,
|
|
999
|
+
entries_sha256: hash(text),
|
|
1000
|
+
attachments: "on_demand_partial",
|
|
1001
|
+
view: "retained_observations_not_current_mirror",
|
|
1002
|
+
});
|
|
1003
|
+
updated++;
|
|
1004
|
+
}
|
|
1005
|
+
const good = results.filter((r) => r.status === "ok").length, bad = results.length - good;
|
|
1006
|
+
if (bad)
|
|
1007
|
+
throw new GbcError(good ? "partial" : "attachments_failed", "attachment results contain failures/unsupported references", {
|
|
1008
|
+
details: {
|
|
1009
|
+
results,
|
|
1010
|
+
bytes_consumed: budget.bytes,
|
|
1011
|
+
updated_sessions: updated,
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
return {
|
|
1015
|
+
results,
|
|
1016
|
+
requested_paths: results.length,
|
|
1017
|
+
bytes_consumed: budget.bytes,
|
|
1018
|
+
updated_sessions: updated,
|
|
1019
|
+
live_verified: false,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
finally {
|
|
1023
|
+
release();
|
|
1024
|
+
}
|
|
1025
|
+
}, i.dryRun ? "dry_run" : undefined);
|
|
1026
|
+
}
|
|
1027
|
+
}
|