sphica 0.4.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 +65 -44
- package/db/schema.sql +631 -208
- package/dist/capture.js +353 -174
- package/dist/cli.js +13322 -34392
- package/dist/deliver.js +31951 -0
- package/dist/mcp-record.js +48262 -0
- package/dist/mcp.js +782 -787
- 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,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,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";
|
|
@@ -11210,8 +11288,15 @@ var plain2 = (s) => visible(stripVTControlCharacters(s.replace(STRING_SEQUENCE,
|
|
|
11210
11288
|
import { execFileSync } from "node:child_process";
|
|
11211
11289
|
import fs2 from "node:fs";
|
|
11212
11290
|
import os2 from "node:os";
|
|
11291
|
+
import path3 from "node:path";
|
|
11292
|
+
|
|
11293
|
+
// server/src/anchors.ts
|
|
11213
11294
|
import path2 from "node:path";
|
|
11214
|
-
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");
|
|
11215
11300
|
var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
|
|
11216
11301
|
function normalizeRemote(url) {
|
|
11217
11302
|
const raw = String(url ?? "").trim();
|
|
@@ -11256,32 +11341,32 @@ function localMap() {
|
|
|
11256
11341
|
} catch {
|
|
11257
11342
|
m = null;
|
|
11258
11343
|
}
|
|
11259
|
-
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)))
|
|
11260
11345
|
throw new Error(`${localFile()} is not a valid JSON project table. Fix or delete it, then name the project again.`);
|
|
11261
11346
|
return m;
|
|
11262
11347
|
}
|
|
11263
11348
|
function identify(dir) {
|
|
11264
|
-
const given =
|
|
11349
|
+
const given = path3.resolve(dir);
|
|
11265
11350
|
const top = git(given, "rev-parse", "--show-toplevel");
|
|
11266
11351
|
const root = top || given;
|
|
11267
11352
|
const remote = top ? normalizeRemote(git(root, "remote", "get-url", "origin")) : null;
|
|
11268
11353
|
if (remote)
|
|
11269
11354
|
return { key: `git:${remote}`, root, name: remote.split("/").slice(1).join("/") || remote };
|
|
11270
11355
|
const map = localMap();
|
|
11271
|
-
for (let d = root;; d =
|
|
11356
|
+
for (let d = root;; d = path3.dirname(d)) {
|
|
11272
11357
|
const local = map[d];
|
|
11273
11358
|
if (local && LOCAL_KEY.test(local))
|
|
11274
11359
|
return { key: `local:${local}`, root: d, name: local };
|
|
11275
|
-
if (top ||
|
|
11360
|
+
if (top || path3.dirname(d) === d)
|
|
11276
11361
|
return null;
|
|
11277
11362
|
}
|
|
11278
11363
|
}
|
|
11279
11364
|
function relativeTo(root, file, cwd = root) {
|
|
11280
|
-
const abs =
|
|
11281
|
-
const rel =
|
|
11282
|
-
if (!rel ||
|
|
11365
|
+
const abs = path3.resolve(cwd, file);
|
|
11366
|
+
const rel = path3.relative(root, abs);
|
|
11367
|
+
if (!rel || leaves(rel))
|
|
11283
11368
|
return null;
|
|
11284
|
-
return rel.split(
|
|
11369
|
+
return rel.split(path3.sep).join("/");
|
|
11285
11370
|
}
|
|
11286
11371
|
function patchPaths(patch) {
|
|
11287
11372
|
const out = [];
|
|
@@ -11294,23 +11379,104 @@ function patchPaths(patch) {
|
|
|
11294
11379
|
return out;
|
|
11295
11380
|
}
|
|
11296
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
|
+
|
|
11297
11441
|
// server/src/capture.ts
|
|
11298
|
-
var spoolDir = () =>
|
|
11299
|
-
var stateFile = () =>
|
|
11300
|
-
var rejectedDir = () =>
|
|
11301
|
-
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");
|
|
11302
11446
|
var HOLD_DAYS = 30;
|
|
11303
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
|
+
}
|
|
11304
11468
|
var MAX_MESSAGE = 128 * 1024;
|
|
11305
11469
|
var KEEP = 8 * 1024;
|
|
11306
11470
|
function fit(body) {
|
|
11307
11471
|
const all = bytes(body);
|
|
11308
11472
|
if (all <= MAX_MESSAGE) {
|
|
11309
11473
|
const kept = mask(body);
|
|
11310
|
-
return { body: kept, truncated: false,
|
|
11474
|
+
return { body: kept, truncated: false, redacted: kept !== body, originalBytes: all };
|
|
11311
11475
|
}
|
|
11312
|
-
const
|
|
11313
|
-
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);
|
|
11314
11480
|
const cut = all - bytes(a) - bytes(z);
|
|
11315
11481
|
return {
|
|
11316
11482
|
body: `${a}
|
|
@@ -11319,13 +11485,14 @@ function fit(body) {
|
|
|
11319
11485
|
|
|
11320
11486
|
${z}`,
|
|
11321
11487
|
truncated: true,
|
|
11488
|
+
redacted: mask(start) !== start || mask(end) !== end,
|
|
11322
11489
|
originalBytes: all
|
|
11323
11490
|
};
|
|
11324
11491
|
}
|
|
11325
11492
|
function prune(held) {
|
|
11326
11493
|
let files;
|
|
11327
11494
|
try {
|
|
11328
|
-
files =
|
|
11495
|
+
files = fs4.readdirSync(held).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
|
|
11329
11496
|
} catch {
|
|
11330
11497
|
return;
|
|
11331
11498
|
}
|
|
@@ -11333,21 +11500,21 @@ function prune(held) {
|
|
|
11333
11500
|
const stale = files.filter((f) => Number(f.split("-")[0]) < cutoff);
|
|
11334
11501
|
const over = files.slice(0, Math.max(0, files.length - HOLD_MAX));
|
|
11335
11502
|
for (const f of new Set([...stale, ...over]))
|
|
11336
|
-
|
|
11503
|
+
fs4.rmSync(path5.join(held, f), { force: true });
|
|
11337
11504
|
}
|
|
11338
11505
|
function spool(record) {
|
|
11339
11506
|
const dir = spoolDir();
|
|
11340
|
-
|
|
11507
|
+
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
11341
11508
|
const name = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}.json`;
|
|
11342
|
-
const tmp =
|
|
11343
|
-
|
|
11344
|
-
|
|
11509
|
+
const tmp = path5.join(dir, `.${name}`);
|
|
11510
|
+
fs4.writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
|
|
11511
|
+
fs4.renameSync(tmp, path5.join(dir, name));
|
|
11345
11512
|
}
|
|
11346
11513
|
var branchOf = (root) => {
|
|
11347
11514
|
try {
|
|
11348
|
-
const dotgit =
|
|
11349
|
-
const gitdir =
|
|
11350
|
-
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();
|
|
11351
11518
|
return h.startsWith("ref: refs/heads/") ? h.slice("ref: refs/heads/".length) : null;
|
|
11352
11519
|
} catch {
|
|
11353
11520
|
return null;
|
|
@@ -11368,63 +11535,30 @@ var INJECTED = [
|
|
|
11368
11535
|
/^(?:Another Claude|A peer) session sent a message(?: while you were working)?:/
|
|
11369
11536
|
];
|
|
11370
11537
|
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
11538
|
function answersOf(input) {
|
|
11411
11539
|
const response = input.tool_response;
|
|
11412
11540
|
const answers = response?.answers;
|
|
11413
11541
|
if (!answers || typeof answers !== "object")
|
|
11414
11542
|
return null;
|
|
11415
|
-
const
|
|
11416
|
-
|
|
11417
|
-
|
|
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() ? `
|
|
11418
11553
|
Notes: ${notes.trim()}` : "";
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
});
|
|
11422
|
-
return lines.length ? lines.join(`
|
|
11554
|
+
return `A${i + 1}: ${Array.isArray(a) ? a.join(" / ") : String(a)}${memo}`;
|
|
11555
|
+
}).join(`
|
|
11423
11556
|
|
|
11424
|
-
`)
|
|
11557
|
+
`)
|
|
11558
|
+
};
|
|
11425
11559
|
}
|
|
11426
11560
|
function captureNotice(file = dbFile()) {
|
|
11427
|
-
if (!
|
|
11561
|
+
if (!fs4.existsSync(file))
|
|
11428
11562
|
return panel("sphica: no database, so conversations are not recorded", [file], "Create it with sphica init");
|
|
11429
11563
|
const s = readState();
|
|
11430
11564
|
if (s.stuck)
|
|
@@ -11433,6 +11567,34 @@ function captureNotice(file = dbFile()) {
|
|
|
11433
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");
|
|
11434
11568
|
return null;
|
|
11435
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
|
+
}
|
|
11436
11598
|
function onHook(host, input) {
|
|
11437
11599
|
const event = input.hook_event_name;
|
|
11438
11600
|
const owner = () => isOwnerTurn(input, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined);
|
|
@@ -11441,9 +11603,14 @@ function onHook(host, input) {
|
|
|
11441
11603
|
return { flush: false };
|
|
11442
11604
|
const file = process.env.CLAUDE_ENV_FILE;
|
|
11443
11605
|
if (file && input.session_id && /^[A-Za-z0-9_-]+$/.test(input.session_id)) {
|
|
11444
|
-
|
|
11606
|
+
fs4.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
|
|
11445
11607
|
`);
|
|
11446
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();
|
|
11447
11614
|
return { flush: false, notice: captureNotice() };
|
|
11448
11615
|
}
|
|
11449
11616
|
if (!owner())
|
|
@@ -11458,7 +11625,7 @@ function onHook(host, input) {
|
|
|
11458
11625
|
return { flush: false };
|
|
11459
11626
|
const at = new Date().toISOString();
|
|
11460
11627
|
const base = {
|
|
11461
|
-
v:
|
|
11628
|
+
v: 2,
|
|
11462
11629
|
host,
|
|
11463
11630
|
session: String(input.session_id),
|
|
11464
11631
|
project: place.key,
|
|
@@ -11466,23 +11633,37 @@ function onHook(host, input) {
|
|
|
11466
11633
|
turn,
|
|
11467
11634
|
at
|
|
11468
11635
|
};
|
|
11469
|
-
const say = (key, speaker, raw) => {
|
|
11636
|
+
const say = (key, speaker, raw, when = at) => {
|
|
11470
11637
|
const kept = fit(clean(raw).trim());
|
|
11471
11638
|
if (!kept.body.trim())
|
|
11472
11639
|
return;
|
|
11473
11640
|
const id = `${key}:${digest(kept.body)}`;
|
|
11474
|
-
spool({ ...base, kind: "message", id, speaker, ...kept });
|
|
11475
|
-
if (speaker === "self")
|
|
11476
|
-
remember(base.session, id);
|
|
11641
|
+
spool({ ...base, at: when, kind: "message", id, speaker, ...kept });
|
|
11477
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
|
+
}
|
|
11478
11651
|
if (event === "UserPromptSubmit" && input.prompt) {
|
|
11479
11652
|
const prompt = input.prompt.trimStart();
|
|
11480
11653
|
if (!INJECTED.some((r) => r.test(prompt)))
|
|
11481
|
-
say(`${turn}:
|
|
11654
|
+
say(`${turn}:owner`, "owner", prompt);
|
|
11482
11655
|
}
|
|
11483
11656
|
if (event === "Stop") {
|
|
11484
11657
|
if (input.last_assistant_message)
|
|
11485
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
|
+
}
|
|
11486
11667
|
return { flush: true };
|
|
11487
11668
|
}
|
|
11488
11669
|
if (event === "PostToolUse") {
|
|
@@ -11490,31 +11671,31 @@ function onHook(host, input) {
|
|
|
11490
11671
|
const ti = input.tool_input ?? {};
|
|
11491
11672
|
if (tool === "AskUserQuestion") {
|
|
11492
11673
|
const said = answersOf(input);
|
|
11493
|
-
if (said)
|
|
11494
|
-
|
|
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
|
+
}
|
|
11495
11679
|
return { flush: false };
|
|
11496
11680
|
}
|
|
11497
11681
|
if (tool === "Read")
|
|
11498
11682
|
return { flush: false };
|
|
11499
|
-
const message = lastSaid(base.session);
|
|
11500
|
-
if (!message)
|
|
11501
|
-
return { flush: false };
|
|
11502
11683
|
const cwd = input.cwd ?? place.root;
|
|
11503
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) ?? []);
|
|
11504
11685
|
for (const p of files)
|
|
11505
|
-
spool({ ...base, kind: "
|
|
11686
|
+
spool({ ...base, kind: "edit", event: input.tool_use_id ?? null, path: p, via: "tool" });
|
|
11506
11687
|
}
|
|
11507
11688
|
return { flush: false };
|
|
11508
11689
|
}
|
|
11509
11690
|
function writeState(s) {
|
|
11510
11691
|
try {
|
|
11511
|
-
|
|
11692
|
+
fs4.writeFileSync(stateFile(), JSON.stringify(s));
|
|
11512
11693
|
} catch {}
|
|
11513
11694
|
}
|
|
11514
11695
|
function readState() {
|
|
11515
11696
|
const count = (dir) => {
|
|
11516
11697
|
try {
|
|
11517
|
-
return
|
|
11698
|
+
return fs4.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).length;
|
|
11518
11699
|
} catch {
|
|
11519
11700
|
return 0;
|
|
11520
11701
|
}
|
|
@@ -11526,7 +11707,7 @@ function readState() {
|
|
|
11526
11707
|
};
|
|
11527
11708
|
let raw = {};
|
|
11528
11709
|
try {
|
|
11529
|
-
const parsed = JSON.parse(
|
|
11710
|
+
const parsed = JSON.parse(fs4.readFileSync(stateFile(), "utf8"));
|
|
11530
11711
|
if (parsed && typeof parsed === "object")
|
|
11531
11712
|
raw = parsed;
|
|
11532
11713
|
} catch {}
|
|
@@ -11540,25 +11721,25 @@ function readState() {
|
|
|
11540
11721
|
};
|
|
11541
11722
|
}
|
|
11542
11723
|
function lock() {
|
|
11543
|
-
const file =
|
|
11544
|
-
|
|
11724
|
+
const file = path5.join(spoolDir(), ".lock");
|
|
11725
|
+
fs4.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
|
|
11545
11726
|
for (let attempt = 0;attempt < 2; attempt++) {
|
|
11546
11727
|
try {
|
|
11547
|
-
|
|
11728
|
+
fs4.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
|
|
11548
11729
|
return () => {
|
|
11549
11730
|
try {
|
|
11550
|
-
if (
|
|
11551
|
-
|
|
11731
|
+
if (fs4.readFileSync(file, "utf8") === String(process.pid))
|
|
11732
|
+
fs4.rmSync(file, { force: true });
|
|
11552
11733
|
} catch {}
|
|
11553
11734
|
};
|
|
11554
11735
|
} catch (e) {
|
|
11555
11736
|
if (e.code !== "EEXIST")
|
|
11556
11737
|
throw e;
|
|
11557
11738
|
}
|
|
11558
|
-
const st =
|
|
11739
|
+
const st = fs4.statSync(file, { throwIfNoEntry: false });
|
|
11559
11740
|
if (!st)
|
|
11560
11741
|
continue;
|
|
11561
|
-
const holder = Number(
|
|
11742
|
+
const holder = Number(fs4.readFileSync(file, "utf8") || 0);
|
|
11562
11743
|
const fresh = Date.now() - st.mtimeMs < 5 * 60000;
|
|
11563
11744
|
const alive = (() => {
|
|
11564
11745
|
if (holder <= 0)
|
|
@@ -11571,7 +11752,7 @@ function lock() {
|
|
|
11571
11752
|
})();
|
|
11572
11753
|
if (alive && fresh)
|
|
11573
11754
|
return null;
|
|
11574
|
-
|
|
11755
|
+
fs4.rmSync(file, { force: true });
|
|
11575
11756
|
}
|
|
11576
11757
|
return null;
|
|
11577
11758
|
}
|
|
@@ -11580,27 +11761,21 @@ var ROWS = 1000;
|
|
|
11580
11761
|
var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
|
|
11581
11762
|
async function write(db, batch, projects) {
|
|
11582
11763
|
return inTransaction(db, async (trx) => {
|
|
11583
|
-
const
|
|
11764
|
+
const sessions = new Map;
|
|
11584
11765
|
for (const r of batch) {
|
|
11585
11766
|
const p = projects.get(r.project);
|
|
11586
11767
|
if (!p)
|
|
11587
11768
|
continue;
|
|
11588
|
-
const id =
|
|
11589
|
-
const prev =
|
|
11769
|
+
const id = sessionId(p.id, r.host, r.session);
|
|
11770
|
+
const prev = sessions.get(id);
|
|
11590
11771
|
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
|
-
});
|
|
11772
|
+
sessions.set(id, { project: p.id, host: r.host, session: r.session, branch: r.branch, at: r.at });
|
|
11598
11773
|
}
|
|
11599
|
-
for (const part of chunks([...
|
|
11600
|
-
await trx.insertInto("
|
|
11774
|
+
for (const part of chunks([...sessions]))
|
|
11775
|
+
await trx.insertInto("capture_session").values(part.map(([id, v]) => ({
|
|
11601
11776
|
id,
|
|
11602
11777
|
project_id: v.project,
|
|
11603
|
-
|
|
11778
|
+
host: v.host,
|
|
11604
11779
|
external_id: v.session,
|
|
11605
11780
|
branch: v.branch,
|
|
11606
11781
|
started_at: iso(v.at)
|
|
@@ -11609,44 +11784,49 @@ async function write(db, batch, projects) {
|
|
|
11609
11784
|
const p = m.kind === "message" ? projects.get(m.project) : undefined;
|
|
11610
11785
|
if (m.kind !== "message" || !p)
|
|
11611
11786
|
return [];
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
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))
|
|
11619
11799
|
await trx.insertInto("capture_message").values(part.map((x) => ({
|
|
11620
|
-
id: x.id,
|
|
11621
|
-
conversation_id: x.conversation,
|
|
11622
11800
|
external_id: x.m.id,
|
|
11801
|
+
session_id: x.session,
|
|
11623
11802
|
turn_id: x.m.turn,
|
|
11624
|
-
|
|
11625
|
-
|
|
11803
|
+
speaker: x.m.speaker,
|
|
11804
|
+
created_at: iso(x.m.at),
|
|
11805
|
+
captured_at: now,
|
|
11806
|
+
text: x.m.body,
|
|
11626
11807
|
truncated: x.m.truncated ? 1 : 0,
|
|
11808
|
+
redacted: x.m.redacted ? 1 : 0,
|
|
11627
11809
|
original_bytes: x.m.originalBytes,
|
|
11628
|
-
|
|
11629
|
-
content_hash: sha256(x.m.body),
|
|
11630
|
-
indexed: indexesMessage(x.m.speaker) ? 1 : 0
|
|
11810
|
+
content_hash: sha256(x.m.body)
|
|
11631
11811
|
}))).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)
|
|
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)
|
|
11639
11816
|
return [];
|
|
11640
11817
|
return [
|
|
11641
11818
|
{
|
|
11642
|
-
|
|
11819
|
+
session_id: sessionId(p.id, r.host, r.session),
|
|
11820
|
+
turn_id: r.turn,
|
|
11821
|
+
tool_event_id: r.event,
|
|
11643
11822
|
path: r.path,
|
|
11644
|
-
|
|
11823
|
+
via: r.via,
|
|
11824
|
+
observed_at: iso(r.at)
|
|
11645
11825
|
}
|
|
11646
11826
|
];
|
|
11647
11827
|
});
|
|
11648
|
-
for (const part of chunks(
|
|
11649
|
-
await trx.insertInto("
|
|
11828
|
+
for (const part of chunks(edits))
|
|
11829
|
+
await trx.insertInto("capture_edit").values(part).execute();
|
|
11650
11830
|
return after - before;
|
|
11651
11831
|
});
|
|
11652
11832
|
}
|
|
@@ -11662,7 +11842,7 @@ async function flush(file = dbFile()) {
|
|
|
11662
11842
|
const held = unregisteredDir();
|
|
11663
11843
|
const list = (from) => {
|
|
11664
11844
|
try {
|
|
11665
|
-
return
|
|
11845
|
+
return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((name) => ({ name, from }));
|
|
11666
11846
|
} catch {
|
|
11667
11847
|
return [];
|
|
11668
11848
|
}
|
|
@@ -11672,15 +11852,18 @@ async function flush(file = dbFile()) {
|
|
|
11672
11852
|
return { sent: 0, deferred: 0, rejected: 0 };
|
|
11673
11853
|
const records = [];
|
|
11674
11854
|
for (const { name, from } of names) {
|
|
11855
|
+
let r;
|
|
11675
11856
|
try {
|
|
11676
|
-
|
|
11677
|
-
name,
|
|
11678
|
-
from,
|
|
11679
|
-
r: JSON.parse(fs3.readFileSync(path3.join(from, name), "utf8"))
|
|
11680
|
-
});
|
|
11857
|
+
r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
|
|
11681
11858
|
} catch {
|
|
11682
|
-
|
|
11859
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11860
|
+
fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
|
|
11861
|
+
continue;
|
|
11683
11862
|
}
|
|
11863
|
+
if (r)
|
|
11864
|
+
records.push({ name, from, r });
|
|
11865
|
+
else
|
|
11866
|
+
fs4.rmSync(path5.join(from, name), { force: true });
|
|
11684
11867
|
}
|
|
11685
11868
|
const db = openWriter("capture", file);
|
|
11686
11869
|
client = db;
|
|
@@ -11694,8 +11877,7 @@ async function flush(file = dbFile()) {
|
|
|
11694
11877
|
} catch (e) {
|
|
11695
11878
|
if (!rejected(e))
|
|
11696
11879
|
throw e;
|
|
11697
|
-
const
|
|
11698
|
-
for (const x of ordered) {
|
|
11880
|
+
for (const x of known) {
|
|
11699
11881
|
try {
|
|
11700
11882
|
sent += await write(db, [x.r], projects);
|
|
11701
11883
|
} catch (e2) {
|
|
@@ -11705,15 +11887,11 @@ async function flush(file = dbFile()) {
|
|
|
11705
11887
|
}
|
|
11706
11888
|
}
|
|
11707
11889
|
}
|
|
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
11890
|
if (bad.length) {
|
|
11713
|
-
|
|
11891
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11714
11892
|
for (const x of bad) {
|
|
11715
11893
|
try {
|
|
11716
|
-
|
|
11894
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
|
|
11717
11895
|
} catch (e) {
|
|
11718
11896
|
if (e.code !== "ENOENT")
|
|
11719
11897
|
throw e;
|
|
@@ -11721,12 +11899,12 @@ async function flush(file = dbFile()) {
|
|
|
11721
11899
|
}
|
|
11722
11900
|
}
|
|
11723
11901
|
if (strayed.length) {
|
|
11724
|
-
|
|
11902
|
+
fs4.mkdirSync(held, { recursive: true, mode: 448 });
|
|
11725
11903
|
for (const x of strayed) {
|
|
11726
11904
|
if (x.from === held)
|
|
11727
11905
|
continue;
|
|
11728
11906
|
try {
|
|
11729
|
-
|
|
11907
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
|
|
11730
11908
|
} catch (e) {
|
|
11731
11909
|
if (e.code !== "ENOENT")
|
|
11732
11910
|
throw e;
|
|
@@ -11736,7 +11914,7 @@ async function flush(file = dbFile()) {
|
|
|
11736
11914
|
const moved = new Set([...bad, ...strayed].map((x) => x.name));
|
|
11737
11915
|
for (const x of records)
|
|
11738
11916
|
if (!moved.has(x.name))
|
|
11739
|
-
|
|
11917
|
+
fs4.rmSync(path5.join(x.from, x.name), { force: true });
|
|
11740
11918
|
prune(held);
|
|
11741
11919
|
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
|
|
11742
11920
|
return { sent, deferred: strayed.length, rejected: bad.length };
|
|
@@ -11776,11 +11954,12 @@ if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
|
|
|
11776
11954
|
export {
|
|
11777
11955
|
MAX_MESSAGE,
|
|
11778
11956
|
answersOf,
|
|
11957
|
+
branchOf,
|
|
11779
11958
|
captureNotice,
|
|
11959
|
+
current,
|
|
11780
11960
|
fit,
|
|
11781
11961
|
flush,
|
|
11782
11962
|
isOwnerTurn,
|
|
11783
|
-
lock,
|
|
11784
11963
|
onHook,
|
|
11785
11964
|
readInput,
|
|
11786
11965
|
readState,
|