sphica 0.6.0 → 0.6.2
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/db/schema.sql +1 -0
- package/dist/capture.js +121 -81
- package/dist/cli.js +119 -81
- package/dist/deliver.js +183 -101
- package/dist/mcp-record.js +123 -84
- package/dist/mcp.js +118 -74
- package/package.json +1 -1
package/dist/deliver.js
CHANGED
|
@@ -11847,6 +11847,7 @@ function lock() {
|
|
|
11847
11847
|
return null;
|
|
11848
11848
|
}
|
|
11849
11849
|
var BATCH = 500;
|
|
11850
|
+
var FLUSH_BUDGET_MS = 30000;
|
|
11850
11851
|
var ROWS = 1000;
|
|
11851
11852
|
var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
|
|
11852
11853
|
async function write(db, batch, projects) {
|
|
@@ -11922,98 +11923,135 @@ async function write(db, batch, projects) {
|
|
|
11922
11923
|
}
|
|
11923
11924
|
var REJECTED = new Set([18, 19, 20, 25]);
|
|
11924
11925
|
var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
|
|
11925
|
-
|
|
11926
|
-
const unlock = lock();
|
|
11927
|
-
if (!unlock)
|
|
11928
|
-
return { sent: 0, deferred: 0, rejected: 0, busy: true };
|
|
11929
|
-
const dir = spoolDir();
|
|
11930
|
-
let client = null;
|
|
11926
|
+
var queued = (from) => {
|
|
11931
11927
|
try {
|
|
11932
|
-
|
|
11933
|
-
|
|
11928
|
+
return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
|
|
11929
|
+
} catch {
|
|
11930
|
+
return [];
|
|
11931
|
+
}
|
|
11932
|
+
};
|
|
11933
|
+
async function sendBatch(db, names) {
|
|
11934
|
+
const held = unregisteredDir();
|
|
11935
|
+
const records = [];
|
|
11936
|
+
for (const { name, from } of names) {
|
|
11937
|
+
let r;
|
|
11938
|
+
try {
|
|
11939
|
+
r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
|
|
11940
|
+
} catch {
|
|
11941
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11942
|
+
fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
|
|
11943
|
+
continue;
|
|
11944
|
+
}
|
|
11945
|
+
if (r)
|
|
11946
|
+
records.push({ name, from, r });
|
|
11947
|
+
else
|
|
11948
|
+
fs4.rmSync(path5.join(from, name), { force: true });
|
|
11949
|
+
}
|
|
11950
|
+
const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
|
|
11951
|
+
const known = records.filter((x) => projects.has(x.r.project));
|
|
11952
|
+
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
11953
|
+
let sent = 0;
|
|
11954
|
+
const bad = [];
|
|
11955
|
+
try {
|
|
11956
|
+
sent = await write(db, known.map((x) => x.r), projects);
|
|
11957
|
+
} catch (e) {
|
|
11958
|
+
if (!rejected(e))
|
|
11959
|
+
throw e;
|
|
11960
|
+
for (const x of known) {
|
|
11934
11961
|
try {
|
|
11935
|
-
|
|
11936
|
-
} catch {
|
|
11937
|
-
|
|
11962
|
+
sent += await write(db, [x.r], projects);
|
|
11963
|
+
} catch (e2) {
|
|
11964
|
+
if (!rejected(e2))
|
|
11965
|
+
throw e2;
|
|
11966
|
+
bad.push(x);
|
|
11938
11967
|
}
|
|
11939
|
-
}
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
const
|
|
11944
|
-
for (const { name, from } of names) {
|
|
11945
|
-
let r;
|
|
11968
|
+
}
|
|
11969
|
+
}
|
|
11970
|
+
if (bad.length) {
|
|
11971
|
+
fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
11972
|
+
for (const x of bad) {
|
|
11946
11973
|
try {
|
|
11947
|
-
|
|
11948
|
-
} catch {
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
continue;
|
|
11952
|
-
}
|
|
11953
|
-
if (r)
|
|
11954
|
-
records.push({ name, from, r });
|
|
11955
|
-
else
|
|
11956
|
-
fs4.rmSync(path5.join(from, name), { force: true });
|
|
11957
|
-
}
|
|
11958
|
-
const db = openWriter("capture", file);
|
|
11959
|
-
client = db;
|
|
11960
|
-
const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
|
|
11961
|
-
const known = records.filter((x) => projects.has(x.r.project));
|
|
11962
|
-
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
11963
|
-
let sent = 0;
|
|
11964
|
-
const bad = [];
|
|
11965
|
-
try {
|
|
11966
|
-
sent = await write(db, known.map((x) => x.r), projects);
|
|
11967
|
-
} catch (e) {
|
|
11968
|
-
if (!rejected(e))
|
|
11969
|
-
throw e;
|
|
11970
|
-
for (const x of known) {
|
|
11971
|
-
try {
|
|
11972
|
-
sent += await write(db, [x.r], projects);
|
|
11973
|
-
} catch (e2) {
|
|
11974
|
-
if (!rejected(e2))
|
|
11975
|
-
throw e2;
|
|
11976
|
-
bad.push(x);
|
|
11977
|
-
}
|
|
11974
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
|
|
11975
|
+
} catch (e) {
|
|
11976
|
+
if (e.code !== "ENOENT")
|
|
11977
|
+
throw e;
|
|
11978
11978
|
}
|
|
11979
11979
|
}
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11980
|
+
}
|
|
11981
|
+
if (strayed.length) {
|
|
11982
|
+
fs4.mkdirSync(held, { recursive: true, mode: 448 });
|
|
11983
|
+
for (const x of strayed) {
|
|
11984
|
+
if (x.from === held)
|
|
11985
|
+
continue;
|
|
11986
|
+
try {
|
|
11987
|
+
fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
|
|
11988
|
+
} catch (e) {
|
|
11989
|
+
if (e.code !== "ENOENT")
|
|
11990
|
+
throw e;
|
|
11989
11991
|
}
|
|
11990
11992
|
}
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
11993
|
+
}
|
|
11994
|
+
const moved = new Set([...bad, ...strayed].map((x) => x.name));
|
|
11995
|
+
for (const x of records)
|
|
11996
|
+
if (!moved.has(x.name))
|
|
11997
|
+
fs4.rmSync(path5.join(x.from, x.name), { force: true });
|
|
11998
|
+
return { sent, rejected: bad.length };
|
|
11999
|
+
}
|
|
12000
|
+
async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
12001
|
+
const deadline = Date.now() + budgetMs;
|
|
12002
|
+
const dir = spoolDir();
|
|
12003
|
+
const held = unregisteredDir();
|
|
12004
|
+
const total = { sent: 0, deferred: 0, rejected: 0 };
|
|
12005
|
+
let batches = 0;
|
|
12006
|
+
let queueBatches = 0;
|
|
12007
|
+
for (let first = true;; first = false) {
|
|
12008
|
+
let unlock = lock();
|
|
12009
|
+
while (!unlock && Date.now() < deadline) {
|
|
12010
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
|
|
12011
|
+
if (Date.now() < deadline)
|
|
12012
|
+
unlock = lock();
|
|
12013
|
+
}
|
|
12014
|
+
if (!unlock)
|
|
12015
|
+
return first ? { ...total, busy: true } : total;
|
|
12016
|
+
let client = null;
|
|
12017
|
+
const send = async (names) => {
|
|
12018
|
+
client ??= openWriter("capture", file);
|
|
12019
|
+
const r = await sendBatch(client, names);
|
|
12020
|
+
total.sent += r.sent;
|
|
12021
|
+
total.rejected += r.rejected;
|
|
12022
|
+
batches++;
|
|
12023
|
+
};
|
|
12024
|
+
const late = () => batches > 0 && Date.now() >= deadline;
|
|
12025
|
+
const sentBefore = batches;
|
|
12026
|
+
try {
|
|
12027
|
+
if (first) {
|
|
12028
|
+
prune(held);
|
|
12029
|
+
for (const part of chunks(queued(held), BATCH)) {
|
|
12030
|
+
if (late())
|
|
12031
|
+
break;
|
|
12032
|
+
await send(part);
|
|
12001
12033
|
}
|
|
12002
12034
|
}
|
|
12035
|
+
while (queueBatches === 0 || !late()) {
|
|
12036
|
+
const part = queued(dir).slice(0, BATCH);
|
|
12037
|
+
if (part.length === 0)
|
|
12038
|
+
break;
|
|
12039
|
+
await send(part);
|
|
12040
|
+
queueBatches++;
|
|
12041
|
+
}
|
|
12042
|
+
prune(held);
|
|
12043
|
+
total.deferred = queued(held).length;
|
|
12044
|
+
if (batches > sentBefore)
|
|
12045
|
+
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
|
|
12046
|
+
} catch (e) {
|
|
12047
|
+
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
12048
|
+
throw e;
|
|
12049
|
+
} finally {
|
|
12050
|
+
await client?.destroy().catch(() => {});
|
|
12051
|
+
unlock();
|
|
12003
12052
|
}
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
if (!moved.has(x.name))
|
|
12007
|
-
fs4.rmSync(path5.join(x.from, x.name), { force: true });
|
|
12008
|
-
prune(held);
|
|
12009
|
-
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
|
|
12010
|
-
return { sent, deferred: strayed.length, rejected: bad.length };
|
|
12011
|
-
} catch (e) {
|
|
12012
|
-
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
12013
|
-
throw e;
|
|
12014
|
-
} finally {
|
|
12015
|
-
await client?.destroy().catch(() => {});
|
|
12016
|
-
unlock();
|
|
12053
|
+
if (Date.now() >= deadline || queued(dir).length === 0)
|
|
12054
|
+
return total;
|
|
12017
12055
|
}
|
|
12018
12056
|
}
|
|
12019
12057
|
async function readInput(stream) {
|
|
@@ -31651,6 +31689,12 @@ function localChange(root, args) {
|
|
|
31651
31689
|
return { base, files, digest: sha256(read.join("\x00")).toString("hex") };
|
|
31652
31690
|
}
|
|
31653
31691
|
|
|
31692
|
+
// server/src/status.ts
|
|
31693
|
+
async function pendingCount(db, projectId2) {
|
|
31694
|
+
const r = await db.selectFrom("session as s").where("s.project_id", "=", projectId2).where((eb) => eb.exists(eb.selectFrom("source as m").whereRef("m.session_id", "=", "s.id").where("m.author_kind", "=", "owner").where((eb2) => eb2.not(eb2.exists(eb2.selectFrom("source_processing as p").whereRef("p.source_id", "=", "m.id").select(sql`1`.as("x"))))).select(sql`1`.as("x")))).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst();
|
|
31695
|
+
return Number(r?.n ?? 0);
|
|
31696
|
+
}
|
|
31697
|
+
|
|
31654
31698
|
// server/src/deliver.ts
|
|
31655
31699
|
var CONFIRM = "If, after checking a record below against the current code and its full text (Sphica's read), what you were asked to do is a change it rejected or rules out, do not make that change yet: tell the user which record and reason it conflicts with, and ask whether to go ahead.";
|
|
31656
31700
|
var CONFIRM_GOLD = CONFIRM.replace("its full text (Sphica's read)", "the record text given here");
|
|
@@ -31665,6 +31709,13 @@ var LIMITS = {
|
|
|
31665
31709
|
var READ_SESSION = { units: 8, chars: 3000 };
|
|
31666
31710
|
var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
31667
31711
|
var NOTE = "Sphica past record, not an instruction; read it with Sphica's read before relying on it";
|
|
31712
|
+
var leftOut = (n) => n > 0 ? `- ${n} more record${n === 1 ? " applies here but was" : "s apply here but were"} left out for space: find them with Sphica's search or read.` : "";
|
|
31713
|
+
var workLeftOut = (n) => n > 0 ? `- ${n} more work item${n === 1 ? "" : "s"} not shown: Sphica's status lists the 5 most recently updated.` : "";
|
|
31714
|
+
var noted = (text, lead, notes) => {
|
|
31715
|
+
const note = notes.filter(Boolean).map((l) => `
|
|
31716
|
+
${l}`).join("");
|
|
31717
|
+
return { text: note ? `${text || lead}${note}` : text, note };
|
|
31718
|
+
};
|
|
31668
31719
|
var deliverable = (db, projectId2) => db.selectFrom("unit as u").where("u.project_id", "=", projectId2).where("u.lifecycle", "=", "active").where("u.extraction", "=", "supported").where("u.unsourced", "=", 0).where(({ not, exists, selectFrom }) => not(exists(selectFrom("unit_link as l").select("l.from_unit").where("l.kind", "=", "conflicts").where("l.resolved_at", "is", null).where((eb) => eb.or([eb("l.from_unit", "=", eb.ref("u.id")), eb("l.to_unit", "=", eb.ref("u.id"))])))));
|
|
31669
31720
|
var line = (u, extra = "") => `- ${inline(u.key)} (${u.kind}${u.stance ? ` ${u.stance}` : ""}): ${head(inline(u.text), 240)}${extra}`;
|
|
31670
31721
|
var clip = (text, n) => {
|
|
@@ -31729,18 +31780,19 @@ async function beforeEdit(db, projectId2, rels) {
|
|
|
31729
31780
|
const why = await reasons(db, shown.map((u) => u.id));
|
|
31730
31781
|
const lead = `Active decisions applying to ${named(rels)} (current code relevance unverified). ${CONFIRM} ${NOTE}:`;
|
|
31731
31782
|
const f = fit2(shown.map((u) => why.has(u.id) ? [line(u, why.get(u.id)), line(u)] : line(u)), LIMITS.pre_edit.chars, lead);
|
|
31783
|
+
const omitted = rows.length - shown.length + f.omitted;
|
|
31732
31784
|
return {
|
|
31733
|
-
|
|
31785
|
+
...noted(f.text, lead, [leftOut(omitted)]),
|
|
31734
31786
|
units: f.kept.flatMap((i) => shown[i]?.id ?? []),
|
|
31735
31787
|
eligible: rows.length,
|
|
31736
|
-
omitted
|
|
31788
|
+
omitted,
|
|
31737
31789
|
path: head(rels.join(" "), 500),
|
|
31738
31790
|
reason: null
|
|
31739
31791
|
};
|
|
31740
31792
|
}
|
|
31741
31793
|
async function beforeRead(db, projectId2, rels, session, how) {
|
|
31742
31794
|
const sent = await db.selectFrom("delivery as d").innerJoin("delivery_unit as x", "x.delivery_id", "d.id").where("d.session_id", "=", session).where("d.outcome", "=", "emitted").select(["x.unit_id", "d.event"]).execute();
|
|
31743
|
-
const spent = await db.selectFrom("delivery").where("session_id", "=", session).where("event", "=", "pre_read").where("outcome", "=", "emitted").select("chars").execute();
|
|
31795
|
+
const spent = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where("d.event", "=", "pre_read").where("d.outcome", "=", "emitted").where(({ exists, selectFrom }) => exists(selectFrom("delivery_unit as x").select("x.unit_id").whereRef("x.delivery_id", "=", "d.id"))).select("d.chars").execute();
|
|
31744
31796
|
const seen = new Set(sent.map((r) => r.unit_id));
|
|
31745
31797
|
const readUnits = sent.filter((r) => r.event === "pre_read").length;
|
|
31746
31798
|
const rows = (await anchoredTo(db, projectId2, rels).where("u.kind", "in", ["decision", "constraint"]).execute()).filter((u) => !seen.has(u.id));
|
|
@@ -31749,11 +31801,12 @@ async function beforeRead(db, projectId2, rels, session, how) {
|
|
|
31749
31801
|
const why = await reasons(db, shown.map((u) => u.id));
|
|
31750
31802
|
const lead = `Active decisions applying to ${named(rels)}, which ${how === "reading" ? "you are reading" : "this command names"} (current code relevance unverified). ${CONFIRM} ${NOTE}:`;
|
|
31751
31803
|
const f = fit2(shown.map((u) => why.has(u.id) ? [line(u, why.get(u.id)), line(u)] : line(u)), Math.min(LIMITS.pre_read.chars, READ_SESSION.chars + ASK - spent.reduce((n, r) => n + Math.max(r.chars - ASK, 0), 0)), lead);
|
|
31804
|
+
const omitted = rows.length - shown.length + f.omitted;
|
|
31752
31805
|
return {
|
|
31753
|
-
|
|
31806
|
+
...noted(f.text, lead, [leftOut(omitted)]),
|
|
31754
31807
|
units: f.kept.flatMap((i) => shown[i]?.id ?? []),
|
|
31755
31808
|
eligible: rows.length,
|
|
31756
|
-
omitted
|
|
31809
|
+
omitted,
|
|
31757
31810
|
path: head(rels.join(" "), 500),
|
|
31758
31811
|
reason: null
|
|
31759
31812
|
};
|
|
@@ -31822,8 +31875,8 @@ async function onPrompt(db, projectId2, prompt) {
|
|
|
31822
31875
|
used += l.length + 1;
|
|
31823
31876
|
}
|
|
31824
31877
|
return {
|
|
31825
|
-
|
|
31826
|
-
`) : "",
|
|
31878
|
+
...noted(kept.length ? [CONFIRM, ...kept].join(`
|
|
31879
|
+
`) : "", CONFIRM, [leftOut(hits.length - kept.length)]),
|
|
31827
31880
|
units: shown.slice(0, kept.length).map((h) => h.u.id),
|
|
31828
31881
|
eligible: hits.length,
|
|
31829
31882
|
omitted: hits.length - kept.length,
|
|
@@ -31831,9 +31884,27 @@ async function onPrompt(db, projectId2, prompt) {
|
|
|
31831
31884
|
reason: null
|
|
31832
31885
|
};
|
|
31833
31886
|
}
|
|
31834
|
-
async function
|
|
31835
|
-
|
|
31836
|
-
|
|
31887
|
+
async function waiting(db, projectId2, place) {
|
|
31888
|
+
if (!place.owner)
|
|
31889
|
+
return "";
|
|
31890
|
+
const n = await pendingCount(db, projectId2);
|
|
31891
|
+
if (!n)
|
|
31892
|
+
return "";
|
|
31893
|
+
const day = new Date().toLocaleDateString("sv-SE");
|
|
31894
|
+
if (!markOnce("pending", `${path7.resolve(place.file)}\x00${place.key}\x00${day}`))
|
|
31895
|
+
return "";
|
|
31896
|
+
const trace = place.host === "codex" ? "$sphica:trace" : "/sphica:trace";
|
|
31897
|
+
return `- ${n} session${n === 1 ? "" : "s"} waiting to be traced: run ${trace} pending.`;
|
|
31898
|
+
}
|
|
31899
|
+
async function atStart(db, projectId2, branch, place) {
|
|
31900
|
+
const current2 = db.selectFrom("work").where("project_id", "=", projectId2).where("status", "in", ["active", "blocked", "paused"]);
|
|
31901
|
+
const work = await current2.select(["title", "current", "next", "status", "branch"]).orderBy("updated_at", "desc").limit(3).execute();
|
|
31902
|
+
const standing = deliverable(db, projectId2).where("u.kind", "=", "constraint").where(({ not, exists, selectFrom }) => not(exists(selectFrom("unit_anchor as a").select("a.id").whereRef("a.unit_id", "=", "u.id").where("a.role", "=", "applies_to").where("a.retired_at", "is", null))));
|
|
31903
|
+
const broad = await standing.select(["u.id", "u.key", "u.kind", "u.stance", "u.text"]).orderBy("u.id", "desc").limit(3).execute();
|
|
31904
|
+
const [workTotal, broadTotal] = await Promise.all([
|
|
31905
|
+
current2.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst(),
|
|
31906
|
+
standing.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()
|
|
31907
|
+
]).then((r) => r.map((x) => Number(x?.n ?? 0)));
|
|
31837
31908
|
const lines = [
|
|
31838
31909
|
...work.map((w) => {
|
|
31839
31910
|
const next = w.next[0];
|
|
@@ -31841,13 +31912,16 @@ async function atStart(db, projectId2, branch) {
|
|
|
31841
31912
|
}),
|
|
31842
31913
|
...broad.map((u) => line(u))
|
|
31843
31914
|
];
|
|
31844
|
-
const
|
|
31845
|
-
const
|
|
31915
|
+
const lead = `Sphica: this project's current work and standing constraints. ${CONFIRM} ${NOTE}:`;
|
|
31916
|
+
const f = fit2(lines, LIMITS.session_start.chars, lead);
|
|
31917
|
+
const shownUnits = f.kept.flatMap((i) => i >= work.length ? broad[i - work.length]?.id ?? [] : []);
|
|
31918
|
+
const workLeft = Math.max((workTotal ?? 0) - f.kept.filter((i) => i < work.length).length, 0);
|
|
31919
|
+
const broadLeft = Math.max((broadTotal ?? 0) - shownUnits.length, 0);
|
|
31846
31920
|
return {
|
|
31847
|
-
|
|
31921
|
+
...noted(f.text, lead, [leftOut(broadLeft), workLeftOut(workLeft), await waiting(db, projectId2, place)]),
|
|
31848
31922
|
units: shownUnits,
|
|
31849
|
-
eligible:
|
|
31850
|
-
omitted:
|
|
31923
|
+
eligible: (workTotal ?? 0) + (broadTotal ?? 0),
|
|
31924
|
+
omitted: workLeft + broadLeft,
|
|
31851
31925
|
path: null,
|
|
31852
31926
|
reason: null
|
|
31853
31927
|
};
|
|
@@ -31856,6 +31930,7 @@ async function beforeReview(db, projectId2, root, call) {
|
|
|
31856
31930
|
const change = localChange(root, call.args);
|
|
31857
31931
|
const said = (text, why) => ({
|
|
31858
31932
|
text,
|
|
31933
|
+
note: "",
|
|
31859
31934
|
units: [],
|
|
31860
31935
|
eligible: 0,
|
|
31861
31936
|
omitted: 0,
|
|
@@ -31875,12 +31950,14 @@ async function beforeReview(db, projectId2, root, call) {
|
|
|
31875
31950
|
if (!rows.length)
|
|
31876
31951
|
return { ...said(`Sphica ${checked}: no active recorded decision applies.`, null), once };
|
|
31877
31952
|
const shown = rows.slice(0, LIMITS.review.units);
|
|
31878
|
-
const
|
|
31953
|
+
const lead = `Sphica: past decisions that apply to this change (${checked}). ${NOTE}; compare the change against each:`;
|
|
31954
|
+
const f = fit2(shown.map((u) => line(u, ` [${inline(u.because)}]`)), LIMITS.review.chars, lead);
|
|
31955
|
+
const omitted = rows.length - shown.length + f.omitted;
|
|
31879
31956
|
return {
|
|
31880
|
-
|
|
31957
|
+
...noted(f.text, lead, [leftOut(omitted)]),
|
|
31881
31958
|
units: f.kept.flatMap((i) => shown[i]?.id ?? []),
|
|
31882
31959
|
eligible: rows.length,
|
|
31883
|
-
omitted
|
|
31960
|
+
omitted,
|
|
31884
31961
|
path: null,
|
|
31885
31962
|
reason: null,
|
|
31886
31963
|
once
|
|
@@ -31923,7 +32000,7 @@ async function log(file2, projectId2, host, external2, event, plan, outcome, bra
|
|
|
31923
32000
|
path: plan.path,
|
|
31924
32001
|
eligible: plan.eligible,
|
|
31925
32002
|
omitted: plan.omitted,
|
|
31926
|
-
chars: plan.text.length,
|
|
32003
|
+
chars: plan.text.length - plan.note.length,
|
|
31927
32004
|
at: now,
|
|
31928
32005
|
units: JSON.stringify(plan.units)
|
|
31929
32006
|
}).execute();
|
|
@@ -31972,7 +32049,12 @@ async function deliver(input2, host = "claude-code", file2 = dbFile()) {
|
|
|
31972
32049
|
if (said)
|
|
31973
32050
|
return "";
|
|
31974
32051
|
}
|
|
31975
|
-
const plan = event === "pre_edit" ? await beforeEdit(db, pid, rels) : event === "pre_read" ? await beforeRead(db, pid, rels, sessionId(pid, host, input2.session_id), shell ? "named" : "reading") : event === "prompt" ? await onPrompt(db, pid, input2.prompt ?? "") : call ? await beforeReview(db, pid, place.root, call) : await atStart(db, pid, branchOf(place.root)
|
|
32052
|
+
const plan = event === "pre_edit" ? await beforeEdit(db, pid, rels) : event === "pre_read" ? await beforeRead(db, pid, rels, sessionId(pid, host, input2.session_id), shell ? "named" : "reading") : event === "prompt" ? await onPrompt(db, pid, input2.prompt ?? "") : call ? await beforeReview(db, pid, place.root, call) : await atStart(db, pid, branchOf(place.root), {
|
|
32053
|
+
file: file2,
|
|
32054
|
+
key: place.key,
|
|
32055
|
+
host,
|
|
32056
|
+
owner: isOwnerTurn(input2, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined)
|
|
32057
|
+
});
|
|
31976
32058
|
if (call && plan.text && toldBefore(`${host}\x00${input2.session_id}`, plan.once ?? plan.text))
|
|
31977
32059
|
return "";
|
|
31978
32060
|
await log(file2, pid, host, input2.session_id, event, plan, plan.text ? "emitted" : "nothing", branchOf(place.root)).catch(() => {});
|
package/dist/mcp-record.js
CHANGED
|
@@ -46683,6 +46683,8 @@ function lock() {
|
|
|
46683
46683
|
return null;
|
|
46684
46684
|
}
|
|
46685
46685
|
var BATCH = 500;
|
|
46686
|
+
var FLUSH_BUDGET_MS = 30000;
|
|
46687
|
+
var TOOL_FLUSH_BUDGET_MS = 2000;
|
|
46686
46688
|
var ROWS = 1000;
|
|
46687
46689
|
var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
|
|
46688
46690
|
async function write(db, batch, projects) {
|
|
@@ -46758,98 +46760,135 @@ async function write(db, batch, projects) {
|
|
|
46758
46760
|
}
|
|
46759
46761
|
var REJECTED = new Set([18, 19, 20, 25]);
|
|
46760
46762
|
var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
|
|
46761
|
-
|
|
46762
|
-
const unlock = lock();
|
|
46763
|
-
if (!unlock)
|
|
46764
|
-
return { sent: 0, deferred: 0, rejected: 0, busy: true };
|
|
46765
|
-
const dir = spoolDir();
|
|
46766
|
-
let client = null;
|
|
46763
|
+
var queued = (from) => {
|
|
46767
46764
|
try {
|
|
46768
|
-
|
|
46769
|
-
|
|
46765
|
+
return fs5.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
|
|
46766
|
+
} catch {
|
|
46767
|
+
return [];
|
|
46768
|
+
}
|
|
46769
|
+
};
|
|
46770
|
+
async function sendBatch(db, names) {
|
|
46771
|
+
const held = unregisteredDir();
|
|
46772
|
+
const records = [];
|
|
46773
|
+
for (const { name, from } of names) {
|
|
46774
|
+
let r;
|
|
46775
|
+
try {
|
|
46776
|
+
r = current(JSON.parse(fs5.readFileSync(path5.join(from, name), "utf8")));
|
|
46777
|
+
} catch {
|
|
46778
|
+
fs5.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
46779
|
+
fs5.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
|
|
46780
|
+
continue;
|
|
46781
|
+
}
|
|
46782
|
+
if (r)
|
|
46783
|
+
records.push({ name, from, r });
|
|
46784
|
+
else
|
|
46785
|
+
fs5.rmSync(path5.join(from, name), { force: true });
|
|
46786
|
+
}
|
|
46787
|
+
const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
|
|
46788
|
+
const known = records.filter((x) => projects.has(x.r.project));
|
|
46789
|
+
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
46790
|
+
let sent = 0;
|
|
46791
|
+
const bad = [];
|
|
46792
|
+
try {
|
|
46793
|
+
sent = await write(db, known.map((x) => x.r), projects);
|
|
46794
|
+
} catch (e) {
|
|
46795
|
+
if (!rejected(e))
|
|
46796
|
+
throw e;
|
|
46797
|
+
for (const x of known) {
|
|
46770
46798
|
try {
|
|
46771
|
-
|
|
46772
|
-
} catch {
|
|
46773
|
-
|
|
46799
|
+
sent += await write(db, [x.r], projects);
|
|
46800
|
+
} catch (e2) {
|
|
46801
|
+
if (!rejected(e2))
|
|
46802
|
+
throw e2;
|
|
46803
|
+
bad.push(x);
|
|
46774
46804
|
}
|
|
46775
|
-
}
|
|
46776
|
-
|
|
46777
|
-
|
|
46778
|
-
|
|
46779
|
-
const
|
|
46780
|
-
for (const { name, from } of names) {
|
|
46781
|
-
let r;
|
|
46805
|
+
}
|
|
46806
|
+
}
|
|
46807
|
+
if (bad.length) {
|
|
46808
|
+
fs5.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
46809
|
+
for (const x of bad) {
|
|
46782
46810
|
try {
|
|
46783
|
-
|
|
46784
|
-
} catch {
|
|
46785
|
-
|
|
46786
|
-
|
|
46787
|
-
continue;
|
|
46788
|
-
}
|
|
46789
|
-
if (r)
|
|
46790
|
-
records.push({ name, from, r });
|
|
46791
|
-
else
|
|
46792
|
-
fs5.rmSync(path5.join(from, name), { force: true });
|
|
46793
|
-
}
|
|
46794
|
-
const db = openWriter("capture", file2);
|
|
46795
|
-
client = db;
|
|
46796
|
-
const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
|
|
46797
|
-
const known = records.filter((x) => projects.has(x.r.project));
|
|
46798
|
-
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
46799
|
-
let sent = 0;
|
|
46800
|
-
const bad = [];
|
|
46801
|
-
try {
|
|
46802
|
-
sent = await write(db, known.map((x) => x.r), projects);
|
|
46803
|
-
} catch (e) {
|
|
46804
|
-
if (!rejected(e))
|
|
46805
|
-
throw e;
|
|
46806
|
-
for (const x of known) {
|
|
46807
|
-
try {
|
|
46808
|
-
sent += await write(db, [x.r], projects);
|
|
46809
|
-
} catch (e2) {
|
|
46810
|
-
if (!rejected(e2))
|
|
46811
|
-
throw e2;
|
|
46812
|
-
bad.push(x);
|
|
46813
|
-
}
|
|
46811
|
+
fs5.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
|
|
46812
|
+
} catch (e) {
|
|
46813
|
+
if (e.code !== "ENOENT")
|
|
46814
|
+
throw e;
|
|
46814
46815
|
}
|
|
46815
46816
|
}
|
|
46816
|
-
|
|
46817
|
-
|
|
46818
|
-
|
|
46819
|
-
|
|
46820
|
-
|
|
46821
|
-
|
|
46822
|
-
|
|
46823
|
-
|
|
46824
|
-
|
|
46817
|
+
}
|
|
46818
|
+
if (strayed.length) {
|
|
46819
|
+
fs5.mkdirSync(held, { recursive: true, mode: 448 });
|
|
46820
|
+
for (const x of strayed) {
|
|
46821
|
+
if (x.from === held)
|
|
46822
|
+
continue;
|
|
46823
|
+
try {
|
|
46824
|
+
fs5.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
|
|
46825
|
+
} catch (e) {
|
|
46826
|
+
if (e.code !== "ENOENT")
|
|
46827
|
+
throw e;
|
|
46825
46828
|
}
|
|
46826
46829
|
}
|
|
46827
|
-
|
|
46828
|
-
|
|
46829
|
-
|
|
46830
|
-
|
|
46831
|
-
|
|
46832
|
-
|
|
46833
|
-
|
|
46834
|
-
|
|
46835
|
-
|
|
46836
|
-
|
|
46830
|
+
}
|
|
46831
|
+
const moved = new Set([...bad, ...strayed].map((x) => x.name));
|
|
46832
|
+
for (const x of records)
|
|
46833
|
+
if (!moved.has(x.name))
|
|
46834
|
+
fs5.rmSync(path5.join(x.from, x.name), { force: true });
|
|
46835
|
+
return { sent, rejected: bad.length };
|
|
46836
|
+
}
|
|
46837
|
+
async function flush(file2 = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
46838
|
+
const deadline = Date.now() + budgetMs;
|
|
46839
|
+
const dir = spoolDir();
|
|
46840
|
+
const held = unregisteredDir();
|
|
46841
|
+
const total = { sent: 0, deferred: 0, rejected: 0 };
|
|
46842
|
+
let batches = 0;
|
|
46843
|
+
let queueBatches = 0;
|
|
46844
|
+
for (let first = true;; first = false) {
|
|
46845
|
+
let unlock = lock();
|
|
46846
|
+
while (!unlock && Date.now() < deadline) {
|
|
46847
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
|
|
46848
|
+
if (Date.now() < deadline)
|
|
46849
|
+
unlock = lock();
|
|
46850
|
+
}
|
|
46851
|
+
if (!unlock)
|
|
46852
|
+
return first ? { ...total, busy: true } : total;
|
|
46853
|
+
let client = null;
|
|
46854
|
+
const send = async (names) => {
|
|
46855
|
+
client ??= openWriter("capture", file2);
|
|
46856
|
+
const r = await sendBatch(client, names);
|
|
46857
|
+
total.sent += r.sent;
|
|
46858
|
+
total.rejected += r.rejected;
|
|
46859
|
+
batches++;
|
|
46860
|
+
};
|
|
46861
|
+
const late2 = () => batches > 0 && Date.now() >= deadline;
|
|
46862
|
+
const sentBefore = batches;
|
|
46863
|
+
try {
|
|
46864
|
+
if (first) {
|
|
46865
|
+
prune(held);
|
|
46866
|
+
for (const part of chunks(queued(held), BATCH)) {
|
|
46867
|
+
if (late2())
|
|
46868
|
+
break;
|
|
46869
|
+
await send(part);
|
|
46837
46870
|
}
|
|
46838
46871
|
}
|
|
46872
|
+
while (queueBatches === 0 || !late2()) {
|
|
46873
|
+
const part = queued(dir).slice(0, BATCH);
|
|
46874
|
+
if (part.length === 0)
|
|
46875
|
+
break;
|
|
46876
|
+
await send(part);
|
|
46877
|
+
queueBatches++;
|
|
46878
|
+
}
|
|
46879
|
+
prune(held);
|
|
46880
|
+
total.deferred = queued(held).length;
|
|
46881
|
+
if (batches > sentBefore)
|
|
46882
|
+
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
|
|
46883
|
+
} catch (e) {
|
|
46884
|
+
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
46885
|
+
throw e;
|
|
46886
|
+
} finally {
|
|
46887
|
+
await client?.destroy().catch(() => {});
|
|
46888
|
+
unlock();
|
|
46839
46889
|
}
|
|
46840
|
-
|
|
46841
|
-
|
|
46842
|
-
if (!moved.has(x.name))
|
|
46843
|
-
fs5.rmSync(path5.join(x.from, x.name), { force: true });
|
|
46844
|
-
prune(held);
|
|
46845
|
-
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
|
|
46846
|
-
return { sent, deferred: strayed.length, rejected: bad.length };
|
|
46847
|
-
} catch (e) {
|
|
46848
|
-
writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
|
|
46849
|
-
throw e;
|
|
46850
|
-
} finally {
|
|
46851
|
-
await client?.destroy().catch(() => {});
|
|
46852
|
-
unlock();
|
|
46890
|
+
if (Date.now() >= deadline || queued(dir).length === 0)
|
|
46891
|
+
return total;
|
|
46853
46892
|
}
|
|
46854
46893
|
}
|
|
46855
46894
|
async function readInput(stream) {
|
|
@@ -48137,7 +48176,7 @@ async function liveUnits(db, projectId2, limit = 40) {
|
|
|
48137
48176
|
// server/src/extract.ts
|
|
48138
48177
|
var newRunId = () => crypto2.randomBytes(9).toString("base64url");
|
|
48139
48178
|
async function pendingText(db, projectId2) {
|
|
48140
|
-
await flush().catch(() => {});
|
|
48179
|
+
await flush(undefined, TOOL_FLUSH_BUDGET_MS).catch(() => {});
|
|
48141
48180
|
const rows = await pendingSessions(db, projectId2);
|
|
48142
48181
|
if (!rows.length)
|
|
48143
48182
|
return "Every captured session has been traced.";
|
|
@@ -48159,14 +48198,14 @@ async function sessionOf(db, projectId2, given) {
|
|
|
48159
48198
|
return s.id;
|
|
48160
48199
|
}
|
|
48161
48200
|
async function beginTrace(db, projectId2, session) {
|
|
48162
|
-
await flush().catch(() => {});
|
|
48201
|
+
await flush(undefined, TOOL_FLUSH_BUDGET_MS).catch(() => {});
|
|
48163
48202
|
const s = await sessionOf(db, projectId2, session);
|
|
48164
48203
|
const run = newRunId();
|
|
48165
48204
|
await openRun(db, { projectId: projectId2, origin: "trace", target: `session:${s}`, sessionId: s, draftId: run });
|
|
48166
48205
|
return run;
|
|
48167
48206
|
}
|
|
48168
48207
|
async function beginGlean(db, projectId2, session) {
|
|
48169
|
-
await flush().catch(() => {});
|
|
48208
|
+
await flush(undefined, TOOL_FLUSH_BUDGET_MS).catch(() => {});
|
|
48170
48209
|
const s = await sessionOf(db, projectId2, session);
|
|
48171
48210
|
const run = newRunId();
|
|
48172
48211
|
await openRun(db, { projectId: projectId2, origin: "glean", target: "glean", sessionId: s, draftId: run });
|