sphica 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/README.md +64 -52
- package/THIRD_PARTY_NOTICES.md +0 -53
- package/db/schema.sql +630 -283
- package/dist/capture.js +353 -183
- package/dist/cli.js +13673 -37191
- package/dist/deliver.js +31951 -0
- package/dist/mcp-record.js +48262 -0
- package/dist/mcp.js +780 -935
- package/hooks/codex.json +25 -0
- package/hooks/hooks.json +26 -11
- package/mcp/claude.json +9 -1
- package/mcp/codex.json +10 -1
- package/package.json +2 -2
- package/skills/glean/SKILL.md +82 -0
- package/skills/glean/agents/openai.yaml +2 -0
- package/skills/harvest/SKILL.md +64 -0
- package/skills/harvest/agents/openai.yaml +2 -0
- package/skills/review/SKILL.md +12 -13
- package/skills/review/reviewers/precedent.md +25 -38
- package/skills/trace/SKILL.md +72 -86
- package/db/migrations/0002_drop_artifact_rows.sql +0 -3
- package/db/migrations/0003_rebuild_source_item.sql +0 -45
- package/db/migrations/0004_knowledge_terms.sql +0 -46
- package/db/migrations/0005_terms_function.sql +0 -20
- package/skills/trace/example.json +0 -108
package/dist/capture.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// server/src/capture.ts
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import
|
|
5
|
+
import fs4 from "node:fs";
|
|
6
6
|
import os3 from "node:os";
|
|
7
|
-
import
|
|
7
|
+
import path5 from "node:path";
|
|
8
8
|
|
|
9
9
|
// server/node_modules/kysely/dist/util/object-utils.js
|
|
10
10
|
function isUndefined(obj) {
|
|
@@ -10813,7 +10813,8 @@ import fs from "node:fs";
|
|
|
10813
10813
|
import os from "node:os";
|
|
10814
10814
|
import path from "node:path";
|
|
10815
10815
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
10816
|
-
var
|
|
10816
|
+
var SCHEMA_GENERATION = 2;
|
|
10817
|
+
var SCHEMA_REVISION = 1;
|
|
10817
10818
|
var dbFile = () => process.env.SPHICA_DB || path.join(os.homedir(), ".sphica", "sphica.db");
|
|
10818
10819
|
function requireRuntime() {
|
|
10819
10820
|
const proto = DatabaseSync.prototype;
|
|
@@ -10824,18 +10825,35 @@ function requireFile(file) {
|
|
|
10824
10825
|
if (!fs.existsSync(file))
|
|
10825
10826
|
throw new Error(`No database at ${file}. Create it with \`sphica init\`.`);
|
|
10826
10827
|
}
|
|
10827
|
-
function prepare(raw,
|
|
10828
|
+
function prepare(raw, check) {
|
|
10828
10829
|
raw.enableDefensive(true);
|
|
10829
10830
|
raw.exec("pragma foreign_keys = on");
|
|
10830
10831
|
raw.exec("pragma busy_timeout = 5000");
|
|
10831
|
-
if (
|
|
10832
|
+
if (check === "none")
|
|
10833
|
+
return;
|
|
10834
|
+
checkGeneration(raw);
|
|
10835
|
+
if (check === "generation")
|
|
10832
10836
|
return;
|
|
10833
10837
|
const got = raw.prepare("pragma user_version").get()?.user_version;
|
|
10834
10838
|
if (got === SCHEMA_REVISION)
|
|
10835
10839
|
return;
|
|
10836
|
-
|
|
10840
|
+
throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "Move it aside (it is left unchanged), then run `sphica init`." : "Update sphica."));
|
|
10841
|
+
}
|
|
10842
|
+
function generationOf(raw) {
|
|
10843
|
+
const has = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'sphica_generation'").get();
|
|
10844
|
+
if (!has) {
|
|
10845
|
+
const any = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'project'").get();
|
|
10846
|
+
return any ? 1 : null;
|
|
10847
|
+
}
|
|
10848
|
+
return raw.prepare("select generation from sphica_generation").get()?.generation ?? null;
|
|
10849
|
+
}
|
|
10850
|
+
function checkGeneration(raw) {
|
|
10851
|
+
const got = generationOf(raw);
|
|
10852
|
+
if (got === SCHEMA_GENERATION)
|
|
10853
|
+
return;
|
|
10854
|
+
if (got === null)
|
|
10837
10855
|
throw new Error("The database has no sphica schema. Create it with `sphica init`.");
|
|
10838
|
-
throw new Error(
|
|
10856
|
+
throw new Error(got < SCHEMA_GENERATION ? "The database was made by Sphica 0.4 or earlier, and this Sphica cannot read it. Move it aside (it is left unchanged), then run `sphica init`." : "The database was made by a newer Sphica. Update sphica.");
|
|
10839
10857
|
}
|
|
10840
10858
|
var READER_FUNCTIONS = new Set([
|
|
10841
10859
|
"bm25",
|
|
@@ -10853,19 +10871,10 @@ var READER_FUNCTIONS = new Set([
|
|
|
10853
10871
|
"min",
|
|
10854
10872
|
"substr"
|
|
10855
10873
|
]);
|
|
10856
|
-
var SHADOW = /^(
|
|
10874
|
+
var SHADOW = /^(unit|source)_fts_(data|idx|docsize|config)$/;
|
|
10857
10875
|
|
|
10858
10876
|
// server/src/db.ts
|
|
10859
|
-
var JSON_COLUMNS = new Set([
|
|
10860
|
-
"refs",
|
|
10861
|
-
"downsides",
|
|
10862
|
-
"next",
|
|
10863
|
-
"metadata",
|
|
10864
|
-
"connectors",
|
|
10865
|
-
"files",
|
|
10866
|
-
"handles",
|
|
10867
|
-
"paths"
|
|
10868
|
-
]);
|
|
10877
|
+
var JSON_COLUMNS = new Set(["next"]);
|
|
10869
10878
|
var TOP_LEVEL = /^\$\[\d+\]\."([^"]+)"$/;
|
|
10870
10879
|
var parseJson = new ParseJSONResultsPlugin({
|
|
10871
10880
|
shouldParse: (_value, jsonPath) => JSON_COLUMNS.has(jsonPath.match(TOP_LEVEL)?.[1] ?? "")
|
|
@@ -10910,13 +10919,27 @@ var HIRAGANA_ONLY = /^[\p{Script=Hiragana}ー]+$/u;
|
|
|
10910
10919
|
var STOP = new Set(["the", "a", "an", "of", "to", "in", "is", "and", "or", "for", "on", "it", "be"]);
|
|
10911
10920
|
var IDENT = /#\d+|[a-z0-9][a-z0-9_./#-]*[a-z0-9]/g;
|
|
10912
10921
|
var MAX_TERM = 100;
|
|
10922
|
+
var OKURIGANA = /^(\p{Script=Han}+)[\p{Script=Hiragana}ー]+$/u;
|
|
10923
|
+
function singular(w) {
|
|
10924
|
+
if (!/^[a-z]{4,}$/.test(w))
|
|
10925
|
+
return w;
|
|
10926
|
+
if (w.endsWith("ies"))
|
|
10927
|
+
return `${w.slice(0, -3)}y`;
|
|
10928
|
+
if (w.endsWith("ie"))
|
|
10929
|
+
return `${w.slice(0, -2)}y`;
|
|
10930
|
+
if (/(?:ss|us|x|z|ch|sh)es$/.test(w))
|
|
10931
|
+
return w.slice(0, -2);
|
|
10932
|
+
if (/(?:ch|sh|us)e$/.test(w))
|
|
10933
|
+
return w.slice(0, -1);
|
|
10934
|
+
return w.endsWith("s") && !/(?:ss|us|is)$/.test(w) ? w.slice(0, -1) : w;
|
|
10935
|
+
}
|
|
10913
10936
|
function terms(text) {
|
|
10914
10937
|
const norm = text.normalize("NFKC").toLowerCase();
|
|
10915
10938
|
const out = [];
|
|
10916
10939
|
const keep = (w) => {
|
|
10917
10940
|
if (w.length > MAX_TERM || STOP.has(w) || HIRAGANA_ONLY.test(w))
|
|
10918
10941
|
return;
|
|
10919
|
-
out.push(w);
|
|
10942
|
+
out.push(OKURIGANA.exec(w)?.[1] ?? singular(w));
|
|
10920
10943
|
};
|
|
10921
10944
|
for (const s of segmenter.segment(norm))
|
|
10922
10945
|
if (s.isWordLike)
|
|
@@ -10926,6 +10949,48 @@ function terms(text) {
|
|
|
10926
10949
|
keep(m[0]);
|
|
10927
10950
|
return out.filter(Boolean);
|
|
10928
10951
|
}
|
|
10952
|
+
var QUESTION = new Set([
|
|
10953
|
+
...[
|
|
10954
|
+
"why",
|
|
10955
|
+
"what",
|
|
10956
|
+
"which",
|
|
10957
|
+
"when",
|
|
10958
|
+
"where",
|
|
10959
|
+
"who",
|
|
10960
|
+
"how",
|
|
10961
|
+
"do",
|
|
10962
|
+
"does",
|
|
10963
|
+
"did",
|
|
10964
|
+
"we",
|
|
10965
|
+
"our",
|
|
10966
|
+
"us",
|
|
10967
|
+
"you",
|
|
10968
|
+
"i",
|
|
10969
|
+
"are",
|
|
10970
|
+
"was",
|
|
10971
|
+
"were"
|
|
10972
|
+
],
|
|
10973
|
+
...[
|
|
10974
|
+
"will",
|
|
10975
|
+
"would",
|
|
10976
|
+
"should",
|
|
10977
|
+
"can",
|
|
10978
|
+
"could",
|
|
10979
|
+
"there",
|
|
10980
|
+
"this",
|
|
10981
|
+
"that",
|
|
10982
|
+
"with",
|
|
10983
|
+
"from",
|
|
10984
|
+
"any",
|
|
10985
|
+
"ever",
|
|
10986
|
+
"long",
|
|
10987
|
+
"use",
|
|
10988
|
+
"used",
|
|
10989
|
+
"using"
|
|
10990
|
+
],
|
|
10991
|
+
...["add", "get", "make", "have", "has", "not", "no", "yes", "reason"],
|
|
10992
|
+
...["理由", "仕組", "何", "方", "場合", "今", "件"]
|
|
10993
|
+
]);
|
|
10929
10994
|
var sha256 = (s) => crypto.createHash("sha256").update(s).digest();
|
|
10930
10995
|
function uuidFrom(...parts) {
|
|
10931
10996
|
const b = crypto.createHash("sha256").update(parts.join("\x00")).digest().subarray(0, 16);
|
|
@@ -11119,16 +11184,21 @@ var DDL = () => ddl ??= new Set([
|
|
|
11119
11184
|
return code;
|
|
11120
11185
|
}));
|
|
11121
11186
|
var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
|
|
11122
|
-
var CAPTURE_VIEWS = new Set(["
|
|
11187
|
+
var CAPTURE_VIEWS = new Set(["capture_session", "capture_message", "capture_edit", "capture_delivery"]);
|
|
11123
11188
|
var TRIGGER_WRITES = {
|
|
11124
|
-
|
|
11125
|
-
capture_message_insert: new Set(["
|
|
11126
|
-
|
|
11127
|
-
|
|
11189
|
+
capture_session_insert: new Set(["session"]),
|
|
11190
|
+
capture_message_insert: new Set(["source"]),
|
|
11191
|
+
capture_edit_insert: new Set(["edit_observation"]),
|
|
11192
|
+
capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
|
|
11193
|
+
source_fts_ai: new Set(["source_fts"])
|
|
11194
|
+
};
|
|
11195
|
+
var TRIGGER_FUNCTIONS = {
|
|
11196
|
+
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
11128
11197
|
};
|
|
11129
11198
|
var CAPTURE_READS = {
|
|
11130
11199
|
project: new Set(["id", "key", "name"]),
|
|
11131
|
-
|
|
11200
|
+
session: new Set(["id"]),
|
|
11201
|
+
source: new Set(["id", "session_id", "external_id", "kind"])
|
|
11132
11202
|
};
|
|
11133
11203
|
function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
11134
11204
|
const table = p1 ?? "";
|
|
@@ -11148,7 +11218,7 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
|
11148
11218
|
return CAPTURE_READS[table]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11149
11219
|
}
|
|
11150
11220
|
if (action === C2.SQLITE_FUNCTION)
|
|
11151
|
-
return p2 === "sphica_terms" ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11221
|
+
return p2 === "sphica_terms" || triggerOrView !== null && TRIGGER_FUNCTIONS[triggerOrView]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11152
11222
|
if (action === C2.SQLITE_PRAGMA)
|
|
11153
11223
|
return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11154
11224
|
if (action === C2.SQLITE_SELECT || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
|
|
@@ -11168,7 +11238,7 @@ function connectWriter(role, file = dbFile(), create = false) {
|
|
|
11168
11238
|
requireFile(file);
|
|
11169
11239
|
const raw = new DatabaseSync2(file);
|
|
11170
11240
|
try {
|
|
11171
|
-
prepare(raw, role === "ingest");
|
|
11241
|
+
prepare(raw, create ? "none" : role === "ingest" ? "revision" : "generation");
|
|
11172
11242
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
11173
11243
|
} catch (e) {
|
|
11174
11244
|
raw.close();
|
|
@@ -11199,8 +11269,7 @@ function openWriter(role, file = dbFile()) {
|
|
|
11199
11269
|
}
|
|
11200
11270
|
|
|
11201
11271
|
// server/src/knowledge.ts
|
|
11202
|
-
var
|
|
11203
|
-
var conversationId = (projectId, origin, externalId) => uuidFrom(String(projectId), origin, externalId);
|
|
11272
|
+
var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), host, externalId);
|
|
11204
11273
|
|
|
11205
11274
|
// server/src/panel.ts
|
|
11206
11275
|
import { stripVTControlCharacters, styleText } from "node:util";
|
|
@@ -11219,8 +11288,15 @@ var plain2 = (s) => visible(stripVTControlCharacters(s.replace(STRING_SEQUENCE,
|
|
|
11219
11288
|
import { execFileSync } from "node:child_process";
|
|
11220
11289
|
import fs2 from "node:fs";
|
|
11221
11290
|
import os2 from "node:os";
|
|
11291
|
+
import path3 from "node:path";
|
|
11292
|
+
|
|
11293
|
+
// server/src/anchors.ts
|
|
11222
11294
|
import path2 from "node:path";
|
|
11223
|
-
var
|
|
11295
|
+
var MAX_BYTES = 2 * 1024 * 1024;
|
|
11296
|
+
var leaves = (rel) => rel === ".." || rel.startsWith(`..${path2.sep}`) || path2.isAbsolute(rel);
|
|
11297
|
+
|
|
11298
|
+
// server/src/project.ts
|
|
11299
|
+
var localFile = () => path3.join(os2.homedir(), ".sphica", "projects.json");
|
|
11224
11300
|
var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
|
|
11225
11301
|
function normalizeRemote(url) {
|
|
11226
11302
|
const raw = String(url ?? "").trim();
|
|
@@ -11265,32 +11341,32 @@ function localMap() {
|
|
|
11265
11341
|
} catch {
|
|
11266
11342
|
m = null;
|
|
11267
11343
|
}
|
|
11268
|
-
if (!m || typeof m !== "object" || Array.isArray(m) || Object.entries(m).some(([root, name]) => !
|
|
11344
|
+
if (!m || typeof m !== "object" || Array.isArray(m) || Object.entries(m).some(([root, name]) => !path3.isAbsolute(root) || typeof name !== "string" || !LOCAL_KEY.test(name)))
|
|
11269
11345
|
throw new Error(`${localFile()} is not a valid JSON project table. Fix or delete it, then name the project again.`);
|
|
11270
11346
|
return m;
|
|
11271
11347
|
}
|
|
11272
11348
|
function identify(dir) {
|
|
11273
|
-
const given =
|
|
11349
|
+
const given = path3.resolve(dir);
|
|
11274
11350
|
const top = git(given, "rev-parse", "--show-toplevel");
|
|
11275
11351
|
const root = top || given;
|
|
11276
11352
|
const remote = top ? normalizeRemote(git(root, "remote", "get-url", "origin")) : null;
|
|
11277
11353
|
if (remote)
|
|
11278
11354
|
return { key: `git:${remote}`, root, name: remote.split("/").slice(1).join("/") || remote };
|
|
11279
11355
|
const map = localMap();
|
|
11280
|
-
for (let d = root;; d =
|
|
11356
|
+
for (let d = root;; d = path3.dirname(d)) {
|
|
11281
11357
|
const local = map[d];
|
|
11282
11358
|
if (local && LOCAL_KEY.test(local))
|
|
11283
11359
|
return { key: `local:${local}`, root: d, name: local };
|
|
11284
|
-
if (top ||
|
|
11360
|
+
if (top || path3.dirname(d) === d)
|
|
11285
11361
|
return null;
|
|
11286
11362
|
}
|
|
11287
11363
|
}
|
|
11288
11364
|
function relativeTo(root, file, cwd = root) {
|
|
11289
|
-
const abs =
|
|
11290
|
-
const rel =
|
|
11291
|
-
if (!rel ||
|
|
11365
|
+
const abs = path3.resolve(cwd, file);
|
|
11366
|
+
const rel = path3.relative(root, abs);
|
|
11367
|
+
if (!rel || leaves(rel))
|
|
11292
11368
|
return null;
|
|
11293
|
-
return rel.split(
|
|
11369
|
+
return rel.split(path3.sep).join("/");
|
|
11294
11370
|
}
|
|
11295
11371
|
function patchPaths(patch) {
|
|
11296
11372
|
const out = [];
|
|
@@ -11303,23 +11379,104 @@ function patchPaths(patch) {
|
|
|
11303
11379
|
return out;
|
|
11304
11380
|
}
|
|
11305
11381
|
|
|
11382
|
+
// server/src/worktree.ts
|
|
11383
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
11384
|
+
import fs3 from "node:fs";
|
|
11385
|
+
import path4 from "node:path";
|
|
11386
|
+
var MAX_PATHS = 200;
|
|
11387
|
+
var git2 = (root, args) => {
|
|
11388
|
+
try {
|
|
11389
|
+
return execFileSync2("git", ["-C", root, ...args], {
|
|
11390
|
+
encoding: "utf8",
|
|
11391
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
11392
|
+
timeout: 5000,
|
|
11393
|
+
maxBuffer: 16 * 1024 * 1024
|
|
11394
|
+
});
|
|
11395
|
+
} catch {
|
|
11396
|
+
return null;
|
|
11397
|
+
}
|
|
11398
|
+
};
|
|
11399
|
+
var usable = (p) => p !== "" && !p.includes("\\");
|
|
11400
|
+
function snapshot(root) {
|
|
11401
|
+
const out = git2(root, ["status", "--porcelain=v2", "-z", "--untracked-files=all"]);
|
|
11402
|
+
if (out === null)
|
|
11403
|
+
return null;
|
|
11404
|
+
const fields = out.split("\x00");
|
|
11405
|
+
const entries = {};
|
|
11406
|
+
for (let i = 0;i < fields.length; i++) {
|
|
11407
|
+
const f = fields[i] ?? "";
|
|
11408
|
+
const skip = { "1": 8, "2": 9, u: 10, "?": 1 }[f[0] ?? ""];
|
|
11409
|
+
if (!skip)
|
|
11410
|
+
continue;
|
|
11411
|
+
const parts = f.split(" ");
|
|
11412
|
+
const p = parts.slice(skip).join(" ");
|
|
11413
|
+
if (f[0] === "2")
|
|
11414
|
+
i++;
|
|
11415
|
+
if (!usable(p))
|
|
11416
|
+
continue;
|
|
11417
|
+
let st;
|
|
11418
|
+
try {
|
|
11419
|
+
st = fs3.lstatSync(path4.join(root, p), { throwIfNoEntry: false });
|
|
11420
|
+
} catch {
|
|
11421
|
+
st = undefined;
|
|
11422
|
+
}
|
|
11423
|
+
entries[p] = `${parts.slice(0, skip).join(" ")} ${st ? `${st.size}:${st.mtimeMs}` : "gone"}`;
|
|
11424
|
+
}
|
|
11425
|
+
const head2 = git2(root, ["rev-parse", "--verify", "-q", "HEAD"])?.trim() || null;
|
|
11426
|
+
return { head: head2, entries };
|
|
11427
|
+
}
|
|
11428
|
+
function changed(root, before, after) {
|
|
11429
|
+
const paths = new Set;
|
|
11430
|
+
for (const p of new Set([...Object.keys(before.entries), ...Object.keys(after.entries)]))
|
|
11431
|
+
if (before.entries[p] !== after.entries[p])
|
|
11432
|
+
paths.add(p);
|
|
11433
|
+
if (before.head && after.head && before.head !== after.head && git2(root, ["merge-base", "--is-ancestor", before.head, after.head]) !== null) {
|
|
11434
|
+
for (const p of (git2(root, ["diff", "--name-only", "-z", before.head, after.head]) ?? "").split("\x00"))
|
|
11435
|
+
if (usable(p))
|
|
11436
|
+
paths.add(p);
|
|
11437
|
+
}
|
|
11438
|
+
return [...paths].sort().slice(0, MAX_PATHS);
|
|
11439
|
+
}
|
|
11440
|
+
|
|
11306
11441
|
// server/src/capture.ts
|
|
11307
|
-
var spoolDir = () =>
|
|
11308
|
-
var stateFile = () =>
|
|
11309
|
-
var rejectedDir = () =>
|
|
11310
|
-
var unregisteredDir = () =>
|
|
11442
|
+
var spoolDir = () => path5.join(os3.homedir(), ".sphica", "spool");
|
|
11443
|
+
var stateFile = () => path5.join(os3.homedir(), ".sphica", "capture.json");
|
|
11444
|
+
var rejectedDir = () => path5.join(spoolDir(), "rejected");
|
|
11445
|
+
var unregisteredDir = () => path5.join(spoolDir(), "unregistered");
|
|
11311
11446
|
var HOLD_DAYS = 30;
|
|
11312
11447
|
var HOLD_MAX = 1000;
|
|
11448
|
+
var baselineDir = () => path5.join(os3.homedir(), ".sphica", "worktree");
|
|
11449
|
+
function current(raw) {
|
|
11450
|
+
const r = raw;
|
|
11451
|
+
if (r.v === 2)
|
|
11452
|
+
return raw;
|
|
11453
|
+
if (r.v !== 1)
|
|
11454
|
+
throw new Error(`unknown queue record version ${String(r.v)}`);
|
|
11455
|
+
const old = raw;
|
|
11456
|
+
if (old.kind === "message")
|
|
11457
|
+
return {
|
|
11458
|
+
...old,
|
|
11459
|
+
v: 2,
|
|
11460
|
+
kind: "message",
|
|
11461
|
+
speaker: old.speaker === "self" ? "owner" : "assistant",
|
|
11462
|
+
redacted: false
|
|
11463
|
+
};
|
|
11464
|
+
if (old.action !== "edit")
|
|
11465
|
+
return null;
|
|
11466
|
+
return { ...old, v: 2, kind: "edit", event: null, path: old.path, via: "tool" };
|
|
11467
|
+
}
|
|
11313
11468
|
var MAX_MESSAGE = 128 * 1024;
|
|
11314
11469
|
var KEEP = 8 * 1024;
|
|
11315
11470
|
function fit(body) {
|
|
11316
11471
|
const all = bytes(body);
|
|
11317
11472
|
if (all <= MAX_MESSAGE) {
|
|
11318
11473
|
const kept = mask(body);
|
|
11319
|
-
return { body: kept, truncated: false,
|
|
11474
|
+
return { body: kept, truncated: false, redacted: kept !== body, originalBytes: all };
|
|
11320
11475
|
}
|
|
11321
|
-
const
|
|
11322
|
-
const
|
|
11476
|
+
const start = head(body, KEEP * 2);
|
|
11477
|
+
const end = tail(body, KEEP * 2);
|
|
11478
|
+
const a = head(mask(start), KEEP);
|
|
11479
|
+
const z = tail(mask(end), KEEP);
|
|
11323
11480
|
const cut = all - bytes(a) - bytes(z);
|
|
11324
11481
|
return {
|
|
11325
11482
|
body: `${a}
|
|
@@ -11328,13 +11485,14 @@ function fit(body) {
|
|
|
11328
11485
|
|
|
11329
11486
|
${z}`,
|
|
11330
11487
|
truncated: true,
|
|
11488
|
+
redacted: mask(start) !== start || mask(end) !== end,
|
|
11331
11489
|
originalBytes: all
|
|
11332
11490
|
};
|
|
11333
11491
|
}
|
|
11334
11492
|
function prune(held) {
|
|
11335
11493
|
let files;
|
|
11336
11494
|
try {
|
|
11337
|
-
files =
|
|
11495
|
+
files = fs4.readdirSync(held).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
|
|
11338
11496
|
} catch {
|
|
11339
11497
|
return;
|
|
11340
11498
|
}
|
|
@@ -11342,21 +11500,21 @@ function prune(held) {
|
|
|
11342
11500
|
const stale = files.filter((f) => Number(f.split("-")[0]) < cutoff);
|
|
11343
11501
|
const over = files.slice(0, Math.max(0, files.length - HOLD_MAX));
|
|
11344
11502
|
for (const f of new Set([...stale, ...over]))
|
|
11345
|
-
|
|
11503
|
+
fs4.rmSync(path5.join(held, f), { force: true });
|
|
11346
11504
|
}
|
|
11347
11505
|
function spool(record) {
|
|
11348
11506
|
const dir = spoolDir();
|
|
11349
|
-
|
|
11507
|
+
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
11350
11508
|
const name = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}.json`;
|
|
11351
|
-
const tmp =
|
|
11352
|
-
|
|
11353
|
-
|
|
11509
|
+
const tmp = path5.join(dir, `.${name}`);
|
|
11510
|
+
fs4.writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
|
|
11511
|
+
fs4.renameSync(tmp, path5.join(dir, name));
|
|
11354
11512
|
}
|
|
11355
11513
|
var branchOf = (root) => {
|
|
11356
11514
|
try {
|
|
11357
|
-
const dotgit =
|
|
11358
|
-
const gitdir =
|
|
11359
|
-
const h =
|
|
11515
|
+
const dotgit = path5.join(root, ".git");
|
|
11516
|
+
const gitdir = fs4.statSync(dotgit).isFile() ? path5.resolve(root, fs4.readFileSync(dotgit, "utf8").match(/^gitdir: (.+)$/m)?.[1]?.trim() ?? "") : dotgit;
|
|
11517
|
+
const h = fs4.readFileSync(path5.join(gitdir, "HEAD"), "utf8").trim();
|
|
11360
11518
|
return h.startsWith("ref: refs/heads/") ? h.slice("ref: refs/heads/".length) : null;
|
|
11361
11519
|
} catch {
|
|
11362
11520
|
return null;
|
|
@@ -11377,63 +11535,30 @@ var INJECTED = [
|
|
|
11377
11535
|
/^(?:Another Claude|A peer) session sent a message(?: while you were working)?:/
|
|
11378
11536
|
];
|
|
11379
11537
|
var digest = (s) => sha256(s).toString("hex").slice(0, 16);
|
|
11380
|
-
var saidDir = () => path3.join(spoolDir(), "said");
|
|
11381
|
-
var RENAME_TRIES = 20;
|
|
11382
|
-
var RENAME_WAIT_MS = 15;
|
|
11383
|
-
var sleepSync = (ms) => {
|
|
11384
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
11385
|
-
};
|
|
11386
|
-
function remember(session, id) {
|
|
11387
|
-
const dir = saidDir();
|
|
11388
|
-
fs3.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
11389
|
-
const file = path3.join(dir, uuidFrom(session));
|
|
11390
|
-
const tmp = `${file}.${process.pid}`;
|
|
11391
|
-
fs3.writeFileSync(tmp, id, { mode: 384 });
|
|
11392
|
-
for (let i = 0;; i++) {
|
|
11393
|
-
try {
|
|
11394
|
-
fs3.renameSync(tmp, file);
|
|
11395
|
-
break;
|
|
11396
|
-
} catch (e) {
|
|
11397
|
-
const code = e.code;
|
|
11398
|
-
if (i >= RENAME_TRIES || code !== "EPERM" && code !== "EBUSY") {
|
|
11399
|
-
fs3.rmSync(tmp, { force: true });
|
|
11400
|
-
throw e;
|
|
11401
|
-
}
|
|
11402
|
-
sleepSync(RENAME_WAIT_MS);
|
|
11403
|
-
}
|
|
11404
|
-
}
|
|
11405
|
-
const old = Date.now() - 30 * 86400000;
|
|
11406
|
-
for (const f of fs3.readdirSync(dir)) {
|
|
11407
|
-
const st = fs3.statSync(path3.join(dir, f), { throwIfNoEntry: false });
|
|
11408
|
-
if (st && st.mtimeMs < old)
|
|
11409
|
-
fs3.rmSync(path3.join(dir, f), { force: true });
|
|
11410
|
-
}
|
|
11411
|
-
}
|
|
11412
|
-
function lastSaid(session) {
|
|
11413
|
-
try {
|
|
11414
|
-
return fs3.readFileSync(path3.join(saidDir(), uuidFrom(session)), "utf8");
|
|
11415
|
-
} catch {
|
|
11416
|
-
return null;
|
|
11417
|
-
}
|
|
11418
|
-
}
|
|
11419
11538
|
function answersOf(input) {
|
|
11420
11539
|
const response = input.tool_response;
|
|
11421
11540
|
const answers = response?.answers;
|
|
11422
11541
|
if (!answers || typeof answers !== "object")
|
|
11423
11542
|
return null;
|
|
11424
|
-
const
|
|
11425
|
-
|
|
11426
|
-
|
|
11543
|
+
const pairs = Object.entries(answers);
|
|
11544
|
+
if (!pairs.length)
|
|
11545
|
+
return null;
|
|
11546
|
+
return {
|
|
11547
|
+
questions: pairs.map(([q], i) => `Q${i + 1}: ${q}`).join(`
|
|
11548
|
+
|
|
11549
|
+
`),
|
|
11550
|
+
answers: pairs.map(([q, a], i) => {
|
|
11551
|
+
const notes = response?.annotations?.[q]?.notes;
|
|
11552
|
+
const memo = typeof notes === "string" && notes.trim() ? `
|
|
11427
11553
|
Notes: ${notes.trim()}` : "";
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
});
|
|
11431
|
-
return lines.length ? lines.join(`
|
|
11554
|
+
return `A${i + 1}: ${Array.isArray(a) ? a.join(" / ") : String(a)}${memo}`;
|
|
11555
|
+
}).join(`
|
|
11432
11556
|
|
|
11433
|
-
`)
|
|
11557
|
+
`)
|
|
11558
|
+
};
|
|
11434
11559
|
}
|
|
11435
11560
|
function captureNotice(file = dbFile()) {
|
|
11436
|
-
if (!
|
|
11561
|
+
if (!fs4.existsSync(file))
|
|
11437
11562
|
return panel("sphica: no database, so conversations are not recorded", [file], "Create it with sphica init");
|
|
11438
11563
|
const s = readState();
|
|
11439
11564
|
if (s.stuck)
|
|
@@ -11442,6 +11567,34 @@ function captureNotice(file = dbFile()) {
|
|
|
11442
11567
|
return panel(`sphica: the database rejected ${plural(s.rejected, "record")}`, [rejectedDir(), `Move them back to ${spoolDir()} to resend`], "Fix them first, then check with sphica doctor");
|
|
11443
11568
|
return null;
|
|
11444
11569
|
}
|
|
11570
|
+
var baselineFile = (host, session) => path5.join(baselineDir(), `${digest(`${host}\x00${session}`)}.json`);
|
|
11571
|
+
function readBaseline(file) {
|
|
11572
|
+
try {
|
|
11573
|
+
return JSON.parse(fs4.readFileSync(file, "utf8"));
|
|
11574
|
+
} catch {
|
|
11575
|
+
return null;
|
|
11576
|
+
}
|
|
11577
|
+
}
|
|
11578
|
+
function writeBaseline(file, b) {
|
|
11579
|
+
fs4.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
|
|
11580
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
11581
|
+
fs4.writeFileSync(tmp, JSON.stringify(b), { mode: 384 });
|
|
11582
|
+
fs4.renameSync(tmp, file);
|
|
11583
|
+
}
|
|
11584
|
+
function pruneBaselines() {
|
|
11585
|
+
const cutoff = Date.now() - HOLD_DAYS * 24 * 60 * 60 * 1000;
|
|
11586
|
+
let files;
|
|
11587
|
+
try {
|
|
11588
|
+
files = fs4.readdirSync(baselineDir());
|
|
11589
|
+
} catch {
|
|
11590
|
+
return;
|
|
11591
|
+
}
|
|
11592
|
+
for (const f of files) {
|
|
11593
|
+
const file = path5.join(baselineDir(), f);
|
|
11594
|
+
if ((fs4.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) < cutoff)
|
|
11595
|
+
fs4.rmSync(file, { force: true });
|
|
11596
|
+
}
|
|
11597
|
+
}
|
|
11445
11598
|
function onHook(host, input) {
|
|
11446
11599
|
const event = input.hook_event_name;
|
|
11447
11600
|
const owner = () => isOwnerTurn(input, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined);
|
|
@@ -11450,9 +11603,14 @@ function onHook(host, input) {
|
|
|
11450
11603
|
return { flush: false };
|
|
11451
11604
|
const file = process.env.CLAUDE_ENV_FILE;
|
|
11452
11605
|
if (file && input.session_id && /^[A-Za-z0-9_-]+$/.test(input.session_id)) {
|
|
11453
|
-
|
|
11606
|
+
fs4.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
|
|
11454
11607
|
`);
|
|
11455
11608
|
}
|
|
11609
|
+
const place2 = identify(input.cwd ?? process.cwd());
|
|
11610
|
+
const now = place2 && snapshot(place2.root);
|
|
11611
|
+
if (now)
|
|
11612
|
+
writeBaseline(baselineFile(host, String(input.session_id)), { ...now, running: false });
|
|
11613
|
+
pruneBaselines();
|
|
11456
11614
|
return { flush: false, notice: captureNotice() };
|
|
11457
11615
|
}
|
|
11458
11616
|
if (!owner())
|
|
@@ -11467,7 +11625,7 @@ function onHook(host, input) {
|
|
|
11467
11625
|
return { flush: false };
|
|
11468
11626
|
const at = new Date().toISOString();
|
|
11469
11627
|
const base = {
|
|
11470
|
-
v:
|
|
11628
|
+
v: 2,
|
|
11471
11629
|
host,
|
|
11472
11630
|
session: String(input.session_id),
|
|
11473
11631
|
project: place.key,
|
|
@@ -11475,23 +11633,37 @@ function onHook(host, input) {
|
|
|
11475
11633
|
turn,
|
|
11476
11634
|
at
|
|
11477
11635
|
};
|
|
11478
|
-
const say = (key, speaker, raw) => {
|
|
11636
|
+
const say = (key, speaker, raw, when = at) => {
|
|
11479
11637
|
const kept = fit(clean(raw).trim());
|
|
11480
11638
|
if (!kept.body.trim())
|
|
11481
11639
|
return;
|
|
11482
11640
|
const id = `${key}:${digest(kept.body)}`;
|
|
11483
|
-
spool({ ...base, kind: "message", id, speaker, ...kept });
|
|
11484
|
-
if (speaker === "self")
|
|
11485
|
-
remember(base.session, id);
|
|
11641
|
+
spool({ ...base, at: when, kind: "message", id, speaker, ...kept });
|
|
11486
11642
|
};
|
|
11643
|
+
const baseline = baselineFile(host, base.session);
|
|
11644
|
+
if (event === "UserPromptSubmit") {
|
|
11645
|
+
if (!readBaseline(baseline)?.running) {
|
|
11646
|
+
const now = snapshot(place.root);
|
|
11647
|
+
if (now)
|
|
11648
|
+
writeBaseline(baseline, { ...now, running: true });
|
|
11649
|
+
}
|
|
11650
|
+
}
|
|
11487
11651
|
if (event === "UserPromptSubmit" && input.prompt) {
|
|
11488
11652
|
const prompt = input.prompt.trimStart();
|
|
11489
11653
|
if (!INJECTED.some((r) => r.test(prompt)))
|
|
11490
|
-
say(`${turn}:
|
|
11654
|
+
say(`${turn}:owner`, "owner", prompt);
|
|
11491
11655
|
}
|
|
11492
11656
|
if (event === "Stop") {
|
|
11493
11657
|
if (input.last_assistant_message)
|
|
11494
11658
|
say(`${turn}:assistant`, "assistant", input.last_assistant_message);
|
|
11659
|
+
const now = snapshot(place.root);
|
|
11660
|
+
if (now) {
|
|
11661
|
+
const before = readBaseline(baseline);
|
|
11662
|
+
if (before)
|
|
11663
|
+
for (const p of changed(place.root, before, now))
|
|
11664
|
+
spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
|
|
11665
|
+
writeBaseline(baseline, { ...now, running: false });
|
|
11666
|
+
}
|
|
11495
11667
|
return { flush: true };
|
|
11496
11668
|
}
|
|
11497
11669
|
if (event === "PostToolUse") {
|
|
@@ -11499,31 +11671,31 @@ function onHook(host, input) {
|
|
|
11499
11671
|
const ti = input.tool_input ?? {};
|
|
11500
11672
|
if (tool === "AskUserQuestion") {
|
|
11501
11673
|
const said = answersOf(input);
|
|
11502
|
-
if (said)
|
|
11503
|
-
|
|
11674
|
+
if (said) {
|
|
11675
|
+
const id = `${turn}:ask:${input.tool_use_id ?? at}`;
|
|
11676
|
+
say(`${id}:q`, "assistant", said.questions, new Date(Date.parse(at) - 1).toISOString());
|
|
11677
|
+
say(id, "owner", said.answers);
|
|
11678
|
+
}
|
|
11504
11679
|
return { flush: false };
|
|
11505
11680
|
}
|
|
11506
11681
|
if (tool === "Read")
|
|
11507
11682
|
return { flush: false };
|
|
11508
|
-
const message = lastSaid(base.session);
|
|
11509
|
-
if (!message)
|
|
11510
|
-
return { flush: false };
|
|
11511
11683
|
const cwd = input.cwd ?? place.root;
|
|
11512
11684
|
const files = (tool === "apply_patch" ? patchPaths(String(ti.command ?? "")) : [ti.file_path, ti.notebook_path].filter((p) => typeof p === "string")).flatMap((p) => relativeTo(place.root, p, cwd) ?? []);
|
|
11513
11685
|
for (const p of files)
|
|
11514
|
-
spool({ ...base, kind: "
|
|
11686
|
+
spool({ ...base, kind: "edit", event: input.tool_use_id ?? null, path: p, via: "tool" });
|
|
11515
11687
|
}
|
|
11516
11688
|
return { flush: false };
|
|
11517
11689
|
}
|
|
11518
11690
|
function writeState(s) {
|
|
11519
11691
|
try {
|
|
11520
|
-
|
|
11692
|
+
fs4.writeFileSync(stateFile(), JSON.stringify(s));
|
|
11521
11693
|
} catch {}
|
|
11522
11694
|
}
|
|
11523
11695
|
function readState() {
|
|
11524
11696
|
const count = (dir) => {
|
|
11525
11697
|
try {
|
|
11526
|
-
return
|
|
11698
|
+
return fs4.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).length;
|
|
11527
11699
|
} catch {
|
|
11528
11700
|
return 0;
|
|
11529
11701
|
}
|
|
@@ -11535,7 +11707,7 @@ function readState() {
|
|
|
11535
11707
|
};
|
|
11536
11708
|
let raw = {};
|
|
11537
11709
|
try {
|
|
11538
|
-
const parsed = JSON.parse(
|
|
11710
|
+
const parsed = JSON.parse(fs4.readFileSync(stateFile(), "utf8"));
|
|
11539
11711
|
if (parsed && typeof parsed === "object")
|
|
11540
11712
|
raw = parsed;
|
|
11541
11713
|
} catch {}
|
|
@@ -11549,25 +11721,25 @@ function readState() {
|
|
|
11549
11721
|
};
|
|
11550
11722
|
}
|
|
11551
11723
|
function lock() {
|
|
11552
|
-
const file =
|
|
11553
|
-
|
|
11724
|
+
const file = path5.join(spoolDir(), ".lock");
|
|
11725
|
+
fs4.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
|
|
11554
11726
|
for (let attempt = 0;attempt < 2; attempt++) {
|
|
11555
11727
|
try {
|
|
11556
|
-
|
|
11728
|
+
fs4.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
|
|
11557
11729
|
return () => {
|
|
11558
11730
|
try {
|
|
11559
|
-
if (
|
|
11560
|
-
|
|
11731
|
+
if (fs4.readFileSync(file, "utf8") === String(process.pid))
|
|
11732
|
+
fs4.rmSync(file, { force: true });
|
|
11561
11733
|
} catch {}
|
|
11562
11734
|
};
|
|
11563
11735
|
} catch (e) {
|
|
11564
11736
|
if (e.code !== "EEXIST")
|
|
11565
11737
|
throw e;
|
|
11566
11738
|
}
|
|
11567
|
-
const st =
|
|
11739
|
+
const st = fs4.statSync(file, { throwIfNoEntry: false });
|
|
11568
11740
|
if (!st)
|
|
11569
11741
|
continue;
|
|
11570
|
-
const holder = Number(
|
|
11742
|
+
const holder = Number(fs4.readFileSync(file, "utf8") || 0);
|
|
11571
11743
|
const fresh = Date.now() - st.mtimeMs < 5 * 60000;
|
|
11572
11744
|
const alive = (() => {
|
|
11573
11745
|
if (holder <= 0)
|
|
@@ -11580,7 +11752,7 @@ function lock() {
|
|
|
11580
11752
|
})();
|
|
11581
11753
|
if (alive && fresh)
|
|
11582
11754
|
return null;
|
|
11583
|
-
|
|
11755
|
+
fs4.rmSync(file, { force: true });
|
|
11584
11756
|
}
|
|
11585
11757
|
return null;
|
|
11586
11758
|
}
|
|
@@ -11589,27 +11761,21 @@ var ROWS = 1000;
|
|
|
11589
11761
|
var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
|
|
11590
11762
|
async function write(db, batch, projects) {
|
|
11591
11763
|
return inTransaction(db, async (trx) => {
|
|
11592
|
-
const
|
|
11764
|
+
const sessions = new Map;
|
|
11593
11765
|
for (const r of batch) {
|
|
11594
11766
|
const p = projects.get(r.project);
|
|
11595
11767
|
if (!p)
|
|
11596
11768
|
continue;
|
|
11597
|
-
const id =
|
|
11598
|
-
const prev =
|
|
11769
|
+
const id = sessionId(p.id, r.host, r.session);
|
|
11770
|
+
const prev = sessions.get(id);
|
|
11599
11771
|
if (!prev || Date.parse(r.at) < Date.parse(prev.at))
|
|
11600
|
-
|
|
11601
|
-
project: p.id,
|
|
11602
|
-
host: r.host,
|
|
11603
|
-
session: r.session,
|
|
11604
|
-
branch: r.branch,
|
|
11605
|
-
at: r.at
|
|
11606
|
-
});
|
|
11772
|
+
sessions.set(id, { project: p.id, host: r.host, session: r.session, branch: r.branch, at: r.at });
|
|
11607
11773
|
}
|
|
11608
|
-
for (const part of chunks([...
|
|
11609
|
-
await trx.insertInto("
|
|
11774
|
+
for (const part of chunks([...sessions]))
|
|
11775
|
+
await trx.insertInto("capture_session").values(part.map(([id, v]) => ({
|
|
11610
11776
|
id,
|
|
11611
11777
|
project_id: v.project,
|
|
11612
|
-
|
|
11778
|
+
host: v.host,
|
|
11613
11779
|
external_id: v.session,
|
|
11614
11780
|
branch: v.branch,
|
|
11615
11781
|
started_at: iso(v.at)
|
|
@@ -11618,44 +11784,49 @@ async function write(db, batch, projects) {
|
|
|
11618
11784
|
const p = m.kind === "message" ? projects.get(m.project) : undefined;
|
|
11619
11785
|
if (m.kind !== "message" || !p)
|
|
11620
11786
|
return [];
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11787
|
+
return [{ m, session: sessionId(p.id, m.host, m.session) }];
|
|
11788
|
+
});
|
|
11789
|
+
const present = async () => {
|
|
11790
|
+
let n = 0;
|
|
11791
|
+
for (const part of chunks(messages))
|
|
11792
|
+
for (const [session, ids] of Map.groupBy(part, (x) => x.session))
|
|
11793
|
+
n += (await trx.selectFrom("source").select("id").where("kind", "=", "session_message").where("session_id", "=", session).where("external_id", "in", ids.map((x) => x.m.id)).execute()).length;
|
|
11794
|
+
return n;
|
|
11795
|
+
};
|
|
11796
|
+
const before = await present();
|
|
11797
|
+
const now = iso(Date.now());
|
|
11798
|
+
for (const part of chunks(messages))
|
|
11628
11799
|
await trx.insertInto("capture_message").values(part.map((x) => ({
|
|
11629
|
-
id: x.id,
|
|
11630
|
-
conversation_id: x.conversation,
|
|
11631
11800
|
external_id: x.m.id,
|
|
11801
|
+
session_id: x.session,
|
|
11632
11802
|
turn_id: x.m.turn,
|
|
11633
|
-
|
|
11634
|
-
|
|
11803
|
+
speaker: x.m.speaker,
|
|
11804
|
+
created_at: iso(x.m.at),
|
|
11805
|
+
captured_at: now,
|
|
11806
|
+
text: x.m.body,
|
|
11635
11807
|
truncated: x.m.truncated ? 1 : 0,
|
|
11808
|
+
redacted: x.m.redacted ? 1 : 0,
|
|
11636
11809
|
original_bytes: x.m.originalBytes,
|
|
11637
|
-
|
|
11638
|
-
content_hash: sha256(x.m.body),
|
|
11639
|
-
indexed: indexesMessage(x.m.host, x.m.speaker) ? 1 : 0
|
|
11810
|
+
content_hash: sha256(x.m.body)
|
|
11640
11811
|
}))).execute();
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
const files = batch.flatMap((r) => {
|
|
11646
|
-
const p = r.kind === "file" ? projects.get(r.project) : undefined;
|
|
11647
|
-
if (r.kind !== "file" || !p)
|
|
11812
|
+
const after = await present();
|
|
11813
|
+
const edits = batch.flatMap((r) => {
|
|
11814
|
+
const p = r.kind === "edit" ? projects.get(r.project) : undefined;
|
|
11815
|
+
if (r.kind !== "edit" || !p)
|
|
11648
11816
|
return [];
|
|
11649
11817
|
return [
|
|
11650
11818
|
{
|
|
11651
|
-
|
|
11819
|
+
session_id: sessionId(p.id, r.host, r.session),
|
|
11820
|
+
turn_id: r.turn,
|
|
11821
|
+
tool_event_id: r.event,
|
|
11652
11822
|
path: r.path,
|
|
11653
|
-
|
|
11823
|
+
via: r.via,
|
|
11824
|
+
observed_at: iso(r.at)
|
|
11654
11825
|
}
|
|
11655
11826
|
];
|
|
11656
11827
|
});
|
|
11657
|
-
for (const part of chunks(
|
|
11658
|
-
await trx.insertInto("
|
|
11828
|
+
for (const part of chunks(edits))
|
|
11829
|
+
await trx.insertInto("capture_edit").values(part).execute();
|
|
11659
11830
|
return after - before;
|
|
11660
11831
|
});
|
|
11661
11832
|
}
|
|
@@ -11671,7 +11842,7 @@ async function flush(file = dbFile()) {
|
|
|
11671
11842
|
const held = unregisteredDir();
|
|
11672
11843
|
const list = (from) => {
|
|
11673
11844
|
try {
|
|
11674
|
-
return
|
|
11845
|
+
return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((name) => ({ name, from }));
|
|
11675
11846
|
} catch {
|
|
11676
11847
|
return [];
|
|
11677
11848
|
}
|
|
@@ -11681,15 +11852,18 @@ async function flush(file = dbFile()) {
|
|
|
11681
11852
|
return { sent: 0, deferred: 0, rejected: 0 };
|
|
11682
11853
|
const records = [];
|
|
11683
11854
|
for (const { name, from } of names) {
|
|
11855
|
+
let r;
|
|
11684
11856
|
try {
|
|
11685
|
-
|
|
11686
|
-
name,
|
|
11687
|
-
from,
|
|
11688
|
-
r: JSON.parse(fs3.readFileSync(path3.join(from, name), "utf8"))
|
|
11689
|
-
});
|
|
11857
|
+
r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
|
|
11690
11858
|
} catch {
|
|
11691
|
-
|
|
11859
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11860
|
+
fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
|
|
11861
|
+
continue;
|
|
11692
11862
|
}
|
|
11863
|
+
if (r)
|
|
11864
|
+
records.push({ name, from, r });
|
|
11865
|
+
else
|
|
11866
|
+
fs4.rmSync(path5.join(from, name), { force: true });
|
|
11693
11867
|
}
|
|
11694
11868
|
const db = openWriter("capture", file);
|
|
11695
11869
|
client = db;
|
|
@@ -11703,8 +11877,7 @@ async function flush(file = dbFile()) {
|
|
|
11703
11877
|
} catch (e) {
|
|
11704
11878
|
if (!rejected(e))
|
|
11705
11879
|
throw e;
|
|
11706
|
-
const
|
|
11707
|
-
for (const x of ordered) {
|
|
11880
|
+
for (const x of known) {
|
|
11708
11881
|
try {
|
|
11709
11882
|
sent += await write(db, [x.r], projects);
|
|
11710
11883
|
} catch (e2) {
|
|
@@ -11714,15 +11887,11 @@ async function flush(file = dbFile()) {
|
|
|
11714
11887
|
}
|
|
11715
11888
|
}
|
|
11716
11889
|
}
|
|
11717
|
-
const lost = new Set(bad.flatMap((x) => x.r.kind === "message" && x.r.speaker === "self" ? [`${x.r.session}\x00${x.r.id}`] : []));
|
|
11718
|
-
for (const x of known)
|
|
11719
|
-
if (x.r.kind === "file" && lost.has(`${x.r.session}\x00${x.r.message}`) && !bad.includes(x))
|
|
11720
|
-
bad.push(x);
|
|
11721
11890
|
if (bad.length) {
|
|
11722
|
-
|
|
11891
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11723
11892
|
for (const x of bad) {
|
|
11724
11893
|
try {
|
|
11725
|
-
|
|
11894
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
|
|
11726
11895
|
} catch (e) {
|
|
11727
11896
|
if (e.code !== "ENOENT")
|
|
11728
11897
|
throw e;
|
|
@@ -11730,12 +11899,12 @@ async function flush(file = dbFile()) {
|
|
|
11730
11899
|
}
|
|
11731
11900
|
}
|
|
11732
11901
|
if (strayed.length) {
|
|
11733
|
-
|
|
11902
|
+
fs4.mkdirSync(held, { recursive: true, mode: 448 });
|
|
11734
11903
|
for (const x of strayed) {
|
|
11735
11904
|
if (x.from === held)
|
|
11736
11905
|
continue;
|
|
11737
11906
|
try {
|
|
11738
|
-
|
|
11907
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
|
|
11739
11908
|
} catch (e) {
|
|
11740
11909
|
if (e.code !== "ENOENT")
|
|
11741
11910
|
throw e;
|
|
@@ -11745,7 +11914,7 @@ async function flush(file = dbFile()) {
|
|
|
11745
11914
|
const moved = new Set([...bad, ...strayed].map((x) => x.name));
|
|
11746
11915
|
for (const x of records)
|
|
11747
11916
|
if (!moved.has(x.name))
|
|
11748
|
-
|
|
11917
|
+
fs4.rmSync(path5.join(x.from, x.name), { force: true });
|
|
11749
11918
|
prune(held);
|
|
11750
11919
|
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
|
|
11751
11920
|
return { sent, deferred: strayed.length, rejected: bad.length };
|
|
@@ -11785,11 +11954,12 @@ if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
|
|
|
11785
11954
|
export {
|
|
11786
11955
|
MAX_MESSAGE,
|
|
11787
11956
|
answersOf,
|
|
11957
|
+
branchOf,
|
|
11788
11958
|
captureNotice,
|
|
11959
|
+
current,
|
|
11789
11960
|
fit,
|
|
11790
11961
|
flush,
|
|
11791
11962
|
isOwnerTurn,
|
|
11792
|
-
lock,
|
|
11793
11963
|
onHook,
|
|
11794
11964
|
readInput,
|
|
11795
11965
|
readState,
|