sphica 0.4.0 → 0.5.1
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 +65 -44
- package/db/schema.sql +631 -208
- package/dist/capture.js +355 -177
- package/dist/cli.js +13320 -34389
- package/dist/deliver.js +31950 -0
- package/dist/mcp-record.js +48261 -0
- package/dist/mcp.js +783 -788
- 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 +43 -93
- package/skills/review/SKILL.md +12 -13
- package/skills/review/reviewers/precedent.md +25 -38
- package/skills/trace/SKILL.md +72 -96
- 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/db/migrations/0006_harvest_provenance.sql +0 -45
- package/db/migrations/0007_drop_bulk_import.sql +0 -238
- package/skills/trace/example.json +0 -108
package/dist/capture.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// server/src/capture.ts
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import path3 from "node:path";
|
|
5
|
+
import fs4 from "node:fs";
|
|
6
|
+
import path5 from "node:path";
|
|
8
7
|
|
|
9
8
|
// server/node_modules/kysely/dist/util/object-utils.js
|
|
10
9
|
function isUndefined(obj) {
|
|
@@ -10813,8 +10812,10 @@ import fs from "node:fs";
|
|
|
10813
10812
|
import os from "node:os";
|
|
10814
10813
|
import path from "node:path";
|
|
10815
10814
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
10816
|
-
var
|
|
10817
|
-
var
|
|
10815
|
+
var SCHEMA_GENERATION = 2;
|
|
10816
|
+
var SCHEMA_REVISION = 1;
|
|
10817
|
+
var sphicaHome = () => process.env.SPHICA_HOME || path.join(os.homedir(), ".sphica");
|
|
10818
|
+
var dbFile = () => process.env.SPHICA_DB || path.join(sphicaHome(), "sphica.db");
|
|
10818
10819
|
function requireRuntime() {
|
|
10819
10820
|
const proto = DatabaseSync.prototype;
|
|
10820
10821
|
if (typeof proto.setAuthorizer !== "function" || typeof proto.enableDefensive !== "function")
|
|
@@ -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,10 +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(["
|
|
10877
|
+
var JSON_COLUMNS = new Set(["next"]);
|
|
10860
10878
|
var TOP_LEVEL = /^\$\[\d+\]\."([^"]+)"$/;
|
|
10861
10879
|
var parseJson = new ParseJSONResultsPlugin({
|
|
10862
10880
|
shouldParse: (_value, jsonPath) => JSON_COLUMNS.has(jsonPath.match(TOP_LEVEL)?.[1] ?? "")
|
|
@@ -10901,13 +10919,27 @@ var HIRAGANA_ONLY = /^[\p{Script=Hiragana}ー]+$/u;
|
|
|
10901
10919
|
var STOP = new Set(["the", "a", "an", "of", "to", "in", "is", "and", "or", "for", "on", "it", "be"]);
|
|
10902
10920
|
var IDENT = /#\d+|[a-z0-9][a-z0-9_./#-]*[a-z0-9]/g;
|
|
10903
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
|
+
}
|
|
10904
10936
|
function terms(text) {
|
|
10905
10937
|
const norm = text.normalize("NFKC").toLowerCase();
|
|
10906
10938
|
const out = [];
|
|
10907
10939
|
const keep = (w) => {
|
|
10908
10940
|
if (w.length > MAX_TERM || STOP.has(w) || HIRAGANA_ONLY.test(w))
|
|
10909
10941
|
return;
|
|
10910
|
-
out.push(w);
|
|
10942
|
+
out.push(OKURIGANA.exec(w)?.[1] ?? singular(w));
|
|
10911
10943
|
};
|
|
10912
10944
|
for (const s of segmenter.segment(norm))
|
|
10913
10945
|
if (s.isWordLike)
|
|
@@ -10917,6 +10949,48 @@ function terms(text) {
|
|
|
10917
10949
|
keep(m[0]);
|
|
10918
10950
|
return out.filter(Boolean);
|
|
10919
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
|
+
]);
|
|
10920
10994
|
var sha256 = (s) => crypto.createHash("sha256").update(s).digest();
|
|
10921
10995
|
function uuidFrom(...parts) {
|
|
10922
10996
|
const b = crypto.createHash("sha256").update(parts.join("\x00")).digest().subarray(0, 16);
|
|
@@ -11110,16 +11184,21 @@ var DDL = () => ddl ??= new Set([
|
|
|
11110
11184
|
return code;
|
|
11111
11185
|
}));
|
|
11112
11186
|
var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
|
|
11113
|
-
var CAPTURE_VIEWS = new Set(["
|
|
11187
|
+
var CAPTURE_VIEWS = new Set(["capture_session", "capture_message", "capture_edit", "capture_delivery"]);
|
|
11114
11188
|
var TRIGGER_WRITES = {
|
|
11115
|
-
|
|
11116
|
-
capture_message_insert: new Set(["
|
|
11117
|
-
|
|
11118
|
-
|
|
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"])
|
|
11119
11197
|
};
|
|
11120
11198
|
var CAPTURE_READS = {
|
|
11121
11199
|
project: new Set(["id", "key", "name"]),
|
|
11122
|
-
|
|
11200
|
+
session: new Set(["id"]),
|
|
11201
|
+
source: new Set(["id", "session_id", "external_id", "kind"])
|
|
11123
11202
|
};
|
|
11124
11203
|
function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
11125
11204
|
const table = p1 ?? "";
|
|
@@ -11139,7 +11218,7 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
|
11139
11218
|
return CAPTURE_READS[table]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11140
11219
|
}
|
|
11141
11220
|
if (action === C2.SQLITE_FUNCTION)
|
|
11142
|
-
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;
|
|
11143
11222
|
if (action === C2.SQLITE_PRAGMA)
|
|
11144
11223
|
return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11145
11224
|
if (action === C2.SQLITE_SELECT || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
|
|
@@ -11159,7 +11238,7 @@ function connectWriter(role, file = dbFile(), create = false) {
|
|
|
11159
11238
|
requireFile(file);
|
|
11160
11239
|
const raw = new DatabaseSync2(file);
|
|
11161
11240
|
try {
|
|
11162
|
-
prepare(raw, role === "ingest");
|
|
11241
|
+
prepare(raw, create ? "none" : role === "ingest" ? "revision" : "generation");
|
|
11163
11242
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
11164
11243
|
} catch (e) {
|
|
11165
11244
|
raw.close();
|
|
@@ -11190,8 +11269,7 @@ function openWriter(role, file = dbFile()) {
|
|
|
11190
11269
|
}
|
|
11191
11270
|
|
|
11192
11271
|
// server/src/knowledge.ts
|
|
11193
|
-
var
|
|
11194
|
-
var conversationId = (projectId, origin, externalId) => uuidFrom(String(projectId), origin, externalId);
|
|
11272
|
+
var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), host, externalId);
|
|
11195
11273
|
|
|
11196
11274
|
// server/src/panel.ts
|
|
11197
11275
|
import { stripVTControlCharacters, styleText } from "node:util";
|
|
@@ -11209,9 +11287,15 @@ var plain2 = (s) => visible(stripVTControlCharacters(s.replace(STRING_SEQUENCE,
|
|
|
11209
11287
|
// server/src/project.ts
|
|
11210
11288
|
import { execFileSync } from "node:child_process";
|
|
11211
11289
|
import fs2 from "node:fs";
|
|
11212
|
-
import
|
|
11290
|
+
import path3 from "node:path";
|
|
11291
|
+
|
|
11292
|
+
// server/src/anchors.ts
|
|
11213
11293
|
import path2 from "node:path";
|
|
11214
|
-
var
|
|
11294
|
+
var MAX_BYTES = 2 * 1024 * 1024;
|
|
11295
|
+
var leaves = (rel) => rel === ".." || rel.startsWith(`..${path2.sep}`) || path2.isAbsolute(rel);
|
|
11296
|
+
|
|
11297
|
+
// server/src/project.ts
|
|
11298
|
+
var localFile = () => path3.join(sphicaHome(), "projects.json");
|
|
11215
11299
|
var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
|
|
11216
11300
|
function normalizeRemote(url) {
|
|
11217
11301
|
const raw = String(url ?? "").trim();
|
|
@@ -11256,32 +11340,32 @@ function localMap() {
|
|
|
11256
11340
|
} catch {
|
|
11257
11341
|
m = null;
|
|
11258
11342
|
}
|
|
11259
|
-
if (!m || typeof m !== "object" || Array.isArray(m) || Object.entries(m).some(([root, name]) => !
|
|
11343
|
+
if (!m || typeof m !== "object" || Array.isArray(m) || Object.entries(m).some(([root, name]) => !path3.isAbsolute(root) || typeof name !== "string" || !LOCAL_KEY.test(name)))
|
|
11260
11344
|
throw new Error(`${localFile()} is not a valid JSON project table. Fix or delete it, then name the project again.`);
|
|
11261
11345
|
return m;
|
|
11262
11346
|
}
|
|
11263
11347
|
function identify(dir) {
|
|
11264
|
-
const given =
|
|
11348
|
+
const given = path3.resolve(dir);
|
|
11265
11349
|
const top = git(given, "rev-parse", "--show-toplevel");
|
|
11266
11350
|
const root = top || given;
|
|
11267
11351
|
const remote = top ? normalizeRemote(git(root, "remote", "get-url", "origin")) : null;
|
|
11268
11352
|
if (remote)
|
|
11269
11353
|
return { key: `git:${remote}`, root, name: remote.split("/").slice(1).join("/") || remote };
|
|
11270
11354
|
const map = localMap();
|
|
11271
|
-
for (let d = root;; d =
|
|
11355
|
+
for (let d = root;; d = path3.dirname(d)) {
|
|
11272
11356
|
const local = map[d];
|
|
11273
11357
|
if (local && LOCAL_KEY.test(local))
|
|
11274
11358
|
return { key: `local:${local}`, root: d, name: local };
|
|
11275
|
-
if (top ||
|
|
11359
|
+
if (top || path3.dirname(d) === d)
|
|
11276
11360
|
return null;
|
|
11277
11361
|
}
|
|
11278
11362
|
}
|
|
11279
11363
|
function relativeTo(root, file, cwd = root) {
|
|
11280
|
-
const abs =
|
|
11281
|
-
const rel =
|
|
11282
|
-
if (!rel ||
|
|
11364
|
+
const abs = path3.resolve(cwd, file);
|
|
11365
|
+
const rel = path3.relative(root, abs);
|
|
11366
|
+
if (!rel || leaves(rel))
|
|
11283
11367
|
return null;
|
|
11284
|
-
return rel.split(
|
|
11368
|
+
return rel.split(path3.sep).join("/");
|
|
11285
11369
|
}
|
|
11286
11370
|
function patchPaths(patch) {
|
|
11287
11371
|
const out = [];
|
|
@@ -11294,23 +11378,104 @@ function patchPaths(patch) {
|
|
|
11294
11378
|
return out;
|
|
11295
11379
|
}
|
|
11296
11380
|
|
|
11381
|
+
// server/src/worktree.ts
|
|
11382
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
11383
|
+
import fs3 from "node:fs";
|
|
11384
|
+
import path4 from "node:path";
|
|
11385
|
+
var MAX_PATHS = 200;
|
|
11386
|
+
var git2 = (root, args) => {
|
|
11387
|
+
try {
|
|
11388
|
+
return execFileSync2("git", ["-C", root, ...args], {
|
|
11389
|
+
encoding: "utf8",
|
|
11390
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
11391
|
+
timeout: 5000,
|
|
11392
|
+
maxBuffer: 16 * 1024 * 1024
|
|
11393
|
+
});
|
|
11394
|
+
} catch {
|
|
11395
|
+
return null;
|
|
11396
|
+
}
|
|
11397
|
+
};
|
|
11398
|
+
var usable = (p) => p !== "" && !p.includes("\\");
|
|
11399
|
+
function snapshot(root) {
|
|
11400
|
+
const out = git2(root, ["status", "--porcelain=v2", "-z", "--untracked-files=all"]);
|
|
11401
|
+
if (out === null)
|
|
11402
|
+
return null;
|
|
11403
|
+
const fields = out.split("\x00");
|
|
11404
|
+
const entries = {};
|
|
11405
|
+
for (let i = 0;i < fields.length; i++) {
|
|
11406
|
+
const f = fields[i] ?? "";
|
|
11407
|
+
const skip = { "1": 8, "2": 9, u: 10, "?": 1 }[f[0] ?? ""];
|
|
11408
|
+
if (!skip)
|
|
11409
|
+
continue;
|
|
11410
|
+
const parts = f.split(" ");
|
|
11411
|
+
const p = parts.slice(skip).join(" ");
|
|
11412
|
+
if (f[0] === "2")
|
|
11413
|
+
i++;
|
|
11414
|
+
if (!usable(p))
|
|
11415
|
+
continue;
|
|
11416
|
+
let st;
|
|
11417
|
+
try {
|
|
11418
|
+
st = fs3.lstatSync(path4.join(root, p), { throwIfNoEntry: false });
|
|
11419
|
+
} catch {
|
|
11420
|
+
st = undefined;
|
|
11421
|
+
}
|
|
11422
|
+
entries[p] = `${parts.slice(0, skip).join(" ")} ${st ? `${st.size}:${st.mtimeMs}` : "gone"}`;
|
|
11423
|
+
}
|
|
11424
|
+
const head2 = git2(root, ["rev-parse", "--verify", "-q", "HEAD"])?.trim() || null;
|
|
11425
|
+
return { head: head2, entries };
|
|
11426
|
+
}
|
|
11427
|
+
function changed(root, before, after) {
|
|
11428
|
+
const paths = new Set;
|
|
11429
|
+
for (const p of new Set([...Object.keys(before.entries), ...Object.keys(after.entries)]))
|
|
11430
|
+
if (before.entries[p] !== after.entries[p])
|
|
11431
|
+
paths.add(p);
|
|
11432
|
+
if (before.head && after.head && before.head !== after.head && git2(root, ["merge-base", "--is-ancestor", before.head, after.head]) !== null) {
|
|
11433
|
+
for (const p of (git2(root, ["diff", "--name-only", "-z", before.head, after.head]) ?? "").split("\x00"))
|
|
11434
|
+
if (usable(p))
|
|
11435
|
+
paths.add(p);
|
|
11436
|
+
}
|
|
11437
|
+
return [...paths].sort().slice(0, MAX_PATHS);
|
|
11438
|
+
}
|
|
11439
|
+
|
|
11297
11440
|
// server/src/capture.ts
|
|
11298
|
-
var spoolDir = () =>
|
|
11299
|
-
var stateFile = () =>
|
|
11300
|
-
var rejectedDir = () =>
|
|
11301
|
-
var unregisteredDir = () =>
|
|
11441
|
+
var spoolDir = () => path5.join(sphicaHome(), "spool");
|
|
11442
|
+
var stateFile = () => path5.join(sphicaHome(), "capture.json");
|
|
11443
|
+
var rejectedDir = () => path5.join(spoolDir(), "rejected");
|
|
11444
|
+
var unregisteredDir = () => path5.join(spoolDir(), "unregistered");
|
|
11302
11445
|
var HOLD_DAYS = 30;
|
|
11303
11446
|
var HOLD_MAX = 1000;
|
|
11447
|
+
var baselineDir = () => path5.join(sphicaHome(), "worktree");
|
|
11448
|
+
function current(raw) {
|
|
11449
|
+
const r = raw;
|
|
11450
|
+
if (r.v === 2)
|
|
11451
|
+
return raw;
|
|
11452
|
+
if (r.v !== 1)
|
|
11453
|
+
throw new Error(`unknown queue record version ${String(r.v)}`);
|
|
11454
|
+
const old = raw;
|
|
11455
|
+
if (old.kind === "message")
|
|
11456
|
+
return {
|
|
11457
|
+
...old,
|
|
11458
|
+
v: 2,
|
|
11459
|
+
kind: "message",
|
|
11460
|
+
speaker: old.speaker === "self" ? "owner" : "assistant",
|
|
11461
|
+
redacted: false
|
|
11462
|
+
};
|
|
11463
|
+
if (old.action !== "edit")
|
|
11464
|
+
return null;
|
|
11465
|
+
return { ...old, v: 2, kind: "edit", event: null, path: old.path, via: "tool" };
|
|
11466
|
+
}
|
|
11304
11467
|
var MAX_MESSAGE = 128 * 1024;
|
|
11305
11468
|
var KEEP = 8 * 1024;
|
|
11306
11469
|
function fit(body) {
|
|
11307
11470
|
const all = bytes(body);
|
|
11308
11471
|
if (all <= MAX_MESSAGE) {
|
|
11309
11472
|
const kept = mask(body);
|
|
11310
|
-
return { body: kept, truncated: false,
|
|
11473
|
+
return { body: kept, truncated: false, redacted: kept !== body, originalBytes: all };
|
|
11311
11474
|
}
|
|
11312
|
-
const
|
|
11313
|
-
const
|
|
11475
|
+
const start = head(body, KEEP * 2);
|
|
11476
|
+
const end = tail(body, KEEP * 2);
|
|
11477
|
+
const a = head(mask(start), KEEP);
|
|
11478
|
+
const z = tail(mask(end), KEEP);
|
|
11314
11479
|
const cut = all - bytes(a) - bytes(z);
|
|
11315
11480
|
return {
|
|
11316
11481
|
body: `${a}
|
|
@@ -11319,13 +11484,14 @@ function fit(body) {
|
|
|
11319
11484
|
|
|
11320
11485
|
${z}`,
|
|
11321
11486
|
truncated: true,
|
|
11487
|
+
redacted: mask(start) !== start || mask(end) !== end,
|
|
11322
11488
|
originalBytes: all
|
|
11323
11489
|
};
|
|
11324
11490
|
}
|
|
11325
11491
|
function prune(held) {
|
|
11326
11492
|
let files;
|
|
11327
11493
|
try {
|
|
11328
|
-
files =
|
|
11494
|
+
files = fs4.readdirSync(held).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
|
|
11329
11495
|
} catch {
|
|
11330
11496
|
return;
|
|
11331
11497
|
}
|
|
@@ -11333,21 +11499,21 @@ function prune(held) {
|
|
|
11333
11499
|
const stale = files.filter((f) => Number(f.split("-")[0]) < cutoff);
|
|
11334
11500
|
const over = files.slice(0, Math.max(0, files.length - HOLD_MAX));
|
|
11335
11501
|
for (const f of new Set([...stale, ...over]))
|
|
11336
|
-
|
|
11502
|
+
fs4.rmSync(path5.join(held, f), { force: true });
|
|
11337
11503
|
}
|
|
11338
11504
|
function spool(record) {
|
|
11339
11505
|
const dir = spoolDir();
|
|
11340
|
-
|
|
11506
|
+
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
11341
11507
|
const name = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}.json`;
|
|
11342
|
-
const tmp =
|
|
11343
|
-
|
|
11344
|
-
|
|
11508
|
+
const tmp = path5.join(dir, `.${name}`);
|
|
11509
|
+
fs4.writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
|
|
11510
|
+
fs4.renameSync(tmp, path5.join(dir, name));
|
|
11345
11511
|
}
|
|
11346
11512
|
var branchOf = (root) => {
|
|
11347
11513
|
try {
|
|
11348
|
-
const dotgit =
|
|
11349
|
-
const gitdir =
|
|
11350
|
-
const h =
|
|
11514
|
+
const dotgit = path5.join(root, ".git");
|
|
11515
|
+
const gitdir = fs4.statSync(dotgit).isFile() ? path5.resolve(root, fs4.readFileSync(dotgit, "utf8").match(/^gitdir: (.+)$/m)?.[1]?.trim() ?? "") : dotgit;
|
|
11516
|
+
const h = fs4.readFileSync(path5.join(gitdir, "HEAD"), "utf8").trim();
|
|
11351
11517
|
return h.startsWith("ref: refs/heads/") ? h.slice("ref: refs/heads/".length) : null;
|
|
11352
11518
|
} catch {
|
|
11353
11519
|
return null;
|
|
@@ -11368,63 +11534,30 @@ var INJECTED = [
|
|
|
11368
11534
|
/^(?:Another Claude|A peer) session sent a message(?: while you were working)?:/
|
|
11369
11535
|
];
|
|
11370
11536
|
var digest = (s) => sha256(s).toString("hex").slice(0, 16);
|
|
11371
|
-
var saidDir = () => path3.join(spoolDir(), "said");
|
|
11372
|
-
var RENAME_TRIES = 20;
|
|
11373
|
-
var RENAME_WAIT_MS = 15;
|
|
11374
|
-
var sleepSync = (ms) => {
|
|
11375
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
11376
|
-
};
|
|
11377
|
-
function remember(session, id) {
|
|
11378
|
-
const dir = saidDir();
|
|
11379
|
-
fs3.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
11380
|
-
const file = path3.join(dir, uuidFrom(session));
|
|
11381
|
-
const tmp = `${file}.${process.pid}`;
|
|
11382
|
-
fs3.writeFileSync(tmp, id, { mode: 384 });
|
|
11383
|
-
for (let i = 0;; i++) {
|
|
11384
|
-
try {
|
|
11385
|
-
fs3.renameSync(tmp, file);
|
|
11386
|
-
break;
|
|
11387
|
-
} catch (e) {
|
|
11388
|
-
const code = e.code;
|
|
11389
|
-
if (i >= RENAME_TRIES || code !== "EPERM" && code !== "EBUSY") {
|
|
11390
|
-
fs3.rmSync(tmp, { force: true });
|
|
11391
|
-
throw e;
|
|
11392
|
-
}
|
|
11393
|
-
sleepSync(RENAME_WAIT_MS);
|
|
11394
|
-
}
|
|
11395
|
-
}
|
|
11396
|
-
const old = Date.now() - 30 * 86400000;
|
|
11397
|
-
for (const f of fs3.readdirSync(dir)) {
|
|
11398
|
-
const st = fs3.statSync(path3.join(dir, f), { throwIfNoEntry: false });
|
|
11399
|
-
if (st && st.mtimeMs < old)
|
|
11400
|
-
fs3.rmSync(path3.join(dir, f), { force: true });
|
|
11401
|
-
}
|
|
11402
|
-
}
|
|
11403
|
-
function lastSaid(session) {
|
|
11404
|
-
try {
|
|
11405
|
-
return fs3.readFileSync(path3.join(saidDir(), uuidFrom(session)), "utf8");
|
|
11406
|
-
} catch {
|
|
11407
|
-
return null;
|
|
11408
|
-
}
|
|
11409
|
-
}
|
|
11410
11537
|
function answersOf(input) {
|
|
11411
11538
|
const response = input.tool_response;
|
|
11412
11539
|
const answers = response?.answers;
|
|
11413
11540
|
if (!answers || typeof answers !== "object")
|
|
11414
11541
|
return null;
|
|
11415
|
-
const
|
|
11416
|
-
|
|
11417
|
-
|
|
11542
|
+
const pairs = Object.entries(answers);
|
|
11543
|
+
if (!pairs.length)
|
|
11544
|
+
return null;
|
|
11545
|
+
return {
|
|
11546
|
+
questions: pairs.map(([q], i) => `Q${i + 1}: ${q}`).join(`
|
|
11547
|
+
|
|
11548
|
+
`),
|
|
11549
|
+
answers: pairs.map(([q, a], i) => {
|
|
11550
|
+
const notes = response?.annotations?.[q]?.notes;
|
|
11551
|
+
const memo = typeof notes === "string" && notes.trim() ? `
|
|
11418
11552
|
Notes: ${notes.trim()}` : "";
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
});
|
|
11422
|
-
return lines.length ? lines.join(`
|
|
11553
|
+
return `A${i + 1}: ${Array.isArray(a) ? a.join(" / ") : String(a)}${memo}`;
|
|
11554
|
+
}).join(`
|
|
11423
11555
|
|
|
11424
|
-
`)
|
|
11556
|
+
`)
|
|
11557
|
+
};
|
|
11425
11558
|
}
|
|
11426
11559
|
function captureNotice(file = dbFile()) {
|
|
11427
|
-
if (!
|
|
11560
|
+
if (!fs4.existsSync(file))
|
|
11428
11561
|
return panel("sphica: no database, so conversations are not recorded", [file], "Create it with sphica init");
|
|
11429
11562
|
const s = readState();
|
|
11430
11563
|
if (s.stuck)
|
|
@@ -11433,6 +11566,34 @@ function captureNotice(file = dbFile()) {
|
|
|
11433
11566
|
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");
|
|
11434
11567
|
return null;
|
|
11435
11568
|
}
|
|
11569
|
+
var baselineFile = (host, session) => path5.join(baselineDir(), `${digest(`${host}\x00${session}`)}.json`);
|
|
11570
|
+
function readBaseline(file) {
|
|
11571
|
+
try {
|
|
11572
|
+
return JSON.parse(fs4.readFileSync(file, "utf8"));
|
|
11573
|
+
} catch {
|
|
11574
|
+
return null;
|
|
11575
|
+
}
|
|
11576
|
+
}
|
|
11577
|
+
function writeBaseline(file, b) {
|
|
11578
|
+
fs4.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
|
|
11579
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
11580
|
+
fs4.writeFileSync(tmp, JSON.stringify(b), { mode: 384 });
|
|
11581
|
+
fs4.renameSync(tmp, file);
|
|
11582
|
+
}
|
|
11583
|
+
function pruneBaselines() {
|
|
11584
|
+
const cutoff = Date.now() - HOLD_DAYS * 24 * 60 * 60 * 1000;
|
|
11585
|
+
let files;
|
|
11586
|
+
try {
|
|
11587
|
+
files = fs4.readdirSync(baselineDir());
|
|
11588
|
+
} catch {
|
|
11589
|
+
return;
|
|
11590
|
+
}
|
|
11591
|
+
for (const f of files) {
|
|
11592
|
+
const file = path5.join(baselineDir(), f);
|
|
11593
|
+
if ((fs4.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) < cutoff)
|
|
11594
|
+
fs4.rmSync(file, { force: true });
|
|
11595
|
+
}
|
|
11596
|
+
}
|
|
11436
11597
|
function onHook(host, input) {
|
|
11437
11598
|
const event = input.hook_event_name;
|
|
11438
11599
|
const owner = () => isOwnerTurn(input, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined);
|
|
@@ -11441,9 +11602,14 @@ function onHook(host, input) {
|
|
|
11441
11602
|
return { flush: false };
|
|
11442
11603
|
const file = process.env.CLAUDE_ENV_FILE;
|
|
11443
11604
|
if (file && input.session_id && /^[A-Za-z0-9_-]+$/.test(input.session_id)) {
|
|
11444
|
-
|
|
11605
|
+
fs4.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
|
|
11445
11606
|
`);
|
|
11446
11607
|
}
|
|
11608
|
+
const place2 = identify(input.cwd ?? process.cwd());
|
|
11609
|
+
const now = place2 && snapshot(place2.root);
|
|
11610
|
+
if (now)
|
|
11611
|
+
writeBaseline(baselineFile(host, String(input.session_id)), { ...now, running: false });
|
|
11612
|
+
pruneBaselines();
|
|
11447
11613
|
return { flush: false, notice: captureNotice() };
|
|
11448
11614
|
}
|
|
11449
11615
|
if (!owner())
|
|
@@ -11458,7 +11624,7 @@ function onHook(host, input) {
|
|
|
11458
11624
|
return { flush: false };
|
|
11459
11625
|
const at = new Date().toISOString();
|
|
11460
11626
|
const base = {
|
|
11461
|
-
v:
|
|
11627
|
+
v: 2,
|
|
11462
11628
|
host,
|
|
11463
11629
|
session: String(input.session_id),
|
|
11464
11630
|
project: place.key,
|
|
@@ -11466,23 +11632,37 @@ function onHook(host, input) {
|
|
|
11466
11632
|
turn,
|
|
11467
11633
|
at
|
|
11468
11634
|
};
|
|
11469
|
-
const say = (key, speaker, raw) => {
|
|
11635
|
+
const say = (key, speaker, raw, when = at) => {
|
|
11470
11636
|
const kept = fit(clean(raw).trim());
|
|
11471
11637
|
if (!kept.body.trim())
|
|
11472
11638
|
return;
|
|
11473
11639
|
const id = `${key}:${digest(kept.body)}`;
|
|
11474
|
-
spool({ ...base, kind: "message", id, speaker, ...kept });
|
|
11475
|
-
if (speaker === "self")
|
|
11476
|
-
remember(base.session, id);
|
|
11640
|
+
spool({ ...base, at: when, kind: "message", id, speaker, ...kept });
|
|
11477
11641
|
};
|
|
11642
|
+
const baseline = baselineFile(host, base.session);
|
|
11643
|
+
if (event === "UserPromptSubmit") {
|
|
11644
|
+
if (!readBaseline(baseline)?.running) {
|
|
11645
|
+
const now = snapshot(place.root);
|
|
11646
|
+
if (now)
|
|
11647
|
+
writeBaseline(baseline, { ...now, running: true });
|
|
11648
|
+
}
|
|
11649
|
+
}
|
|
11478
11650
|
if (event === "UserPromptSubmit" && input.prompt) {
|
|
11479
11651
|
const prompt = input.prompt.trimStart();
|
|
11480
11652
|
if (!INJECTED.some((r) => r.test(prompt)))
|
|
11481
|
-
say(`${turn}:
|
|
11653
|
+
say(`${turn}:owner`, "owner", prompt);
|
|
11482
11654
|
}
|
|
11483
11655
|
if (event === "Stop") {
|
|
11484
11656
|
if (input.last_assistant_message)
|
|
11485
11657
|
say(`${turn}:assistant`, "assistant", input.last_assistant_message);
|
|
11658
|
+
const now = snapshot(place.root);
|
|
11659
|
+
if (now) {
|
|
11660
|
+
const before = readBaseline(baseline);
|
|
11661
|
+
if (before)
|
|
11662
|
+
for (const p of changed(place.root, before, now))
|
|
11663
|
+
spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
|
|
11664
|
+
writeBaseline(baseline, { ...now, running: false });
|
|
11665
|
+
}
|
|
11486
11666
|
return { flush: true };
|
|
11487
11667
|
}
|
|
11488
11668
|
if (event === "PostToolUse") {
|
|
@@ -11490,31 +11670,31 @@ function onHook(host, input) {
|
|
|
11490
11670
|
const ti = input.tool_input ?? {};
|
|
11491
11671
|
if (tool === "AskUserQuestion") {
|
|
11492
11672
|
const said = answersOf(input);
|
|
11493
|
-
if (said)
|
|
11494
|
-
|
|
11673
|
+
if (said) {
|
|
11674
|
+
const id = `${turn}:ask:${input.tool_use_id ?? at}`;
|
|
11675
|
+
say(`${id}:q`, "assistant", said.questions, new Date(Date.parse(at) - 1).toISOString());
|
|
11676
|
+
say(id, "owner", said.answers);
|
|
11677
|
+
}
|
|
11495
11678
|
return { flush: false };
|
|
11496
11679
|
}
|
|
11497
11680
|
if (tool === "Read")
|
|
11498
11681
|
return { flush: false };
|
|
11499
|
-
const message = lastSaid(base.session);
|
|
11500
|
-
if (!message)
|
|
11501
|
-
return { flush: false };
|
|
11502
11682
|
const cwd = input.cwd ?? place.root;
|
|
11503
11683
|
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) ?? []);
|
|
11504
11684
|
for (const p of files)
|
|
11505
|
-
spool({ ...base, kind: "
|
|
11685
|
+
spool({ ...base, kind: "edit", event: input.tool_use_id ?? null, path: p, via: "tool" });
|
|
11506
11686
|
}
|
|
11507
11687
|
return { flush: false };
|
|
11508
11688
|
}
|
|
11509
11689
|
function writeState(s) {
|
|
11510
11690
|
try {
|
|
11511
|
-
|
|
11691
|
+
fs4.writeFileSync(stateFile(), JSON.stringify(s));
|
|
11512
11692
|
} catch {}
|
|
11513
11693
|
}
|
|
11514
11694
|
function readState() {
|
|
11515
11695
|
const count = (dir) => {
|
|
11516
11696
|
try {
|
|
11517
|
-
return
|
|
11697
|
+
return fs4.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).length;
|
|
11518
11698
|
} catch {
|
|
11519
11699
|
return 0;
|
|
11520
11700
|
}
|
|
@@ -11526,7 +11706,7 @@ function readState() {
|
|
|
11526
11706
|
};
|
|
11527
11707
|
let raw = {};
|
|
11528
11708
|
try {
|
|
11529
|
-
const parsed = JSON.parse(
|
|
11709
|
+
const parsed = JSON.parse(fs4.readFileSync(stateFile(), "utf8"));
|
|
11530
11710
|
if (parsed && typeof parsed === "object")
|
|
11531
11711
|
raw = parsed;
|
|
11532
11712
|
} catch {}
|
|
@@ -11540,25 +11720,25 @@ function readState() {
|
|
|
11540
11720
|
};
|
|
11541
11721
|
}
|
|
11542
11722
|
function lock() {
|
|
11543
|
-
const file =
|
|
11544
|
-
|
|
11723
|
+
const file = path5.join(spoolDir(), ".lock");
|
|
11724
|
+
fs4.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
|
|
11545
11725
|
for (let attempt = 0;attempt < 2; attempt++) {
|
|
11546
11726
|
try {
|
|
11547
|
-
|
|
11727
|
+
fs4.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
|
|
11548
11728
|
return () => {
|
|
11549
11729
|
try {
|
|
11550
|
-
if (
|
|
11551
|
-
|
|
11730
|
+
if (fs4.readFileSync(file, "utf8") === String(process.pid))
|
|
11731
|
+
fs4.rmSync(file, { force: true });
|
|
11552
11732
|
} catch {}
|
|
11553
11733
|
};
|
|
11554
11734
|
} catch (e) {
|
|
11555
11735
|
if (e.code !== "EEXIST")
|
|
11556
11736
|
throw e;
|
|
11557
11737
|
}
|
|
11558
|
-
const st =
|
|
11738
|
+
const st = fs4.statSync(file, { throwIfNoEntry: false });
|
|
11559
11739
|
if (!st)
|
|
11560
11740
|
continue;
|
|
11561
|
-
const holder = Number(
|
|
11741
|
+
const holder = Number(fs4.readFileSync(file, "utf8") || 0);
|
|
11562
11742
|
const fresh = Date.now() - st.mtimeMs < 5 * 60000;
|
|
11563
11743
|
const alive = (() => {
|
|
11564
11744
|
if (holder <= 0)
|
|
@@ -11571,7 +11751,7 @@ function lock() {
|
|
|
11571
11751
|
})();
|
|
11572
11752
|
if (alive && fresh)
|
|
11573
11753
|
return null;
|
|
11574
|
-
|
|
11754
|
+
fs4.rmSync(file, { force: true });
|
|
11575
11755
|
}
|
|
11576
11756
|
return null;
|
|
11577
11757
|
}
|
|
@@ -11580,27 +11760,21 @@ var ROWS = 1000;
|
|
|
11580
11760
|
var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
|
|
11581
11761
|
async function write(db, batch, projects) {
|
|
11582
11762
|
return inTransaction(db, async (trx) => {
|
|
11583
|
-
const
|
|
11763
|
+
const sessions = new Map;
|
|
11584
11764
|
for (const r of batch) {
|
|
11585
11765
|
const p = projects.get(r.project);
|
|
11586
11766
|
if (!p)
|
|
11587
11767
|
continue;
|
|
11588
|
-
const id =
|
|
11589
|
-
const prev =
|
|
11768
|
+
const id = sessionId(p.id, r.host, r.session);
|
|
11769
|
+
const prev = sessions.get(id);
|
|
11590
11770
|
if (!prev || Date.parse(r.at) < Date.parse(prev.at))
|
|
11591
|
-
|
|
11592
|
-
project: p.id,
|
|
11593
|
-
host: r.host,
|
|
11594
|
-
session: r.session,
|
|
11595
|
-
branch: r.branch,
|
|
11596
|
-
at: r.at
|
|
11597
|
-
});
|
|
11771
|
+
sessions.set(id, { project: p.id, host: r.host, session: r.session, branch: r.branch, at: r.at });
|
|
11598
11772
|
}
|
|
11599
|
-
for (const part of chunks([...
|
|
11600
|
-
await trx.insertInto("
|
|
11773
|
+
for (const part of chunks([...sessions]))
|
|
11774
|
+
await trx.insertInto("capture_session").values(part.map(([id, v]) => ({
|
|
11601
11775
|
id,
|
|
11602
11776
|
project_id: v.project,
|
|
11603
|
-
|
|
11777
|
+
host: v.host,
|
|
11604
11778
|
external_id: v.session,
|
|
11605
11779
|
branch: v.branch,
|
|
11606
11780
|
started_at: iso(v.at)
|
|
@@ -11609,44 +11783,49 @@ async function write(db, batch, projects) {
|
|
|
11609
11783
|
const p = m.kind === "message" ? projects.get(m.project) : undefined;
|
|
11610
11784
|
if (m.kind !== "message" || !p)
|
|
11611
11785
|
return [];
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11786
|
+
return [{ m, session: sessionId(p.id, m.host, m.session) }];
|
|
11787
|
+
});
|
|
11788
|
+
const present = async () => {
|
|
11789
|
+
let n = 0;
|
|
11790
|
+
for (const part of chunks(messages))
|
|
11791
|
+
for (const [session, ids] of Map.groupBy(part, (x) => x.session))
|
|
11792
|
+
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;
|
|
11793
|
+
return n;
|
|
11794
|
+
};
|
|
11795
|
+
const before = await present();
|
|
11796
|
+
const now = iso(Date.now());
|
|
11797
|
+
for (const part of chunks(messages))
|
|
11619
11798
|
await trx.insertInto("capture_message").values(part.map((x) => ({
|
|
11620
|
-
id: x.id,
|
|
11621
|
-
conversation_id: x.conversation,
|
|
11622
11799
|
external_id: x.m.id,
|
|
11800
|
+
session_id: x.session,
|
|
11623
11801
|
turn_id: x.m.turn,
|
|
11624
|
-
|
|
11625
|
-
|
|
11802
|
+
speaker: x.m.speaker,
|
|
11803
|
+
created_at: iso(x.m.at),
|
|
11804
|
+
captured_at: now,
|
|
11805
|
+
text: x.m.body,
|
|
11626
11806
|
truncated: x.m.truncated ? 1 : 0,
|
|
11807
|
+
redacted: x.m.redacted ? 1 : 0,
|
|
11627
11808
|
original_bytes: x.m.originalBytes,
|
|
11628
|
-
|
|
11629
|
-
content_hash: sha256(x.m.body),
|
|
11630
|
-
indexed: indexesMessage(x.m.speaker) ? 1 : 0
|
|
11809
|
+
content_hash: sha256(x.m.body)
|
|
11631
11810
|
}))).execute();
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
const files = batch.flatMap((r) => {
|
|
11637
|
-
const p = r.kind === "file" ? projects.get(r.project) : undefined;
|
|
11638
|
-
if (r.kind !== "file" || !p)
|
|
11811
|
+
const after = await present();
|
|
11812
|
+
const edits = batch.flatMap((r) => {
|
|
11813
|
+
const p = r.kind === "edit" ? projects.get(r.project) : undefined;
|
|
11814
|
+
if (r.kind !== "edit" || !p)
|
|
11639
11815
|
return [];
|
|
11640
11816
|
return [
|
|
11641
11817
|
{
|
|
11642
|
-
|
|
11818
|
+
session_id: sessionId(p.id, r.host, r.session),
|
|
11819
|
+
turn_id: r.turn,
|
|
11820
|
+
tool_event_id: r.event,
|
|
11643
11821
|
path: r.path,
|
|
11644
|
-
|
|
11822
|
+
via: r.via,
|
|
11823
|
+
observed_at: iso(r.at)
|
|
11645
11824
|
}
|
|
11646
11825
|
];
|
|
11647
11826
|
});
|
|
11648
|
-
for (const part of chunks(
|
|
11649
|
-
await trx.insertInto("
|
|
11827
|
+
for (const part of chunks(edits))
|
|
11828
|
+
await trx.insertInto("capture_edit").values(part).execute();
|
|
11650
11829
|
return after - before;
|
|
11651
11830
|
});
|
|
11652
11831
|
}
|
|
@@ -11662,7 +11841,7 @@ async function flush(file = dbFile()) {
|
|
|
11662
11841
|
const held = unregisteredDir();
|
|
11663
11842
|
const list = (from) => {
|
|
11664
11843
|
try {
|
|
11665
|
-
return
|
|
11844
|
+
return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((name) => ({ name, from }));
|
|
11666
11845
|
} catch {
|
|
11667
11846
|
return [];
|
|
11668
11847
|
}
|
|
@@ -11672,15 +11851,18 @@ async function flush(file = dbFile()) {
|
|
|
11672
11851
|
return { sent: 0, deferred: 0, rejected: 0 };
|
|
11673
11852
|
const records = [];
|
|
11674
11853
|
for (const { name, from } of names) {
|
|
11854
|
+
let r;
|
|
11675
11855
|
try {
|
|
11676
|
-
|
|
11677
|
-
name,
|
|
11678
|
-
from,
|
|
11679
|
-
r: JSON.parse(fs3.readFileSync(path3.join(from, name), "utf8"))
|
|
11680
|
-
});
|
|
11856
|
+
r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
|
|
11681
11857
|
} catch {
|
|
11682
|
-
|
|
11858
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11859
|
+
fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
|
|
11860
|
+
continue;
|
|
11683
11861
|
}
|
|
11862
|
+
if (r)
|
|
11863
|
+
records.push({ name, from, r });
|
|
11864
|
+
else
|
|
11865
|
+
fs4.rmSync(path5.join(from, name), { force: true });
|
|
11684
11866
|
}
|
|
11685
11867
|
const db = openWriter("capture", file);
|
|
11686
11868
|
client = db;
|
|
@@ -11694,8 +11876,7 @@ async function flush(file = dbFile()) {
|
|
|
11694
11876
|
} catch (e) {
|
|
11695
11877
|
if (!rejected(e))
|
|
11696
11878
|
throw e;
|
|
11697
|
-
const
|
|
11698
|
-
for (const x of ordered) {
|
|
11879
|
+
for (const x of known) {
|
|
11699
11880
|
try {
|
|
11700
11881
|
sent += await write(db, [x.r], projects);
|
|
11701
11882
|
} catch (e2) {
|
|
@@ -11705,15 +11886,11 @@ async function flush(file = dbFile()) {
|
|
|
11705
11886
|
}
|
|
11706
11887
|
}
|
|
11707
11888
|
}
|
|
11708
|
-
const lost = new Set(bad.flatMap((x) => x.r.kind === "message" && x.r.speaker === "self" ? [`${x.r.session}\x00${x.r.id}`] : []));
|
|
11709
|
-
for (const x of known)
|
|
11710
|
-
if (x.r.kind === "file" && lost.has(`${x.r.session}\x00${x.r.message}`) && !bad.includes(x))
|
|
11711
|
-
bad.push(x);
|
|
11712
11889
|
if (bad.length) {
|
|
11713
|
-
|
|
11890
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11714
11891
|
for (const x of bad) {
|
|
11715
11892
|
try {
|
|
11716
|
-
|
|
11893
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
|
|
11717
11894
|
} catch (e) {
|
|
11718
11895
|
if (e.code !== "ENOENT")
|
|
11719
11896
|
throw e;
|
|
@@ -11721,12 +11898,12 @@ async function flush(file = dbFile()) {
|
|
|
11721
11898
|
}
|
|
11722
11899
|
}
|
|
11723
11900
|
if (strayed.length) {
|
|
11724
|
-
|
|
11901
|
+
fs4.mkdirSync(held, { recursive: true, mode: 448 });
|
|
11725
11902
|
for (const x of strayed) {
|
|
11726
11903
|
if (x.from === held)
|
|
11727
11904
|
continue;
|
|
11728
11905
|
try {
|
|
11729
|
-
|
|
11906
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
|
|
11730
11907
|
} catch (e) {
|
|
11731
11908
|
if (e.code !== "ENOENT")
|
|
11732
11909
|
throw e;
|
|
@@ -11736,7 +11913,7 @@ async function flush(file = dbFile()) {
|
|
|
11736
11913
|
const moved = new Set([...bad, ...strayed].map((x) => x.name));
|
|
11737
11914
|
for (const x of records)
|
|
11738
11915
|
if (!moved.has(x.name))
|
|
11739
|
-
|
|
11916
|
+
fs4.rmSync(path5.join(x.from, x.name), { force: true });
|
|
11740
11917
|
prune(held);
|
|
11741
11918
|
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
|
|
11742
11919
|
return { sent, deferred: strayed.length, rejected: bad.length };
|
|
@@ -11776,11 +11953,12 @@ if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
|
|
|
11776
11953
|
export {
|
|
11777
11954
|
MAX_MESSAGE,
|
|
11778
11955
|
answersOf,
|
|
11956
|
+
branchOf,
|
|
11779
11957
|
captureNotice,
|
|
11958
|
+
current,
|
|
11780
11959
|
fit,
|
|
11781
11960
|
flush,
|
|
11782
11961
|
isOwnerTurn,
|
|
11783
|
-
lock,
|
|
11784
11962
|
onHook,
|
|
11785
11963
|
readInput,
|
|
11786
11964
|
readState,
|