failproofai 1.0.2-beta.3 → 1.0.2-beta.5
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -3
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -6
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +3 -4
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0e32-7c._.js → [externals]__1qzqcio._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__03r-lqq._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12021ot._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0igxt32._.js → [root-of-the-server]__1p8poxd._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1zkek3z._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +3 -0
- package/.next/standalone/.next/server/chunks/_12moi5-._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +6 -5
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js} +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js} +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1m84s36._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +4 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__08zpry-._.js → [root-of-the-server]__0stn0gx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{src_hooks_08o7bqo._.js → [root-of-the-server]__15r6r7w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0jzbk4_._.js → [root-of-the-server]__190won8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rsd3ll._.js → [root-of-the-server]__1sc84bb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tag8_-._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1db3-8n._.js → _1if9ixi._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_11opd9e._.js → _1kesqs-._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_15xtekm._.js → _1km9-gn._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_1cf5ya2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_15bjxkm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/middleware.js +1 -1
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
- package/.next/standalone/.next/static/chunks/0tyt-3a5rtzy8.js +1 -0
- package/.next/standalone/.next/static/chunks/{1mk3vmmc673hu.js → 21r7iw3rmixf5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{10pok-mrqv426.js → 26lfjhb2kzzgb.js} +1 -1
- package/.next/standalone/.next/static/chunks/2_27jraat038x.js +1 -0
- package/.next/standalone/.next/static/chunks/2hwc8e923g5df.js +1 -0
- package/.next/standalone/.next/static/chunks/{3cs_rf5p765v0.js → 33np9nrh1mvj9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1oml7c7env4-g.js → 354-n7p7labpn.js} +1 -1
- package/.next/standalone/.next/static/chunks/36-8i_vbkh85w.js +1 -0
- package/.next/standalone/.next/static/chunks/{2jsomd79g7kn0.js → 391tpdqcas7rq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1yywl_a87108w.js → 3_b73tc78jwsq.js} +2 -2
- package/.next/standalone/.next/static/chunks/{1za08zjcctyil.js → 3i8rg2fv9nd6e.js} +2 -2
- package/.next/standalone/.next/static/chunks/{2ey_ih6j6cm15.js → 3jqm8ubpv3i-4.js} +1 -1
- package/.next/standalone/.next/static/chunks/turbopack-2x-o099q64i8h.js +1 -0
- package/.next/standalone/SECURITY.md +18 -3
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +7 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +6 -6
- package/.next/standalone/lib/opencode-db.ts +77 -0
- package/.next/standalone/lib/opencode-projects.ts +37 -41
- package/.next/standalone/lib/opencode-sessions.ts +59 -36
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/cache.js +23 -34
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +10 -10
- package/.next/standalone/package.json +19 -19
- package/.next/standalone/sdk/python/CHANGELOG.md +7 -0
- package/.next/standalone/sdk/python/uv.lock +23 -23
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +151 -60
- package/dist/cli.mjs +3670 -3005
- package/dist/worker.mjs +189 -164
- package/lib/opencode-db.ts +77 -0
- package/lib/opencode-projects.ts +37 -41
- package/lib/opencode-sessions.ts +59 -36
- package/package.json +19 -19
- package/src/audit/cache.ts +201 -40
- package/src/audit/cli-adapters/claude.ts +111 -1
- package/src/audit/cli-adapters/index.ts +31 -1
- package/src/audit/index.ts +131 -11
- package/src/audit/replay.ts +15 -91
- package/src/hooks/configure-wizard.ts +16 -48
- package/src/hooks/handler.ts +8 -1
- package/src/hooks/manager.ts +72 -3
- package/src/hooks/pack-cli.ts +793 -45
- package/src/hooks/pack-manifest.ts +28 -0
- package/src/hooks/pack-store.ts +8 -25
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l-khtb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mhmdzs._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +0 -3
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +0 -3
- package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +0 -12
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +0 -5
- package/.next/standalone/.next/static/chunks/01nuy-aqshahr.js +0 -1
- package/.next/standalone/.next/static/chunks/1pl7h8vllvoi2.js +0 -1
- package/.next/standalone/.next/static/chunks/2efnffq2ipgr_.js +0 -1
- package/.next/standalone/.next/static/chunks/2gd07pinz0i7f.js +0 -1
- package/.next/standalone/.next/static/chunks/turbopack-3kjgdgwci4tqf.js +0 -1
- /package/.next/standalone/.next/static/{G-faMkpZioETgewZ4X4wX → 59jsH11LoyXu9UBoHvgGM}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{G-faMkpZioETgewZ4X4wX → 59jsH11LoyXu9UBoHvgGM}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{G-faMkpZioETgewZ4X4wX → 59jsH11LoyXu9UBoHvgGM}/_ssgManifest.js +0 -0
package/src/audit/cache.ts
CHANGED
|
@@ -10,35 +10,38 @@
|
|
|
10
10
|
* sessions live in a SQLite DB rather than a file with a stable mtime).
|
|
11
11
|
*/
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, openSync, readSync, closeSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { BUILTIN_POLICIES } from "../hooks/builtin-policies";
|
|
16
|
-
import { readInstalledPacks } from "../hooks/pack-manifest";
|
|
17
16
|
import { AUDIT_DETECTORS } from "./detectors";
|
|
18
|
-
import type { TranscriptAuditResult } from "./types";
|
|
17
|
+
import type { TranscriptAuditResult, DetectorSessionState } from "./types";
|
|
19
18
|
import { auditCacheDir } from "../hooks/fp-home";
|
|
20
19
|
|
|
21
20
|
let cachedEngineVersion: string | null = null;
|
|
22
21
|
let cachedDetectorVersion: string | null = null;
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
|
-
* Hash of every builtin policy's name
|
|
26
|
-
*
|
|
27
|
-
* caches.
|
|
24
|
+
* Hash of every builtin policy's name and function body. Changes when policy
|
|
25
|
+
* code changes, invalidating downstream caches.
|
|
28
26
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
27
|
+
* Nothing else is in it, and specifically not installed packs. The audit
|
|
28
|
+
* evaluates the implementations compiled into this build and only those —
|
|
29
|
+
* `replay.ts` iterates `BUILTIN_POLICIES` and never reads the installed packs —
|
|
30
|
+
* so a pack cannot move an audit result, and keying on one meant every install
|
|
31
|
+
* or removal cold-rescanned the entire history to reproduce the answers it
|
|
32
|
+
* already had. The key it replaced folded packs in by `id|version|sha256`; the
|
|
33
|
+
* rationale was that packs "change what a machine would have caught", which is
|
|
34
|
+
* true of enforcement and was never true of this replay.
|
|
36
35
|
*
|
|
37
|
-
* **A machine with no packs
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
36
|
+
* **A machine with no packs hashes byte-identically to the old key**, because
|
|
37
|
+
* that one contributed nothing for an empty pack set — not an empty line, not a
|
|
38
|
+
* separator. So this change costs a rescan only to machines that had a pack
|
|
39
|
+
* installed, and costs it exactly once.
|
|
40
|
+
*
|
|
41
|
+
* It is deliberately NOT the source text of anything loaded at runtime: the
|
|
42
|
+
* loader rewrites a per-load temporary filename into every import specifier, so
|
|
43
|
+
* hashing loaded source would change on every single run and cold-rescan the
|
|
44
|
+
* whole history each time.
|
|
42
45
|
*/
|
|
43
46
|
function getEngineVersion(): string {
|
|
44
47
|
if (cachedEngineVersion) return cachedEngineVersion;
|
|
@@ -46,19 +49,21 @@ function getEngineVersion(): string {
|
|
|
46
49
|
.map((p) => `${p.name}|${p.fn.toString()}`)
|
|
47
50
|
.sort()
|
|
48
51
|
.join("\n");
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
// Packs are NOT in this key, and that is the whole point of the audit's design
|
|
53
|
+
// rather than an omission.
|
|
54
|
+
//
|
|
55
|
+
// `replay.ts` evaluates `BUILTIN_POLICIES` and nothing else — it never reads
|
|
56
|
+
// `readInstalledPacks()`, and third-party pack policies are never replayed. So
|
|
57
|
+
// an installed pack cannot change an audit result, and folding its identity in
|
|
58
|
+
// here invalidated every cached transcript on a change that provably produces
|
|
59
|
+
// the same answers: a full rescan of thousands of transcripts to arrive back
|
|
60
|
+
// where it started. That was survivable while packs were rare; policies ARE
|
|
61
|
+
// packs now, so installing or removing one is routine.
|
|
62
|
+
//
|
|
63
|
+
// If audit is ever extended to evaluate what a pack would actually have
|
|
64
|
+
// caught, this key has to carry pack identity again — and that change belongs
|
|
65
|
+
// in the same commit as the one that makes it true.
|
|
66
|
+
cachedEngineVersion = createHash("sha1").update(builtinBlob).digest("hex").slice(0, 16);
|
|
62
67
|
return cachedEngineVersion;
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -105,7 +110,29 @@ function getCachePathFor(transcriptPath: string): string {
|
|
|
105
110
|
* accidentally on some paths) so we reject them outright and force a
|
|
106
111
|
* re-scan.
|
|
107
112
|
*/
|
|
108
|
-
|
|
113
|
+
/**
|
|
114
|
+
* v4: entries carry `bytesScanned`, `anchorHash` and `detectorState` so a
|
|
115
|
+
* transcript that GREW can be resumed from where the last scan stopped
|
|
116
|
+
* instead of being read from byte zero. v3 entries have none of the three;
|
|
117
|
+
* they are rejected rather than treated as "scanned 0 bytes", which would
|
|
118
|
+
* re-scan everything anyway but silently, and rather than as "scanned it
|
|
119
|
+
* all", which would drop every event appended since.
|
|
120
|
+
*/
|
|
121
|
+
export const CACHE_SCHEMA_VERSION = 4;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* How much of the head and of the resume boundary is hashed to decide the
|
|
125
|
+
* bytes already scanned are still the same bytes.
|
|
126
|
+
*
|
|
127
|
+
* A transcript is append-only in normal operation, so a grown file is
|
|
128
|
+
* ordinarily the same file plus more. "Ordinarily" is not a guarantee: a
|
|
129
|
+
* compaction can rewrite one in place. Re-reading the whole prefix to be sure
|
|
130
|
+
* would cost exactly what this change exists to avoid, so two cheap windows
|
|
131
|
+
* stand in for it — the first bytes of the file, and the last bytes before the
|
|
132
|
+
* offset being resumed from. A rewrite that leaves both intact and diverges
|
|
133
|
+
* only in between is not a shape any of these writers produce.
|
|
134
|
+
*/
|
|
135
|
+
const ANCHOR_WINDOW = 4096;
|
|
109
136
|
|
|
110
137
|
/** Hard expiry: a cached transcript result is rejected on read once it's
|
|
111
138
|
* this old. Keeps the audit pipeline honest about long-lived results that
|
|
@@ -136,39 +163,163 @@ interface CacheEntry {
|
|
|
136
163
|
engineVersion: string;
|
|
137
164
|
detectorVersion: string;
|
|
138
165
|
result: TranscriptAuditResult;
|
|
166
|
+
/**
|
|
167
|
+
* How far into the file the recorded result actually accounts for, at a LINE
|
|
168
|
+
* boundary. Not the same as `sizeBytes`: a transcript being appended to while
|
|
169
|
+
* the audit reads it ends in a partial line, which is not parsed and must not
|
|
170
|
+
* be counted as scanned or the events in it are lost forever.
|
|
171
|
+
*/
|
|
172
|
+
bytesScanned?: number;
|
|
173
|
+
/** Head + boundary fingerprint of the bytes already scanned. */
|
|
174
|
+
anchorHash?: string;
|
|
175
|
+
/** Stateful detectors' session state at `bytesScanned`, Map-aware encoded. */
|
|
176
|
+
detectorState?: string;
|
|
139
177
|
}
|
|
140
178
|
|
|
141
|
-
|
|
179
|
+
/** What a cache lookup can say. */
|
|
180
|
+
export type CacheLookup =
|
|
181
|
+
| { kind: "hit"; result: TranscriptAuditResult }
|
|
182
|
+
| {
|
|
183
|
+
kind: "resume";
|
|
184
|
+
result: TranscriptAuditResult;
|
|
185
|
+
fromByte: number;
|
|
186
|
+
detectorState: DetectorSessionState;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* `Map` does not survive `JSON.stringify` — it encodes as `{}`, silently. The
|
|
191
|
+
* one stateful detector keeps its state in one, so a plain round-trip would
|
|
192
|
+
* hand back an empty countdown and quietly change what the detector finds
|
|
193
|
+
* across a resume boundary. Encoded generically rather than per-detector so a
|
|
194
|
+
* detector added later cannot forget to do it.
|
|
195
|
+
*/
|
|
196
|
+
function encodeDetectorState(state: DetectorSessionState): string {
|
|
197
|
+
return JSON.stringify(state, (_k, v) =>
|
|
198
|
+
v instanceof Map ? { __map: [...v.entries()] } : v,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function decodeDetectorState(raw: string | undefined): DetectorSessionState {
|
|
203
|
+
if (!raw) return {};
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(raw, (_k, v) =>
|
|
206
|
+
v && typeof v === "object" && Array.isArray((v as { __map?: unknown }).__map)
|
|
207
|
+
? new Map((v as { __map: [unknown, unknown][] }).__map)
|
|
208
|
+
: v,
|
|
209
|
+
) as DetectorSessionState;
|
|
210
|
+
} catch {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A fingerprint of the head of the file and of the bytes immediately before
|
|
217
|
+
* `upTo`, which together stand in for "the prefix has not been rewritten".
|
|
218
|
+
* Returns null when the file cannot be read that far — which is itself an
|
|
219
|
+
* answer: do not resume.
|
|
220
|
+
*/
|
|
221
|
+
export function anchorHashFor(transcriptPath: string, upTo: number): string | null {
|
|
222
|
+
if (upTo <= 0) return null;
|
|
223
|
+
let fd: number | null = null;
|
|
224
|
+
try {
|
|
225
|
+
fd = openSync(transcriptPath, "r");
|
|
226
|
+
const head = Buffer.alloc(Math.min(ANCHOR_WINDOW, upTo));
|
|
227
|
+
readSync(fd, head, 0, head.length, 0);
|
|
228
|
+
const tailLen = Math.min(ANCHOR_WINDOW, upTo);
|
|
229
|
+
const tail = Buffer.alloc(tailLen);
|
|
230
|
+
readSync(fd, tail, 0, tailLen, upTo - tailLen);
|
|
231
|
+
return createHash("sha1")
|
|
232
|
+
.update(String(upTo))
|
|
233
|
+
.update(head)
|
|
234
|
+
.update(tail)
|
|
235
|
+
.digest("hex")
|
|
236
|
+
.slice(0, 32);
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
} finally {
|
|
240
|
+
if (fd !== null) {
|
|
241
|
+
try { closeSync(fd); } catch { /* nothing useful to do */ }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* What the cache can offer for this transcript: everything, the part before it
|
|
248
|
+
* grew, or nothing.
|
|
249
|
+
*
|
|
250
|
+
* The exact-match case is unchanged and still the common one. What is new is
|
|
251
|
+
* the middle answer. Keying validity on `(mtime, size)` alone made the cache
|
|
252
|
+
* all-or-nothing per FILE, so a session gaining a single line was re-parsed and
|
|
253
|
+
* re-replayed from byte zero — and the files that gain lines are the long-lived
|
|
254
|
+
* ones, which are also the largest. The cost of an audit tracked the size of
|
|
255
|
+
* whatever was still being written to, rather than the amount actually written
|
|
256
|
+
* since last time.
|
|
257
|
+
*/
|
|
258
|
+
export function readCachedTranscript(
|
|
142
259
|
transcriptPath: string,
|
|
143
260
|
mtimeMs: number,
|
|
144
261
|
sizeBytes: number,
|
|
145
|
-
):
|
|
146
|
-
if (sizeBytes === 0) return null; //
|
|
262
|
+
): CacheLookup | null {
|
|
263
|
+
if (sizeBytes === 0) return null; // DB-backed sources have no file to key on
|
|
147
264
|
const cachePath = getCachePathFor(transcriptPath);
|
|
148
265
|
if (!existsSync(cachePath)) return null;
|
|
149
266
|
try {
|
|
150
267
|
const raw = readFileSync(cachePath, "utf-8");
|
|
151
268
|
const entry = JSON.parse(raw) as Partial<CacheEntry>;
|
|
152
269
|
if (entry.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
|
|
153
|
-
if (entry.mtimeMs !== mtimeMs) return null;
|
|
154
|
-
if (entry.sizeBytes !== sizeBytes) return null;
|
|
155
270
|
if (entry.engineVersion !== getEngineVersion()) return null;
|
|
156
271
|
if (entry.detectorVersion !== getDetectorVersion()) return null;
|
|
157
272
|
// Number.isFinite (not typeof) so a malformed JSON `Infinity` /
|
|
158
273
|
// `NaN` is rejected — those would otherwise bypass the TTL check
|
|
159
274
|
// and pin a stale entry as valid forever.
|
|
160
275
|
if (!Number.isFinite(entry.cachedAt) || Date.now() - (entry.cachedAt as number) > CACHE_TTL_MS) return null;
|
|
161
|
-
|
|
276
|
+
if (!entry.result) return null;
|
|
277
|
+
|
|
278
|
+
if (entry.mtimeMs === mtimeMs && entry.sizeBytes === sizeBytes) {
|
|
279
|
+
return { kind: "hit", result: entry.result };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Grew. Everything below has to hold before any of the recorded result can
|
|
283
|
+
// be trusted, and each one failing means a full re-scan rather than a
|
|
284
|
+
// guess.
|
|
285
|
+
const from = entry.bytesScanned;
|
|
286
|
+
if (!Number.isFinite(from) || (from as number) <= 0) return null;
|
|
287
|
+
if ((from as number) > sizeBytes) return null; // truncated, or rewritten shorter
|
|
288
|
+
if (!entry.anchorHash) return null;
|
|
289
|
+
if (anchorHashFor(transcriptPath, from as number) !== entry.anchorHash) return null;
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
kind: "resume",
|
|
293
|
+
result: entry.result,
|
|
294
|
+
fromByte: from as number,
|
|
295
|
+
detectorState: decodeDetectorState(entry.detectorState),
|
|
296
|
+
};
|
|
162
297
|
} catch {
|
|
163
298
|
return null;
|
|
164
299
|
}
|
|
165
300
|
}
|
|
166
301
|
|
|
302
|
+
/**
|
|
303
|
+
* The exact-match half on its own, for callers that cannot resume.
|
|
304
|
+
*
|
|
305
|
+
* Kept because "is this transcript already fully accounted for" is a question
|
|
306
|
+
* worth being able to ask without also being handed a partial answer to merge.
|
|
307
|
+
*/
|
|
308
|
+
export function readCachedTranscriptResult(
|
|
309
|
+
transcriptPath: string,
|
|
310
|
+
mtimeMs: number,
|
|
311
|
+
sizeBytes: number,
|
|
312
|
+
): TranscriptAuditResult | null {
|
|
313
|
+
const found = readCachedTranscript(transcriptPath, mtimeMs, sizeBytes);
|
|
314
|
+
return found?.kind === "hit" ? found.result : null;
|
|
315
|
+
}
|
|
316
|
+
|
|
167
317
|
export function writeCachedTranscriptResult(
|
|
168
318
|
transcriptPath: string,
|
|
169
319
|
mtimeMs: number,
|
|
170
320
|
sizeBytes: number,
|
|
171
321
|
result: TranscriptAuditResult,
|
|
322
|
+
resume?: { bytesScanned: number; detectorState: DetectorSessionState },
|
|
172
323
|
): void {
|
|
173
324
|
if (sizeBytes === 0) return;
|
|
174
325
|
const cachePath = getCachePathFor(transcriptPath);
|
|
@@ -183,6 +334,18 @@ export function writeCachedTranscriptResult(
|
|
|
183
334
|
detectorVersion: getDetectorVersion(),
|
|
184
335
|
result,
|
|
185
336
|
};
|
|
337
|
+
if (resume && resume.bytesScanned > 0) {
|
|
338
|
+
const anchor = anchorHashFor(transcriptPath, resume.bytesScanned);
|
|
339
|
+
// No anchor means no way to prove the prefix later, so the entry is
|
|
340
|
+
// written WITHOUT resume data: still a valid exact-match entry, just not
|
|
341
|
+
// a resumable one. Recording an offset we cannot verify would be worse
|
|
342
|
+
// than recording none.
|
|
343
|
+
if (anchor) {
|
|
344
|
+
entry.bytesScanned = resume.bytesScanned;
|
|
345
|
+
entry.anchorHash = anchor;
|
|
346
|
+
entry.detectorState = encodeDetectorState(resume.detectorState);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
186
349
|
// Set 0o600 at file-creation time so there's no window where the file
|
|
187
350
|
// exists with the umask default (typically 0o644). The chmodSync below is
|
|
188
351
|
// a belt-and-suspenders pass for the case where the file already existed.
|
|
@@ -193,8 +356,6 @@ export function writeCachedTranscriptResult(
|
|
|
193
356
|
}
|
|
194
357
|
}
|
|
195
358
|
|
|
196
|
-
/** Test helper: reset memoized version hashes (so they recompute after a
|
|
197
|
-
* detector or policy is monkey-patched in a test). */
|
|
198
359
|
export function _resetVersionCache(): void {
|
|
199
360
|
cachedEngineVersion = null;
|
|
200
361
|
cachedDetectorVersion = null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* transcripts under <sessionId>/subagents/) via lib/claude-sessions.ts, then
|
|
6
6
|
* parses each via lib/log-entries.ts.
|
|
7
7
|
*/
|
|
8
|
-
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { readFile, open, type FileHandle } from "node:fs/promises";
|
|
9
9
|
import {
|
|
10
10
|
listClaudeProjects,
|
|
11
11
|
listClaudeTranscripts,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { parseLogContent, type LogSource } from "../../../lib/log-entries";
|
|
15
15
|
import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
|
|
16
16
|
import { logEntriesToEvents } from "./shared";
|
|
17
|
+
import type { IncrementalEvents } from "./index";
|
|
17
18
|
|
|
18
19
|
export interface ListOpts {
|
|
19
20
|
/** Restrict to sessions whose decoded cwd matches one of these paths. */
|
|
@@ -95,3 +96,112 @@ export async function streamClaudeEvents(
|
|
|
95
96
|
cwd,
|
|
96
97
|
});
|
|
97
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The events after `fromByte`, and nothing before it.
|
|
102
|
+
*
|
|
103
|
+
* Claude writes session transcripts as append-only JSONL, which is what makes
|
|
104
|
+
* this sound: the bytes before `fromByte` were parsed on an earlier run and
|
|
105
|
+
* have not moved. The caller has already established that — see
|
|
106
|
+
* `anchorHashFor` in the audit cache — so this reads from the offset and does
|
|
107
|
+
* not re-verify it.
|
|
108
|
+
*
|
|
109
|
+
* Two details carry the correctness:
|
|
110
|
+
*
|
|
111
|
+
* - The read starts at `fromByte`, which the caller only ever sets to a
|
|
112
|
+
* position it previously reported through `bytesConsumed`, so it always
|
|
113
|
+
* lands at the start of a line.
|
|
114
|
+
* - It stops at the last NEWLINE, not at end-of-file. A transcript is
|
|
115
|
+
* frequently being written while the audit reads it, so the tail is often
|
|
116
|
+
* half a line; parsing it would drop or mangle that event, and reporting
|
|
117
|
+
* the file size as consumed would mean the next run started after it and
|
|
118
|
+
* lost it for good.
|
|
119
|
+
*/
|
|
120
|
+
export async function streamClaudeEventsFrom(
|
|
121
|
+
meta: TranscriptMetadata,
|
|
122
|
+
fromByte: number,
|
|
123
|
+
): Promise<IncrementalEvents | null> {
|
|
124
|
+
if (!Number.isFinite(fromByte) || fromByte < 0) return null;
|
|
125
|
+
let handle: FileHandle | null = null;
|
|
126
|
+
let chunk: string;
|
|
127
|
+
try {
|
|
128
|
+
handle = await open(meta.transcriptPath, "r");
|
|
129
|
+
const { size } = await handle.stat();
|
|
130
|
+
if (fromByte > size) return null; // truncated under us; caller re-scans
|
|
131
|
+
if (fromByte === size) {
|
|
132
|
+
return { events: [], bytesConsumed: fromByte };
|
|
133
|
+
}
|
|
134
|
+
const buf = Buffer.alloc(size - fromByte);
|
|
135
|
+
await handle.read(buf, 0, buf.length, fromByte);
|
|
136
|
+
chunk = buf.toString("utf-8");
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
} finally {
|
|
140
|
+
if (handle) {
|
|
141
|
+
try { await handle.close(); } catch { /* nothing useful to do */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Everything up to the last newline is unambiguously complete. What follows
|
|
146
|
+
// it is either a line still being written, or the last line of a file that
|
|
147
|
+
// simply does not end in a newline — and those need opposite treatment:
|
|
148
|
+
// dropping the first is required, dropping the second loses the final event
|
|
149
|
+
// of every completed transcript.
|
|
150
|
+
//
|
|
151
|
+
// The only signal available in the bytes is whether the remainder PARSES. A
|
|
152
|
+
// half-written JSONL record does not (its braces are unclosed); a whole one
|
|
153
|
+
// missing only its newline does. So a parseable tail is taken and counted,
|
|
154
|
+
// and an unparseable one is left for the next run to pick up whole — which
|
|
155
|
+
// is also what stops it being counted twice when it arrives.
|
|
156
|
+
const lastNewline = chunk.lastIndexOf("\n");
|
|
157
|
+
const head = lastNewline === -1 ? "" : chunk.slice(0, lastNewline + 1);
|
|
158
|
+
const tail = chunk.slice(lastNewline + 1);
|
|
159
|
+
const tailIsWholeLine = tail.trim().length > 0 && (() => {
|
|
160
|
+
try {
|
|
161
|
+
JSON.parse(tail);
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
167
|
+
const complete = tailIsWholeLine ? head + tail : head;
|
|
168
|
+
if (complete.length === 0) {
|
|
169
|
+
// Nothing whole to parse, and the offset does not move.
|
|
170
|
+
return { events: [], bytesConsumed: fromByte };
|
|
171
|
+
}
|
|
172
|
+
// Byte length, not string length — a multi-byte character in the tail would
|
|
173
|
+
// otherwise put the next resume offset mid-character.
|
|
174
|
+
const bytesConsumed = fromByte + Buffer.byteLength(complete, "utf-8");
|
|
175
|
+
|
|
176
|
+
let entries;
|
|
177
|
+
try {
|
|
178
|
+
entries = await parseLogContent(complete, "session" as LogSource);
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let cwd = "";
|
|
184
|
+
for (const line of complete.split("\n", 50)) {
|
|
185
|
+
if (!line.trim()) continue;
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(line) as { cwd?: unknown };
|
|
188
|
+
if (typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
189
|
+
cwd = parsed.cwd;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// skip malformed lines
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
events: logEntriesToEvents(entries, {
|
|
199
|
+
cli: "claude",
|
|
200
|
+
sessionId: meta.sessionId,
|
|
201
|
+
transcriptPath: meta.transcriptPath,
|
|
202
|
+
cwd,
|
|
203
|
+
}),
|
|
204
|
+
bytesConsumed,
|
|
205
|
+
cwd: cwd || undefined,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -11,7 +11,7 @@ import type { IntegrationType } from "../../hooks/types";
|
|
|
11
11
|
import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
|
|
12
12
|
import type { ListOpts } from "./claude";
|
|
13
13
|
|
|
14
|
-
import { listClaudeTranscriptMetadata, streamClaudeEvents } from "./claude";
|
|
14
|
+
import { listClaudeTranscriptMetadata, streamClaudeEvents, streamClaudeEventsFrom } from "./claude";
|
|
15
15
|
import { listCodexTranscriptMetadata, streamCodexEvents } from "./codex";
|
|
16
16
|
import { listCopilotTranscriptMetadata, streamCopilotEvents } from "./copilot";
|
|
17
17
|
import { listCursorTranscriptMetadata, streamCursorEvents } from "./cursor";
|
|
@@ -26,10 +26,39 @@ import { listGooseTranscriptMetadata, streamGooseEvents } from "./goose";
|
|
|
26
26
|
|
|
27
27
|
export type { ListOpts };
|
|
28
28
|
|
|
29
|
+
/** What an append-only source can hand back when asked to resume. */
|
|
30
|
+
export interface IncrementalEvents {
|
|
31
|
+
events: NormalizedToolEvent[];
|
|
32
|
+
/**
|
|
33
|
+
* How far into the file `events` accounts for, at a LINE boundary — which is
|
|
34
|
+
* NOT the file size when a transcript is being appended to as it is read. The
|
|
35
|
+
* trailing partial line is left unparsed and uncounted so the next run picks
|
|
36
|
+
* it up whole; recording the file size instead would skip it permanently.
|
|
37
|
+
*/
|
|
38
|
+
bytesConsumed: number;
|
|
39
|
+
/** The session cwd, when the resumed region carried one. */
|
|
40
|
+
cwd?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
export interface CliAdapter {
|
|
30
44
|
cli: IntegrationType;
|
|
31
45
|
listTranscripts: (opts?: ListOpts) => Promise<TranscriptMetadata[]>;
|
|
32
46
|
streamEvents: (meta: TranscriptMetadata) => Promise<NormalizedToolEvent[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Read only the events after `fromByte`, for sources whose transcripts are
|
|
49
|
+
* append-only files.
|
|
50
|
+
*
|
|
51
|
+
* OPTIONAL, and deliberately a separate method rather than a parameter on
|
|
52
|
+
* `streamEvents`. A parameter an adapter ignored would return the WHOLE
|
|
53
|
+
* transcript to a caller that is about to merge it into an existing partial
|
|
54
|
+
* result — every event counted twice, quietly, on exactly the sources that
|
|
55
|
+
* had not been updated. Absence of this method is what tells the caller it
|
|
56
|
+
* cannot resume, and there is no way to be wrong about that by omission.
|
|
57
|
+
*/
|
|
58
|
+
streamEventsFrom?: (
|
|
59
|
+
meta: TranscriptMetadata,
|
|
60
|
+
fromByte: number,
|
|
61
|
+
) => Promise<IncrementalEvents | null>;
|
|
33
62
|
}
|
|
34
63
|
|
|
35
64
|
export const ADAPTERS: Record<IntegrationType, CliAdapter> = {
|
|
@@ -37,6 +66,7 @@ export const ADAPTERS: Record<IntegrationType, CliAdapter> = {
|
|
|
37
66
|
cli: "claude",
|
|
38
67
|
listTranscripts: listClaudeTranscriptMetadata,
|
|
39
68
|
streamEvents: streamClaudeEvents,
|
|
69
|
+
streamEventsFrom: streamClaudeEventsFrom,
|
|
40
70
|
},
|
|
41
71
|
codex: {
|
|
42
72
|
cli: "codex",
|