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,43 @@
|
|
|
1
|
+
import { Archive } from "./fs.js";
|
|
2
|
+
import { type ObjectReader } from "./decode.js";
|
|
3
|
+
import { type FetchFn, type TransferBudget } from "./attachments.js";
|
|
4
|
+
import type { Entry } from "./core.js";
|
|
5
|
+
export type HydrateStats = {
|
|
6
|
+
needed: number;
|
|
7
|
+
recovered: number;
|
|
8
|
+
decode_failed: number;
|
|
9
|
+
failed: number;
|
|
10
|
+
cached_version: number;
|
|
11
|
+
skipped_deterministic: number;
|
|
12
|
+
version_adopted: number;
|
|
13
|
+
inline_observed: number;
|
|
14
|
+
inline_decode_failed: number;
|
|
15
|
+
omitted_observed: number;
|
|
16
|
+
blob_observed: number;
|
|
17
|
+
requests: number;
|
|
18
|
+
reread_pages: number;
|
|
19
|
+
archive_body_skipped: number;
|
|
20
|
+
network_bytes: number;
|
|
21
|
+
generation_changed: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare const hydrationCoverage: (rows: Entry[], readObject?: ObjectReader) => {
|
|
24
|
+
retained_rows: number;
|
|
25
|
+
observed_inline: number;
|
|
26
|
+
observed_omitted: number;
|
|
27
|
+
observed_blob_ref: number;
|
|
28
|
+
hydrated_version_bound: number;
|
|
29
|
+
decoded: number;
|
|
30
|
+
unavailable_or_unsupported: number;
|
|
31
|
+
current_membership: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function hydrateEntries(rows: Entry[], ctx: {
|
|
34
|
+
archive: Archive;
|
|
35
|
+
fetchImpl: FetchFn;
|
|
36
|
+
call: (method: string, request: any) => Promise<any>;
|
|
37
|
+
maxRequests: number;
|
|
38
|
+
maxBodyBytes: number;
|
|
39
|
+
budget: TransferBudget;
|
|
40
|
+
maxArchiveBytes?: number;
|
|
41
|
+
/** Seqs observed in this pull page set — hydrated before older archive rows. */
|
|
42
|
+
prioritySeqs?: Set<string>;
|
|
43
|
+
}): Promise<HydrateStats>;
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/** App 0.79.3 ge/Ee/j3: version-exact omitted reads and transcript-body blob reads.
|
|
2
|
+
* No Watch; no attachments. Opaque blob refs are NOT asserted to be SHA-256.
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { GbcError } from "../errors.js";
|
|
6
|
+
import { decodeBody, decodeStored, hydrationBinding, verifiedHydratedBytes, BODY_DECODE_MAX, } from "./decode.js";
|
|
7
|
+
import { downloadToStage, sha256Hex, } from "./attachments.js";
|
|
8
|
+
/** Same-version failures that must not burn hydrate_rpc_budget forever. */
|
|
9
|
+
const DETERMINISTIC = new Set([
|
|
10
|
+
"body_unavailable",
|
|
11
|
+
"exact_row_missing",
|
|
12
|
+
"body_version_changed",
|
|
13
|
+
"seq_successor_overflow",
|
|
14
|
+
"invalid_blob_reference",
|
|
15
|
+
"body_encoding",
|
|
16
|
+
"body_version_invalid",
|
|
17
|
+
]);
|
|
18
|
+
export const hydrationCoverage = (rows, readObject) => ({
|
|
19
|
+
retained_rows: rows.length,
|
|
20
|
+
observed_inline: rows.filter((e) => e.body_base64 !== undefined).length,
|
|
21
|
+
observed_omitted: rows.filter((e) => e.body_omitted).length,
|
|
22
|
+
observed_blob_ref: rows.filter((e) => !!e.blob_hash).length,
|
|
23
|
+
hydrated_version_bound: rows.filter((e) => !!verifiedHydratedBytes(e, readObject)).length,
|
|
24
|
+
decoded: rows.filter((e) => decodeStored(e, readObject).decode_status === "decoded").length,
|
|
25
|
+
unavailable_or_unsupported: rows.filter((e) => decodeStored(e, readObject).decode_status !== "decoded").length,
|
|
26
|
+
current_membership: "unknown; retained observations, deletes not consumed",
|
|
27
|
+
});
|
|
28
|
+
function jsonSafe(v) {
|
|
29
|
+
if (v?.toJson)
|
|
30
|
+
return v.toJson();
|
|
31
|
+
return JSON.parse(JSON.stringify(v, (_k, x) => typeof x === "bigint"
|
|
32
|
+
? String(x)
|
|
33
|
+
: x instanceof Uint8Array
|
|
34
|
+
? Buffer.from(x).toString("base64")
|
|
35
|
+
: x));
|
|
36
|
+
}
|
|
37
|
+
function reason(e) {
|
|
38
|
+
return e instanceof GbcError ? e.code : "body_read_failed";
|
|
39
|
+
}
|
|
40
|
+
function safeRef(ref) {
|
|
41
|
+
if (typeof ref !== "string" ||
|
|
42
|
+
!/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,255}$/.test(ref) ||
|
|
43
|
+
ref === "." ||
|
|
44
|
+
ref === "..")
|
|
45
|
+
throw new GbcError("invalid_blob_reference", "body blob reference must be a safe opaque segment");
|
|
46
|
+
return ref;
|
|
47
|
+
}
|
|
48
|
+
function uint64(v) {
|
|
49
|
+
if ((typeof v !== "string" && typeof v !== "bigint") ||
|
|
50
|
+
!/^(0|[1-9][0-9]*)$/.test(String(v)) ||
|
|
51
|
+
BigInt(v) > 18446744073709551615n)
|
|
52
|
+
throw new GbcError("body_version_invalid", "invalid exact row uint64");
|
|
53
|
+
return String(v);
|
|
54
|
+
}
|
|
55
|
+
function storeUrl(instruction, rel) {
|
|
56
|
+
if (instruction?.relPath !== rel || typeof instruction?.url !== "string")
|
|
57
|
+
throw new GbcError("presign_mapping", "body presign mapping mismatch");
|
|
58
|
+
const expiry = String(instruction.expiresAtMs ?? "");
|
|
59
|
+
if (!/^\d+$/.test(expiry) ||
|
|
60
|
+
!Number.isSafeInteger(Number(expiry)) ||
|
|
61
|
+
Number(expiry) <= Date.now() + 1000)
|
|
62
|
+
throw new GbcError("presign_expired", "body URL missing/invalid/expired expiry");
|
|
63
|
+
let u;
|
|
64
|
+
try {
|
|
65
|
+
u = new URL(instruction.url);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new GbcError("presign_url", "invalid body URL");
|
|
69
|
+
}
|
|
70
|
+
if (u.protocol !== "https:" ||
|
|
71
|
+
u.username ||
|
|
72
|
+
u.password ||
|
|
73
|
+
u.hash ||
|
|
74
|
+
(u.port && u.port !== "443"))
|
|
75
|
+
throw new GbcError("presign_url", "unusable body URL");
|
|
76
|
+
return instruction.url;
|
|
77
|
+
}
|
|
78
|
+
function archiveReader(archive) {
|
|
79
|
+
return (rel) => {
|
|
80
|
+
try {
|
|
81
|
+
const fd = fs.openSync(archive.file(rel), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
82
|
+
try {
|
|
83
|
+
const st = fs.fstatSync(fd);
|
|
84
|
+
if (st.nlink !== 1 || st.size > BODY_DECODE_MAX)
|
|
85
|
+
return;
|
|
86
|
+
return fs.readFileSync(fd);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Strip body bytes from reread JSON; body lives only in content-addressed object. */
|
|
98
|
+
function rereadMeta(r, bodyObject) {
|
|
99
|
+
const raw = jsonSafe(r);
|
|
100
|
+
if (raw && typeof raw === "object") {
|
|
101
|
+
delete raw.body;
|
|
102
|
+
delete raw.bodyBase64;
|
|
103
|
+
if (bodyObject)
|
|
104
|
+
raw.body_object = bodyObject;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
raw,
|
|
108
|
+
...(r.toBinary
|
|
109
|
+
? { wire_base64: Buffer.from(r.toBinary()).toString("base64") }
|
|
110
|
+
: {}),
|
|
111
|
+
...(bodyObject ?? {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function adoptNewerObservation(e, r) {
|
|
115
|
+
const body = r.body;
|
|
116
|
+
if (body != null && !(body instanceof Uint8Array))
|
|
117
|
+
throw new GbcError("body_encoding", "expected body bytes");
|
|
118
|
+
e.updated_seq = uint64(r.updatedSeq);
|
|
119
|
+
if (typeof r.entryId === "string" && r.entryId)
|
|
120
|
+
e.entry_id = r.entryId;
|
|
121
|
+
e.entry_kind = String(r.entryKind ?? e.entry_kind);
|
|
122
|
+
e.body_omitted = Boolean(r.bodyOmitted);
|
|
123
|
+
e.blob_hash = r.blobHash || undefined;
|
|
124
|
+
e.body_base64 =
|
|
125
|
+
body == null ? undefined : Buffer.from(body).toString("base64");
|
|
126
|
+
e.raw = jsonSafe(r);
|
|
127
|
+
e.wire_base64 = r.toBinary
|
|
128
|
+
? Buffer.from(r.toBinary()).toString("base64")
|
|
129
|
+
: undefined;
|
|
130
|
+
delete e.hydration;
|
|
131
|
+
delete e.decoded;
|
|
132
|
+
delete e.decode_reason;
|
|
133
|
+
delete e.body_source;
|
|
134
|
+
delete e.last_available_body;
|
|
135
|
+
delete e.attachment_local;
|
|
136
|
+
}
|
|
137
|
+
export async function hydrateEntries(rows, ctx) {
|
|
138
|
+
const stats = {
|
|
139
|
+
needed: 0,
|
|
140
|
+
recovered: 0,
|
|
141
|
+
decode_failed: 0,
|
|
142
|
+
failed: 0,
|
|
143
|
+
cached_version: 0,
|
|
144
|
+
skipped_deterministic: 0,
|
|
145
|
+
version_adopted: 0,
|
|
146
|
+
inline_observed: 0,
|
|
147
|
+
inline_decode_failed: 0,
|
|
148
|
+
omitted_observed: 0,
|
|
149
|
+
blob_observed: 0,
|
|
150
|
+
requests: 0,
|
|
151
|
+
reread_pages: 0,
|
|
152
|
+
archive_body_skipped: 0,
|
|
153
|
+
network_bytes: 0,
|
|
154
|
+
generation_changed: false,
|
|
155
|
+
};
|
|
156
|
+
const readObject = archiveReader(ctx.archive);
|
|
157
|
+
const start = ctx.budget.bytes;
|
|
158
|
+
const call = async (method, req) => {
|
|
159
|
+
if (ctx.budget.bytes >= ctx.budget.maxBytes)
|
|
160
|
+
throw new GbcError("resource_exhausted", "body byte budget exhausted");
|
|
161
|
+
if (stats.requests >= ctx.maxRequests)
|
|
162
|
+
throw new GbcError("hydrate_rpc_budget", "body read RPC budget exhausted");
|
|
163
|
+
if (Date.now() >= ctx.budget.deadline)
|
|
164
|
+
throw new GbcError("deadline_exceeded", "body read deadline");
|
|
165
|
+
stats.requests++;
|
|
166
|
+
if (method === "listGrokBotTranscriptEntries")
|
|
167
|
+
stats.reread_pages++;
|
|
168
|
+
const res = await ctx.call(method, req);
|
|
169
|
+
const bytes = Buffer.byteLength(JSON.stringify(jsonSafe(res)));
|
|
170
|
+
ctx.budget.bytes += bytes;
|
|
171
|
+
if (ctx.budget.bytes > ctx.budget.maxBytes)
|
|
172
|
+
throw new GbcError("resource_exhausted", "body read response budget exceeded");
|
|
173
|
+
return res;
|
|
174
|
+
};
|
|
175
|
+
const blobs = new Map();
|
|
176
|
+
const blob = async (e, ref) => {
|
|
177
|
+
const key = JSON.stringify([e.agent_id, e.session_id, e.generation, ref]);
|
|
178
|
+
let pending = blobs.get(key);
|
|
179
|
+
if (pending)
|
|
180
|
+
return pending;
|
|
181
|
+
pending = (async () => {
|
|
182
|
+
const rp = `blobs/${safeRef(ref)}`;
|
|
183
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
184
|
+
const presign = await call("presignSandBoxStoreReads", {
|
|
185
|
+
relPaths: [rp],
|
|
186
|
+
});
|
|
187
|
+
if (!Array.isArray(presign.instructions) ||
|
|
188
|
+
presign.instructions.length !== 1)
|
|
189
|
+
throw new GbcError("presign_mapping", "missing/duplicate/conflicting body presign instructions");
|
|
190
|
+
const url = storeUrl(presign.instructions[0], rp), stage = ctx.archive.stage();
|
|
191
|
+
try {
|
|
192
|
+
const transfer = await downloadToStage(url, {
|
|
193
|
+
maxBytes: Math.min(ctx.maxBodyBytes, BODY_DECODE_MAX),
|
|
194
|
+
budget: ctx.budget,
|
|
195
|
+
fetchImpl: ctx.fetchImpl,
|
|
196
|
+
sink: stage,
|
|
197
|
+
});
|
|
198
|
+
if (transfer.status === 403) {
|
|
199
|
+
if (attempt === 0)
|
|
200
|
+
continue;
|
|
201
|
+
throw new GbcError("permission_denied", "body URL rejected after bounded refresh");
|
|
202
|
+
}
|
|
203
|
+
if (transfer.status !== 200 || !transfer.digest)
|
|
204
|
+
throw new GbcError("body_http_status", "body response rejected");
|
|
205
|
+
const rel = `bodies/objects/sha256-${transfer.digest}`;
|
|
206
|
+
stage.commit(rel);
|
|
207
|
+
const bytes = readObject(rel);
|
|
208
|
+
if (!bytes || sha256Hex(bytes) !== transfer.digest)
|
|
209
|
+
throw new GbcError("body_integrity", "body object integrity mismatch");
|
|
210
|
+
return { bytes, digest: transfer.digest, rel };
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
stage.discard();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
throw new GbcError("body_read_failed", "body read exhausted");
|
|
217
|
+
})();
|
|
218
|
+
blobs.set(key, pending);
|
|
219
|
+
return pending;
|
|
220
|
+
};
|
|
221
|
+
const maxArchiveBytes = ctx.maxArchiveBytes ?? 64 * 1024 * 1024;
|
|
222
|
+
let archiveBytes = rows.reduce((n, e) => n + Buffer.byteLength(JSON.stringify(e)) + 1, 0);
|
|
223
|
+
const priority = ctx.prioritySeqs ?? new Set();
|
|
224
|
+
const ordered = [...rows].sort((a, b) => {
|
|
225
|
+
const ap = priority.has(a.seq) ? 0 : 1;
|
|
226
|
+
const bp = priority.has(b.seq) ? 0 : 1;
|
|
227
|
+
if (ap !== bp)
|
|
228
|
+
return ap - bp;
|
|
229
|
+
const as = BigInt(a.seq), bs = BigInt(b.seq);
|
|
230
|
+
return as < bs ? 1 : as > bs ? -1 : 0;
|
|
231
|
+
});
|
|
232
|
+
const publish = (e, h) => {
|
|
233
|
+
const originalSize = Buffer.byteLength(JSON.stringify(e));
|
|
234
|
+
const candidate = { ...e, hydration: h };
|
|
235
|
+
delete candidate.decoded;
|
|
236
|
+
delete candidate.decode_reason;
|
|
237
|
+
delete candidate.body_source;
|
|
238
|
+
Object.assign(candidate, decodeStored(candidate, readObject));
|
|
239
|
+
let delta = Buffer.byteLength(JSON.stringify(candidate)) - originalSize;
|
|
240
|
+
if (archiveBytes + delta > maxArchiveBytes) {
|
|
241
|
+
stats.archive_body_skipped++;
|
|
242
|
+
if (h.status === "fetched") {
|
|
243
|
+
stats.recovered--;
|
|
244
|
+
if (h.reason?.startsWith("decoder:"))
|
|
245
|
+
stats.decode_failed--;
|
|
246
|
+
stats.failed++;
|
|
247
|
+
h = {
|
|
248
|
+
format_version: 1,
|
|
249
|
+
status: "error",
|
|
250
|
+
binding: h.binding,
|
|
251
|
+
source: h.source,
|
|
252
|
+
context: h.context,
|
|
253
|
+
observed_at_ms: h.observed_at_ms,
|
|
254
|
+
reason: "archive_body_budget",
|
|
255
|
+
attempts: h.attempts ?? 1,
|
|
256
|
+
retry: "retryable",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
h = {
|
|
261
|
+
...h,
|
|
262
|
+
reason: "archive_body_budget",
|
|
263
|
+
retry: "retryable",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const minimal = { ...candidate, hydration: h };
|
|
267
|
+
delete minimal.decoded;
|
|
268
|
+
delete minimal.decode_reason;
|
|
269
|
+
delete minimal.body_source;
|
|
270
|
+
Object.assign(minimal, decodeStored(minimal, readObject));
|
|
271
|
+
delta = Buffer.byteLength(JSON.stringify(minimal)) - originalSize;
|
|
272
|
+
if (archiveBytes + delta <= maxArchiveBytes) {
|
|
273
|
+
delete e.decoded;
|
|
274
|
+
delete e.decode_reason;
|
|
275
|
+
delete e.body_source;
|
|
276
|
+
Object.assign(e, minimal);
|
|
277
|
+
archiveBytes += delta;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
delete e.decoded;
|
|
282
|
+
delete e.decode_reason;
|
|
283
|
+
delete e.body_source;
|
|
284
|
+
Object.assign(e, candidate);
|
|
285
|
+
archiveBytes += delta;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
for (const e of ordered) {
|
|
289
|
+
if (e.body_omitted)
|
|
290
|
+
stats.omitted_observed++;
|
|
291
|
+
if (e.blob_hash)
|
|
292
|
+
stats.blob_observed++;
|
|
293
|
+
if (e.body_base64 !== undefined) {
|
|
294
|
+
stats.inline_observed++;
|
|
295
|
+
if (decodeStored(e, readObject).decode_status !== "decoded")
|
|
296
|
+
stats.inline_decode_failed++;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
stats.needed++;
|
|
300
|
+
if (e.hydration && e.hydration.format_version !== 1) {
|
|
301
|
+
stats.failed++;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const bindingNow = hydrationBinding(e);
|
|
305
|
+
if (e.hydration?.status === "error" &&
|
|
306
|
+
e.hydration.retry === "never" &&
|
|
307
|
+
e.hydration.binding === bindingNow &&
|
|
308
|
+
DETERMINISTIC.has(e.hydration.reason ?? "")) {
|
|
309
|
+
stats.skipped_deterministic++;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const cached = verifiedHydratedBytes(e, readObject);
|
|
313
|
+
if (cached) {
|
|
314
|
+
stats.cached_version++;
|
|
315
|
+
if (decodeStored(e, readObject).decode_status !== "decoded")
|
|
316
|
+
stats.decode_failed++;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
let h = {
|
|
320
|
+
format_version: 1,
|
|
321
|
+
status: "error",
|
|
322
|
+
binding: bindingNow,
|
|
323
|
+
source: e.blob_hash ? "blob" : "omitted_list",
|
|
324
|
+
context: {
|
|
325
|
+
archive_scope_sha256: e.archive_scope_sha256 ?? "",
|
|
326
|
+
agent_id: e.agent_id,
|
|
327
|
+
session_id: e.session_id,
|
|
328
|
+
generation: e.generation,
|
|
329
|
+
seq: e.seq,
|
|
330
|
+
updated_seq: e.updated_seq,
|
|
331
|
+
entry_id: e.entry_id,
|
|
332
|
+
},
|
|
333
|
+
observed_at_ms: String(Date.now()),
|
|
334
|
+
attempts: (e.hydration?.attempts ?? 0) + 1,
|
|
335
|
+
retry: "retryable",
|
|
336
|
+
};
|
|
337
|
+
try {
|
|
338
|
+
let bytes, remote = e.blob_hash;
|
|
339
|
+
// App Ee: inline already handled; prefer blob ref; else omitted exact re-list.
|
|
340
|
+
if (!remote && e.body_omitted) {
|
|
341
|
+
const seq = BigInt(e.seq);
|
|
342
|
+
if (seq === 18446744073709551615n)
|
|
343
|
+
throw new GbcError("seq_successor_overflow", "cannot form before_seq=UINT64_MAX+1");
|
|
344
|
+
// App ge always passes generation, including 0.
|
|
345
|
+
const res = await call("listGrokBotTranscriptEntries", {
|
|
346
|
+
agentId: e.agent_id,
|
|
347
|
+
sessionId: e.session_id,
|
|
348
|
+
generation: e.generation,
|
|
349
|
+
beforeSeq: seq + 1n,
|
|
350
|
+
limit: 1,
|
|
351
|
+
});
|
|
352
|
+
if (res.generation !== e.generation) {
|
|
353
|
+
stats.generation_changed = true;
|
|
354
|
+
throw new GbcError("generation_changed", "omitted read changed generation");
|
|
355
|
+
}
|
|
356
|
+
if (!Array.isArray(res.entries) || res.entries.length !== 1)
|
|
357
|
+
throw new GbcError("exact_row_missing", "exact body row missing/conflicting");
|
|
358
|
+
const r = res.entries[0];
|
|
359
|
+
if (uint64(r.seq) !== e.seq)
|
|
360
|
+
throw new GbcError("body_version_changed", "omitted read does not match exact row/version");
|
|
361
|
+
if (String(r.entryKind ?? "") !== e.entry_kind)
|
|
362
|
+
throw new GbcError("body_version_changed", "omitted read does not match exact row/version");
|
|
363
|
+
if (e.entry_id !== undefined && r.entryId !== e.entry_id)
|
|
364
|
+
throw new GbcError("body_version_changed", "omitted read does not match exact row/version");
|
|
365
|
+
const remoteUpdated = uint64(r.updatedSeq);
|
|
366
|
+
if (BigInt(remoteUpdated) > BigInt(e.updated_seq)) {
|
|
367
|
+
// Same seq/identity with a newer version: merge then hydrate that version.
|
|
368
|
+
adoptNewerObservation(e, r);
|
|
369
|
+
stats.version_adopted++;
|
|
370
|
+
h.binding = hydrationBinding(e);
|
|
371
|
+
h.context = {
|
|
372
|
+
archive_scope_sha256: e.archive_scope_sha256 ?? "",
|
|
373
|
+
agent_id: e.agent_id,
|
|
374
|
+
session_id: e.session_id,
|
|
375
|
+
generation: e.generation,
|
|
376
|
+
seq: e.seq,
|
|
377
|
+
updated_seq: e.updated_seq,
|
|
378
|
+
entry_id: e.entry_id,
|
|
379
|
+
};
|
|
380
|
+
if (e.body_base64 !== undefined) {
|
|
381
|
+
// Newer observation brought inline body — retained as observation, not hydration.
|
|
382
|
+
Object.assign(e, decodeStored(e, readObject));
|
|
383
|
+
// Count as recovered coverage without inventing hydration present.
|
|
384
|
+
stats.recovered++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
remote = e.blob_hash;
|
|
388
|
+
if (!remote) {
|
|
389
|
+
h.reread = rereadMeta(r);
|
|
390
|
+
throw new GbcError("body_unavailable", "exact row still has no body/ref");
|
|
391
|
+
}
|
|
392
|
+
h.reread = rereadMeta(r);
|
|
393
|
+
}
|
|
394
|
+
else if (remoteUpdated !== e.updated_seq) {
|
|
395
|
+
throw new GbcError("body_version_changed", "omitted read does not match exact row/version");
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
// Exact same version — take body/blob from the reread.
|
|
399
|
+
if (r.body instanceof Uint8Array &&
|
|
400
|
+
r.body.length > Math.min(ctx.maxBodyBytes, BODY_DECODE_MAX))
|
|
401
|
+
throw new GbcError("resource_exhausted", "exact body exceeds bound");
|
|
402
|
+
if (r.body !== undefined && r.body !== null) {
|
|
403
|
+
if (!(r.body instanceof Uint8Array))
|
|
404
|
+
throw new GbcError("body_encoding", "expected body bytes");
|
|
405
|
+
bytes = Buffer.from(r.body);
|
|
406
|
+
// Defer reread meta until object hash is known (no body in raw JSON).
|
|
407
|
+
h._exact_reread_row = r;
|
|
408
|
+
}
|
|
409
|
+
else if (r.blobHash) {
|
|
410
|
+
remote = String(r.blobHash);
|
|
411
|
+
h.reread = rereadMeta(r);
|
|
412
|
+
}
|
|
413
|
+
else
|
|
414
|
+
throw new GbcError("body_unavailable", "exact row still has no body/ref");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (!bytes && remote) {
|
|
418
|
+
h.source = "blob";
|
|
419
|
+
h.remote_blob_ref = safeRef(remote);
|
|
420
|
+
const b = await blob(e, remote);
|
|
421
|
+
bytes = b.bytes;
|
|
422
|
+
h.content_sha256 = b.digest;
|
|
423
|
+
h.object_rel = b.rel;
|
|
424
|
+
}
|
|
425
|
+
if (!bytes)
|
|
426
|
+
throw new GbcError("body_unavailable", "no inline body or blob ref");
|
|
427
|
+
if (bytes.length > ctx.maxBodyBytes || bytes.length > BODY_DECODE_MAX)
|
|
428
|
+
throw new GbcError("resource_exhausted", "body exceeds decode limit");
|
|
429
|
+
h.status = "fetched";
|
|
430
|
+
h.content_sha256 = sha256Hex(bytes);
|
|
431
|
+
h.object_rel = `bodies/objects/sha256-${h.content_sha256}`;
|
|
432
|
+
// Persist once as content-addressed object; do not also inline body_base64.
|
|
433
|
+
if (h.source === "omitted_list" || !ctx.archive.read(h.object_rel))
|
|
434
|
+
ctx.archive.atomicBytes(h.object_rel, bytes);
|
|
435
|
+
const bodyObject = {
|
|
436
|
+
object_rel: h.object_rel,
|
|
437
|
+
content_sha256: h.content_sha256,
|
|
438
|
+
};
|
|
439
|
+
const exactRow = h._exact_reread_row;
|
|
440
|
+
delete h._exact_reread_row;
|
|
441
|
+
if (exactRow)
|
|
442
|
+
h.reread = rereadMeta(exactRow, bodyObject);
|
|
443
|
+
else if (h.reread) {
|
|
444
|
+
h.reread = {
|
|
445
|
+
...h.reread,
|
|
446
|
+
...bodyObject,
|
|
447
|
+
raw: h.reread.raw && typeof h.reread.raw === "object"
|
|
448
|
+
? { ...h.reread.raw, body_object: bodyObject }
|
|
449
|
+
: h.reread.raw,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
delete h.body_base64;
|
|
453
|
+
stats.recovered++;
|
|
454
|
+
const decoded = decodeBody(bytes, false, e.entry_kind);
|
|
455
|
+
if (decoded.decode_status !== "decoded") {
|
|
456
|
+
h.reason = `decoder:${decoded.decode_reason ?? decoded.decode_status}`;
|
|
457
|
+
stats.decode_failed++;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
h.reason = reason(error);
|
|
462
|
+
if (DETERMINISTIC.has(h.reason))
|
|
463
|
+
h.retry = "never";
|
|
464
|
+
stats.failed++;
|
|
465
|
+
}
|
|
466
|
+
publish(e, h);
|
|
467
|
+
if (stats.generation_changed)
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
stats.network_bytes = ctx.budget.bytes - start;
|
|
471
|
+
return stats;
|
|
472
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ObjectReader } from "./decode.js";
|
|
2
|
+
import type { Entry } from "./core.js";
|
|
3
|
+
/** Indented Markdown code, not HTML/links/instructions. Escape terminal control/bidi codes. */
|
|
4
|
+
export declare function dataBlock(s: string): string;
|
|
5
|
+
export declare function renderEntry(e: Entry, readObject?: ObjectReader): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { decodeStored } from "./decode.js";
|
|
2
|
+
/** Indented Markdown code, not HTML/links/instructions. Escape terminal control/bidi codes. */
|
|
3
|
+
export function dataBlock(s) {
|
|
4
|
+
const safe = s
|
|
5
|
+
.replace(/\r\n/g, "\n")
|
|
6
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u2028-\u202e\u2066-\u2069]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
|
|
7
|
+
return (safe
|
|
8
|
+
.split("\n")
|
|
9
|
+
.map((line) => " " + line)
|
|
10
|
+
.join("\n") + "\n");
|
|
11
|
+
}
|
|
12
|
+
function display(d) {
|
|
13
|
+
const meta = {
|
|
14
|
+
kind: d.kind,
|
|
15
|
+
body_id: d.body_id,
|
|
16
|
+
role: d.role,
|
|
17
|
+
role_source: d.role_source,
|
|
18
|
+
timestamp_ms: d.timestamp_ms,
|
|
19
|
+
tool_name: d.tool_name,
|
|
20
|
+
tool_status: d.tool_status,
|
|
21
|
+
attachment: d.attachment,
|
|
22
|
+
suppressed: d.suppressed,
|
|
23
|
+
};
|
|
24
|
+
let out = dataBlock(JSON.stringify(meta, null, 2)) + "\n";
|
|
25
|
+
if (d.timestamp_ms !== undefined)
|
|
26
|
+
out +=
|
|
27
|
+
"Timestamp: " +
|
|
28
|
+
new Date(Number(d.timestamp_ms)).toLocaleString("sv-SE", {
|
|
29
|
+
timeZone: "Asia/Shanghai",
|
|
30
|
+
hour12: false,
|
|
31
|
+
}) +
|
|
32
|
+
" (Asia/Shanghai)\n\n";
|
|
33
|
+
if (d.text !== undefined)
|
|
34
|
+
out +=
|
|
35
|
+
(d.kind === "tool-call"
|
|
36
|
+
? "Tool summary (not a complete tool result):\n\n"
|
|
37
|
+
: "Text:\n\n") +
|
|
38
|
+
dataBlock(d.text) +
|
|
39
|
+
"\n";
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
export function renderEntry(e, readObject) {
|
|
43
|
+
// Do not trust cached decoded fields or body.seq/session fields for source scope/order.
|
|
44
|
+
const b = decodeStored(e, readObject);
|
|
45
|
+
let out = `## seq ${e.seq}\n\n` +
|
|
46
|
+
dataBlock(JSON.stringify({
|
|
47
|
+
entry_id: e.entry_id,
|
|
48
|
+
entry_kind: e.entry_kind,
|
|
49
|
+
updated_seq: e.updated_seq,
|
|
50
|
+
decode_status: b.decode_status,
|
|
51
|
+
decode_reason: b.decode_reason,
|
|
52
|
+
encryption_status: b.encryption_status,
|
|
53
|
+
blob_hash: e.blob_hash,
|
|
54
|
+
body_source: b.body_source,
|
|
55
|
+
hydration: e.hydration
|
|
56
|
+
? {
|
|
57
|
+
status: e.hydration.status,
|
|
58
|
+
source: e.hydration.source,
|
|
59
|
+
reason: e.hydration.reason,
|
|
60
|
+
content_sha256: e.hydration.content_sha256,
|
|
61
|
+
context: e.hydration.context,
|
|
62
|
+
}
|
|
63
|
+
: undefined,
|
|
64
|
+
}, null, 2)) +
|
|
65
|
+
"\n";
|
|
66
|
+
out += b.decoded
|
|
67
|
+
? display(b.decoded)
|
|
68
|
+
: `[${b.decode_status}; ${b.decode_reason ?? "body unavailable"}; encryption unknown]\n\n`;
|
|
69
|
+
if (e.attachment_local) {
|
|
70
|
+
out +=
|
|
71
|
+
"Local attachment ref (untrusted metadata; not a URL):\n\n" +
|
|
72
|
+
dataBlock(JSON.stringify({
|
|
73
|
+
status: e.attachment_local.status,
|
|
74
|
+
relative_path: e.attachment_local.relative_path,
|
|
75
|
+
content_sha256: e.attachment_local.content_sha256,
|
|
76
|
+
size_bytes: e.attachment_local.size_bytes,
|
|
77
|
+
content_type: e.attachment_local.content_type,
|
|
78
|
+
reason: e.attachment_local.reason,
|
|
79
|
+
remote_path: e.attachment_local.remote_path,
|
|
80
|
+
}, null, 2)) +
|
|
81
|
+
"\n";
|
|
82
|
+
}
|
|
83
|
+
if (e.last_available_body &&
|
|
84
|
+
(b.decode_status === "omitted" || b.decode_status === "absent")) {
|
|
85
|
+
const prior = decodeStored({
|
|
86
|
+
...e,
|
|
87
|
+
body_omitted: false,
|
|
88
|
+
body_base64: e.last_available_body.body_base64,
|
|
89
|
+
});
|
|
90
|
+
// Version is data, not a heading/path, and cannot overwrite the current observation.
|
|
91
|
+
out +=
|
|
92
|
+
"### Earlier available body (NOT current revision)\n\n" +
|
|
93
|
+
dataBlock(JSON.stringify({ updated_seq: e.last_available_body.updated_seq })) +
|
|
94
|
+
"\n";
|
|
95
|
+
out += prior.decoded
|
|
96
|
+
? display(prior.decoded)
|
|
97
|
+
: `[${prior.decode_status}; earlier body unavailable to decoder]\n\n`;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createEmptyMockState } from "./mock-transport.js";
|
|
5
|
+
function statePath() {
|
|
6
|
+
return (process.env.GBT_MOCK_STATE ||
|
|
7
|
+
path.join(process.env.GBT_CREDENTIALS_DIR || path.join(os.homedir(), ".gbc"), "mock-state.json"));
|
|
8
|
+
}
|
|
9
|
+
function jsonReplacer(_k, v) {
|
|
10
|
+
if (typeof v === "bigint")
|
|
11
|
+
return v.toString();
|
|
12
|
+
return v;
|
|
13
|
+
}
|
|
14
|
+
export function loadMockState() {
|
|
15
|
+
const f = statePath();
|
|
16
|
+
try {
|
|
17
|
+
if (fs.existsSync(f)) {
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(f, "utf8"));
|
|
19
|
+
return {
|
|
20
|
+
...createEmptyMockState(),
|
|
21
|
+
...raw,
|
|
22
|
+
bots: raw.bots ?? [],
|
|
23
|
+
sections: raw.sections ?? [],
|
|
24
|
+
messages: raw.messages ?? [],
|
|
25
|
+
statuses: raw.statuses ?? [],
|
|
26
|
+
sessions: raw.sessions ?? [],
|
|
27
|
+
todos: raw.todos ?? [],
|
|
28
|
+
tasks: raw.tasks ?? [],
|
|
29
|
+
subagents: raw.subagents ?? [],
|
|
30
|
+
transcript: raw.transcript ?? [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* fallthrough */
|
|
36
|
+
}
|
|
37
|
+
return createEmptyMockState();
|
|
38
|
+
}
|
|
39
|
+
export function saveMockState(state) {
|
|
40
|
+
const f = statePath();
|
|
41
|
+
fs.mkdirSync(path.dirname(f), { recursive: true, mode: 0o700 });
|
|
42
|
+
const tmp = `${f}.${process.pid}.tmp`;
|
|
43
|
+
fs.writeFileSync(tmp, JSON.stringify(state, jsonReplacer));
|
|
44
|
+
fs.renameSync(tmp, f);
|
|
45
|
+
}
|