sphica 0.6.15 → 0.6.17
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/migrations/0006.sql +20 -0
- package/db/schema.sql +19 -4
- package/dist/capture.js +17 -9
- package/dist/cli.js +17 -9
- package/dist/deliver.js +114 -40
- package/dist/mcp-record.js +17 -9
- package/dist/mcp.js +3 -3
- package/hooks/codex.json +12 -0
- package/hooks/hooks.json +11 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "sphica",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.17",
|
|
5
5
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "iroha924",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Revision 5 → 6 of generation 2: the subagent a delivery ran in (existing rows keep null, the main conversation), and a capture view
|
|
2
|
+
-- that writes it while capture_delivery keeps its columns. `sphica init` runs this in one transaction with foreign keys off, then checks
|
|
3
|
+
-- foreign_key_check. Every statement matches db/schema.sql at revision 6; server/test/migrate.test.ts compares the two.
|
|
4
|
+
|
|
5
|
+
alter table delivery add column agent_id text check (length(agent_id) between 1 and 200);
|
|
6
|
+
|
|
7
|
+
create view capture_delivery_scoped as
|
|
8
|
+
select session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at, null as units from delivery;
|
|
9
|
+
create trigger capture_delivery_scoped_insert instead of insert on capture_delivery_scoped begin
|
|
10
|
+
select raise(abort, 'the unit and the delivered session belong to different projects')
|
|
11
|
+
where exists (select 1 from json_each(coalesce(new.units, '[]')) j
|
|
12
|
+
where (select project_id from unit where id = j.value) is not (select project_id from session where id = new.session_id));
|
|
13
|
+
insert into delivery (session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at)
|
|
14
|
+
values (new.session_id, new.agent_id, new.event, new.outcome, new.reason, new.path, coalesce(new.eligible, 0),
|
|
15
|
+
coalesce(new.omitted, 0), coalesce(new.chars, 0), new.at);
|
|
16
|
+
insert into delivery_unit (delivery_id, unit_id)
|
|
17
|
+
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
18
|
+
end;
|
|
19
|
+
|
|
20
|
+
pragma user_version = 6;
|
package/db/schema.sql
CHANGED
|
@@ -934,8 +934,9 @@ create table work (
|
|
|
934
934
|
create index work_open on work (project_id, updated_at desc) where status in ('active', 'blocked', 'paused');
|
|
935
935
|
create index work_run on work (run_id) where run_id is not null;
|
|
936
936
|
|
|
937
|
-
-- What a delivery hook emitted or suppressed, and how many eligible units it left out
|
|
938
|
-
--
|
|
937
|
+
-- What a delivery hook emitted or suppressed, and how many eligible units it left out (no source text). chars leaves out the omission note,
|
|
938
|
+
-- since the read budget adds it up. A session_start's reason is the start source or 'subagent'; agent_id is the subagent (null: main),
|
|
939
|
+
-- written the way SQLite stores a column ALTER TABLE added, so a migrated database has the same definition.
|
|
939
940
|
create table delivery (
|
|
940
941
|
id integer primary key autoincrement not null,
|
|
941
942
|
session_id text references session (id) on delete cascade,
|
|
@@ -947,7 +948,7 @@ create table delivery (
|
|
|
947
948
|
omitted integer not null default 0 check (omitted >= 0 and omitted <= eligible),
|
|
948
949
|
chars integer not null default 0 check (chars >= 0),
|
|
949
950
|
at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', at) is at)
|
|
950
|
-
) strict;
|
|
951
|
+
, agent_id text check (length(agent_id) between 1 and 200)) strict;
|
|
951
952
|
create index delivery_session on delivery (session_id, at);
|
|
952
953
|
create table delivery_unit (
|
|
953
954
|
delivery_id integer not null references delivery (id) on delete cascade,
|
|
@@ -1024,5 +1025,19 @@ create trigger capture_delivery_insert instead of insert on capture_delivery beg
|
|
|
1024
1025
|
insert into delivery_unit (delivery_id, unit_id)
|
|
1025
1026
|
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
1026
1027
|
end;
|
|
1028
|
+
-- The same as capture_delivery with the subagent the delivery ran in. capture_delivery keeps its columns, since an older plugin's
|
|
1029
|
+
-- capture writes through it into a database of this revision
|
|
1030
|
+
create view capture_delivery_scoped as
|
|
1031
|
+
select session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at, null as units from delivery;
|
|
1032
|
+
create trigger capture_delivery_scoped_insert instead of insert on capture_delivery_scoped begin
|
|
1033
|
+
select raise(abort, 'the unit and the delivered session belong to different projects')
|
|
1034
|
+
where exists (select 1 from json_each(coalesce(new.units, '[]')) j
|
|
1035
|
+
where (select project_id from unit where id = j.value) is not (select project_id from session where id = new.session_id));
|
|
1036
|
+
insert into delivery (session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at)
|
|
1037
|
+
values (new.session_id, new.agent_id, new.event, new.outcome, new.reason, new.path, coalesce(new.eligible, 0),
|
|
1038
|
+
coalesce(new.omitted, 0), coalesce(new.chars, 0), new.at);
|
|
1039
|
+
insert into delivery_unit (delivery_id, unit_id)
|
|
1040
|
+
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
1041
|
+
end;
|
|
1027
1042
|
|
|
1028
|
-
pragma user_version =
|
|
1043
|
+
pragma user_version = 6;
|
package/dist/capture.js
CHANGED
|
@@ -11109,7 +11109,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
11109
11109
|
|
|
11110
11110
|
// server/src/sqlite.ts
|
|
11111
11111
|
var SCHEMA_GENERATION = 2;
|
|
11112
|
-
var SCHEMA_REVISION =
|
|
11112
|
+
var SCHEMA_REVISION = 6;
|
|
11113
11113
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
11114
11114
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
11115
11115
|
function requireRuntime() {
|
|
@@ -11121,10 +11121,10 @@ function requireFile(file) {
|
|
|
11121
11121
|
if (!fs2.existsSync(file))
|
|
11122
11122
|
throw new Error(`No database at ${file}. Create it with \`sphica init\`.`);
|
|
11123
11123
|
}
|
|
11124
|
-
function prepare(raw, check) {
|
|
11124
|
+
function prepare(raw, check, busyMs = 5000) {
|
|
11125
11125
|
raw.enableDefensive(true);
|
|
11126
11126
|
raw.exec("pragma foreign_keys = on");
|
|
11127
|
-
raw.exec(
|
|
11127
|
+
raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
|
|
11128
11128
|
if (check === "none")
|
|
11129
11129
|
return;
|
|
11130
11130
|
checkGeneration(raw);
|
|
@@ -11245,16 +11245,24 @@ var DDL = () => ddl ??= new Set([
|
|
|
11245
11245
|
return code;
|
|
11246
11246
|
}));
|
|
11247
11247
|
var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
|
|
11248
|
-
var CAPTURE_VIEWS = new Set([
|
|
11248
|
+
var CAPTURE_VIEWS = new Set([
|
|
11249
|
+
"capture_session",
|
|
11250
|
+
"capture_message",
|
|
11251
|
+
"capture_edit",
|
|
11252
|
+
"capture_delivery",
|
|
11253
|
+
"capture_delivery_scoped"
|
|
11254
|
+
]);
|
|
11249
11255
|
var TRIGGER_WRITES = {
|
|
11250
11256
|
capture_session_insert: new Set(["session"]),
|
|
11251
11257
|
capture_message_insert: new Set(["source"]),
|
|
11252
11258
|
capture_edit_insert: new Set(["edit_observation"]),
|
|
11253
11259
|
capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
|
|
11260
|
+
capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
|
|
11254
11261
|
source_fts_ai: new Set(["source_fts"])
|
|
11255
11262
|
};
|
|
11256
11263
|
var TRIGGER_FUNCTIONS = {
|
|
11257
|
-
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
11264
|
+
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
|
|
11265
|
+
capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
11258
11266
|
};
|
|
11259
11267
|
var CAPTURE_READS = {
|
|
11260
11268
|
project: new Set(["id", "key", "name"]),
|
|
@@ -11399,13 +11407,13 @@ function forgetAuthorizer(action, p1, p2) {
|
|
|
11399
11407
|
return C2.SQLITE_OK;
|
|
11400
11408
|
return C2.SQLITE_DENY;
|
|
11401
11409
|
}
|
|
11402
|
-
function connectWriter(role, file = dbFile(), create = false) {
|
|
11410
|
+
function connectWriter(role, file = dbFile(), create = false, busyMs) {
|
|
11403
11411
|
requireRuntime();
|
|
11404
11412
|
if (!create)
|
|
11405
11413
|
requireFile(file);
|
|
11406
11414
|
const raw = new DatabaseSync2(file);
|
|
11407
11415
|
try {
|
|
11408
|
-
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
|
|
11416
|
+
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation", busyMs);
|
|
11409
11417
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
11410
11418
|
} catch (e) {
|
|
11411
11419
|
raw.close();
|
|
@@ -11433,8 +11441,8 @@ function connectWriter(role, file = dbFile(), create = false) {
|
|
|
11433
11441
|
}
|
|
11434
11442
|
return raw;
|
|
11435
11443
|
}
|
|
11436
|
-
function openWriter(role, file = dbFile()) {
|
|
11437
|
-
return kyselyOn(() => connectWriter(role, file));
|
|
11444
|
+
function openWriter(role, file = dbFile(), busyMs) {
|
|
11445
|
+
return kyselyOn(() => connectWriter(role, file, false, busyMs));
|
|
11438
11446
|
}
|
|
11439
11447
|
|
|
11440
11448
|
// server/src/knowledge.ts
|
package/dist/cli.js
CHANGED
|
@@ -3625,7 +3625,7 @@ ${indent}${caution(note)}` : ""}`);
|
|
|
3625
3625
|
|
|
3626
3626
|
// server/src/sqlite.ts
|
|
3627
3627
|
var SCHEMA_GENERATION = 2;
|
|
3628
|
-
var SCHEMA_REVISION =
|
|
3628
|
+
var SCHEMA_REVISION = 6;
|
|
3629
3629
|
var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os2.homedir(), ".sphica");
|
|
3630
3630
|
var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
|
|
3631
3631
|
function requireRuntime() {
|
|
@@ -3637,10 +3637,10 @@ function requireFile(file) {
|
|
|
3637
3637
|
if (!fs3.existsSync(file))
|
|
3638
3638
|
throw new Error(`No database at ${file}. Create it with \`sphica init\`.`);
|
|
3639
3639
|
}
|
|
3640
|
-
function prepare(raw, check) {
|
|
3640
|
+
function prepare(raw, check, busyMs = 5000) {
|
|
3641
3641
|
raw.enableDefensive(true);
|
|
3642
3642
|
raw.exec("pragma foreign_keys = on");
|
|
3643
|
-
raw.exec(
|
|
3643
|
+
raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
|
|
3644
3644
|
if (check === "none")
|
|
3645
3645
|
return;
|
|
3646
3646
|
checkGeneration(raw);
|
|
@@ -14712,16 +14712,24 @@ var DDL = () => ddl ??= new Set([
|
|
|
14712
14712
|
return code;
|
|
14713
14713
|
}));
|
|
14714
14714
|
var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
|
|
14715
|
-
var CAPTURE_VIEWS = new Set([
|
|
14715
|
+
var CAPTURE_VIEWS = new Set([
|
|
14716
|
+
"capture_session",
|
|
14717
|
+
"capture_message",
|
|
14718
|
+
"capture_edit",
|
|
14719
|
+
"capture_delivery",
|
|
14720
|
+
"capture_delivery_scoped"
|
|
14721
|
+
]);
|
|
14716
14722
|
var TRIGGER_WRITES = {
|
|
14717
14723
|
capture_session_insert: new Set(["session"]),
|
|
14718
14724
|
capture_message_insert: new Set(["source"]),
|
|
14719
14725
|
capture_edit_insert: new Set(["edit_observation"]),
|
|
14720
14726
|
capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
|
|
14727
|
+
capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
|
|
14721
14728
|
source_fts_ai: new Set(["source_fts"])
|
|
14722
14729
|
};
|
|
14723
14730
|
var TRIGGER_FUNCTIONS = {
|
|
14724
|
-
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
14731
|
+
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
|
|
14732
|
+
capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
14725
14733
|
};
|
|
14726
14734
|
var CAPTURE_READS = {
|
|
14727
14735
|
project: new Set(["id", "key", "name"]),
|
|
@@ -14866,13 +14874,13 @@ function forgetAuthorizer(action, p1, p2) {
|
|
|
14866
14874
|
return C2.SQLITE_OK;
|
|
14867
14875
|
return C2.SQLITE_DENY;
|
|
14868
14876
|
}
|
|
14869
|
-
function connectWriter(role, file = dbFile(), create = false) {
|
|
14877
|
+
function connectWriter(role, file = dbFile(), create = false, busyMs) {
|
|
14870
14878
|
requireRuntime();
|
|
14871
14879
|
if (!create)
|
|
14872
14880
|
requireFile(file);
|
|
14873
14881
|
const raw = new DatabaseSync2(file);
|
|
14874
14882
|
try {
|
|
14875
|
-
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
|
|
14883
|
+
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation", busyMs);
|
|
14876
14884
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
14877
14885
|
} catch (e) {
|
|
14878
14886
|
raw.close();
|
|
@@ -14900,8 +14908,8 @@ function connectWriter(role, file = dbFile(), create = false) {
|
|
|
14900
14908
|
}
|
|
14901
14909
|
return raw;
|
|
14902
14910
|
}
|
|
14903
|
-
function openWriter(role, file = dbFile()) {
|
|
14904
|
-
return kyselyOn(() => connectWriter(role, file));
|
|
14911
|
+
function openWriter(role, file = dbFile(), busyMs) {
|
|
14912
|
+
return kyselyOn(() => connectWriter(role, file, false, busyMs));
|
|
14905
14913
|
}
|
|
14906
14914
|
|
|
14907
14915
|
// server/src/admin.ts
|
package/dist/deliver.js
CHANGED
|
@@ -11134,7 +11134,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
11134
11134
|
|
|
11135
11135
|
// server/src/sqlite.ts
|
|
11136
11136
|
var SCHEMA_GENERATION = 2;
|
|
11137
|
-
var SCHEMA_REVISION =
|
|
11137
|
+
var SCHEMA_REVISION = 6;
|
|
11138
11138
|
var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os.homedir(), ".sphica");
|
|
11139
11139
|
var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
|
|
11140
11140
|
function requireRuntime() {
|
|
@@ -11146,10 +11146,10 @@ function requireFile(file) {
|
|
|
11146
11146
|
if (!fs2.existsSync(file))
|
|
11147
11147
|
throw new Error(`No database at ${file}. Create it with \`sphica init\`.`);
|
|
11148
11148
|
}
|
|
11149
|
-
function prepare(raw, check) {
|
|
11149
|
+
function prepare(raw, check, busyMs = 5000) {
|
|
11150
11150
|
raw.enableDefensive(true);
|
|
11151
11151
|
raw.exec("pragma foreign_keys = on");
|
|
11152
|
-
raw.exec(
|
|
11152
|
+
raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
|
|
11153
11153
|
if (check === "none")
|
|
11154
11154
|
return;
|
|
11155
11155
|
checkGeneration(raw);
|
|
@@ -11294,16 +11294,24 @@ var DDL = () => ddl ??= new Set([
|
|
|
11294
11294
|
return code;
|
|
11295
11295
|
}));
|
|
11296
11296
|
var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
|
|
11297
|
-
var CAPTURE_VIEWS = new Set([
|
|
11297
|
+
var CAPTURE_VIEWS = new Set([
|
|
11298
|
+
"capture_session",
|
|
11299
|
+
"capture_message",
|
|
11300
|
+
"capture_edit",
|
|
11301
|
+
"capture_delivery",
|
|
11302
|
+
"capture_delivery_scoped"
|
|
11303
|
+
]);
|
|
11298
11304
|
var TRIGGER_WRITES = {
|
|
11299
11305
|
capture_session_insert: new Set(["session"]),
|
|
11300
11306
|
capture_message_insert: new Set(["source"]),
|
|
11301
11307
|
capture_edit_insert: new Set(["edit_observation"]),
|
|
11302
11308
|
capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
|
|
11309
|
+
capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
|
|
11303
11310
|
source_fts_ai: new Set(["source_fts"])
|
|
11304
11311
|
};
|
|
11305
11312
|
var TRIGGER_FUNCTIONS = {
|
|
11306
|
-
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
11313
|
+
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
|
|
11314
|
+
capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
11307
11315
|
};
|
|
11308
11316
|
var CAPTURE_READS = {
|
|
11309
11317
|
project: new Set(["id", "key", "name"]),
|
|
@@ -11448,13 +11456,13 @@ function forgetAuthorizer(action, p1, p2) {
|
|
|
11448
11456
|
return C2.SQLITE_OK;
|
|
11449
11457
|
return C2.SQLITE_DENY;
|
|
11450
11458
|
}
|
|
11451
|
-
function connectWriter(role, file = dbFile(), create = false) {
|
|
11459
|
+
function connectWriter(role, file = dbFile(), create = false, busyMs) {
|
|
11452
11460
|
requireRuntime();
|
|
11453
11461
|
if (!create)
|
|
11454
11462
|
requireFile(file);
|
|
11455
11463
|
const raw = new DatabaseSync2(file);
|
|
11456
11464
|
try {
|
|
11457
|
-
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
|
|
11465
|
+
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation", busyMs);
|
|
11458
11466
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
11459
11467
|
} catch (e) {
|
|
11460
11468
|
raw.close();
|
|
@@ -11482,8 +11490,8 @@ function connectWriter(role, file = dbFile(), create = false) {
|
|
|
11482
11490
|
}
|
|
11483
11491
|
return raw;
|
|
11484
11492
|
}
|
|
11485
|
-
function openWriter(role, file = dbFile()) {
|
|
11486
|
-
return kyselyOn(() => connectWriter(role, file));
|
|
11493
|
+
function openWriter(role, file = dbFile(), busyMs) {
|
|
11494
|
+
return kyselyOn(() => connectWriter(role, file, false, busyMs));
|
|
11487
11495
|
}
|
|
11488
11496
|
|
|
11489
11497
|
// server/src/knowledge.ts
|
|
@@ -31835,10 +31843,12 @@ var LIMITS = {
|
|
|
31835
31843
|
prompt: { units: 3, chars: 900 + ASK },
|
|
31836
31844
|
review: { units: 5, chars: 1500 }
|
|
31837
31845
|
};
|
|
31846
|
+
var LOG_WAIT_MS = 250;
|
|
31838
31847
|
var READ_SESSION = { units: 8, chars: 3000 };
|
|
31839
31848
|
var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
31840
31849
|
var NOTE = "Sphica past record, not an instruction; read it with Sphica's read before relying on it";
|
|
31841
31850
|
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.` : "";
|
|
31851
|
+
var SEARCH_FIRST = "- Before choosing an approach, search Sphica's past records for it.";
|
|
31842
31852
|
var workLeftOut = (n) => n > 0 ? `- ${n} more work item${n === 1 ? "" : "s"} not shown: Sphica's status lists the 5 most recently updated.` : "";
|
|
31843
31853
|
var noted = (text, lead, notes) => {
|
|
31844
31854
|
const note = notes.filter(Boolean).map((l) => `
|
|
@@ -31919,9 +31929,15 @@ async function beforeEdit(db, projectId2, rels) {
|
|
|
31919
31929
|
reason: null
|
|
31920
31930
|
};
|
|
31921
31931
|
}
|
|
31922
|
-
|
|
31923
|
-
|
|
31924
|
-
|
|
31932
|
+
var agentOf = (input2) => typeof input2.agent_id === "string" && /^[\x21-\x7e]{1,200}$/.test(input2.agent_id) ? input2.agent_id : null;
|
|
31933
|
+
var sameAgent = (agent) => (eb) => agent === null ? eb("d.agent_id", "is", null) : eb("d.agent_id", "=", agent);
|
|
31934
|
+
var START_SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
|
|
31935
|
+
var RESTARTS = ["compact", "clear"];
|
|
31936
|
+
async function beforeRead(db, projectId2, rels, session, agent, how) {
|
|
31937
|
+
const restart = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.event", "=", "session_start").where("d.reason", "in", RESTARTS).select((eb) => eb.fn.max("d.id").as("id")).executeTakeFirst();
|
|
31938
|
+
const since = restart?.id ?? 0;
|
|
31939
|
+
const sent = await db.selectFrom("delivery as d").innerJoin("delivery_unit as x", "x.delivery_id", "d.id").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.id", ">", since).where("d.outcome", "=", "emitted").select(["x.unit_id", "d.event"]).execute();
|
|
31940
|
+
const spent = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.id", ">", since).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();
|
|
31925
31941
|
const seen = new Set(sent.map((r) => r.unit_id));
|
|
31926
31942
|
const readUnits = sent.filter((r) => r.event === "pre_read").length;
|
|
31927
31943
|
const rows = (await anchoredTo(db, projectId2, rels).where("u.kind", "in", ["decision", "constraint"]).execute()).filter((u) => !seen.has(u.id));
|
|
@@ -31973,7 +31989,11 @@ async function namedInCommand(db, projectId2, root, cwd, command) {
|
|
|
31973
31989
|
};
|
|
31974
31990
|
return paths.map((r) => r.path).filter((p) => forms(p).some((t) => new RegExp(`(?:^|[${edge}])${esc2(t)}(?:$|[${edge}:])`).test(command)));
|
|
31975
31991
|
}
|
|
31976
|
-
|
|
31992
|
+
function pathNamed(prompt, root, rel) {
|
|
31993
|
+
const steps = (p) => p.split(/[\\/]/).map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]");
|
|
31994
|
+
return new RegExp(`(?<![A-Za-z0-9_$.\\-/\\\\])(?:(?:\\.[\\\\/])?${steps(rel)}|${steps(path8.join(root, rel))})(?![A-Za-z0-9_$\\-/\\\\]|\\.[A-Za-z0-9_$])`).test(prompt);
|
|
31995
|
+
}
|
|
31996
|
+
async function onPrompt(db, projectId2, root, prompt) {
|
|
31977
31997
|
const text = prompt.normalize("NFKC");
|
|
31978
31998
|
const lower = text.toLowerCase();
|
|
31979
31999
|
const word = (w, s) => new RegExp(`(?<![\\p{L}\\p{N}_$])${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\p{L}\\p{N}_$])`, "u").test(s);
|
|
@@ -31986,7 +32006,7 @@ async function onPrompt(db, projectId2, prompt) {
|
|
|
31986
32006
|
]) : [[], []];
|
|
31987
32007
|
const hits = [];
|
|
31988
32008
|
for (const u of units) {
|
|
31989
|
-
const a = anchors.find((x) => x.unit_id === u.id && (x.symbol && named2(x.symbol) ||
|
|
32009
|
+
const a = anchors.find((x) => x.unit_id === u.id && (x.symbol && named2(x.symbol) || pathNamed(prompt, root, x.path)));
|
|
31990
32010
|
const o = options.find((x) => x.unit_id === u.id && x.text.length >= 3 && word(x.text.normalize("NFKC").toLowerCase(), lower));
|
|
31991
32011
|
if (a)
|
|
31992
32012
|
hits.push({ u, why: ` [names ${a.symbol && named2(a.symbol) ? a.symbol : a.path}]` });
|
|
@@ -32047,7 +32067,12 @@ async function atStart(db, projectId2, branch, place) {
|
|
|
32047
32067
|
const workLeft = Math.max((workTotal ?? 0) - f.kept.filter((i) => i < work.length).length, 0);
|
|
32048
32068
|
const broadLeft = Math.max((broadTotal ?? 0) - shownUnits.length, 0);
|
|
32049
32069
|
return {
|
|
32050
|
-
...noted(f.text, lead, [
|
|
32070
|
+
...noted(f.text, lead, [
|
|
32071
|
+
leftOut(broadLeft),
|
|
32072
|
+
workLeftOut(workLeft),
|
|
32073
|
+
await waiting(db, projectId2, place),
|
|
32074
|
+
place.subagent ? SEARCH_FIRST : ""
|
|
32075
|
+
]),
|
|
32051
32076
|
units: shownUnits,
|
|
32052
32077
|
eligible: (workTotal ?? 0) + (broadTotal ?? 0),
|
|
32053
32078
|
omitted: workLeft + broadLeft,
|
|
@@ -32115,32 +32140,62 @@ function markOnce(kind, key) {
|
|
|
32115
32140
|
function onceUnavailable(session) {
|
|
32116
32141
|
return markOnce("unavailable", session);
|
|
32117
32142
|
}
|
|
32118
|
-
async function
|
|
32119
|
-
const
|
|
32143
|
+
async function write2(trx, e, plan) {
|
|
32144
|
+
const id = sessionId(e.projectId, e.host, e.external);
|
|
32145
|
+
const now = iso(Date.now());
|
|
32146
|
+
await trx.insertInto("capture_session").values({
|
|
32147
|
+
id,
|
|
32148
|
+
project_id: e.projectId,
|
|
32149
|
+
host: e.host,
|
|
32150
|
+
external_id: e.external,
|
|
32151
|
+
branch: e.branch,
|
|
32152
|
+
started_at: now
|
|
32153
|
+
}).execute();
|
|
32154
|
+
await trx.insertInto("capture_delivery_scoped").values({
|
|
32155
|
+
session_id: id,
|
|
32156
|
+
agent_id: e.agent,
|
|
32157
|
+
event: e.event,
|
|
32158
|
+
outcome: plan.text ? "emitted" : "nothing",
|
|
32159
|
+
reason: plan.reason,
|
|
32160
|
+
path: plan.path,
|
|
32161
|
+
eligible: plan.eligible,
|
|
32162
|
+
omitted: plan.omitted,
|
|
32163
|
+
chars: plan.text.length - plan.note.length,
|
|
32164
|
+
at: now,
|
|
32165
|
+
units: JSON.stringify(plan.units)
|
|
32166
|
+
}).execute();
|
|
32167
|
+
}
|
|
32168
|
+
async function log(file2, e, plan) {
|
|
32169
|
+
const cap = openWriter("capture", file2, LOG_WAIT_MS);
|
|
32120
32170
|
try {
|
|
32121
|
-
|
|
32122
|
-
|
|
32123
|
-
await cap.
|
|
32124
|
-
|
|
32125
|
-
|
|
32126
|
-
|
|
32127
|
-
|
|
32128
|
-
|
|
32129
|
-
|
|
32130
|
-
|
|
32131
|
-
|
|
32132
|
-
|
|
32133
|
-
|
|
32134
|
-
|
|
32135
|
-
|
|
32171
|
+
await inTransaction(cap, (trx) => write2(trx, e, plan));
|
|
32172
|
+
} finally {
|
|
32173
|
+
await cap.destroy().catch(() => {});
|
|
32174
|
+
}
|
|
32175
|
+
}
|
|
32176
|
+
async function lockedPlan(file2, e, make, keep) {
|
|
32177
|
+
const cap = openWriter("capture", file2, LOG_WAIT_MS);
|
|
32178
|
+
let held = false;
|
|
32179
|
+
let plan = null;
|
|
32180
|
+
try {
|
|
32181
|
+
await inTransaction(cap, async (trx) => {
|
|
32182
|
+
held = true;
|
|
32183
|
+
plan = await make();
|
|
32184
|
+
if (keep(plan))
|
|
32185
|
+
await write2(trx, e, plan);
|
|
32186
|
+
});
|
|
32187
|
+
} catch (err) {
|
|
32188
|
+
if (held && !plan)
|
|
32189
|
+
throw err;
|
|
32136
32190
|
} finally {
|
|
32137
32191
|
await cap.destroy().catch(() => {});
|
|
32138
32192
|
}
|
|
32193
|
+
return plan ?? make();
|
|
32139
32194
|
}
|
|
32140
32195
|
async function deliver(input2, host = "claude-code", file2 = dbFile()) {
|
|
32141
32196
|
const name = input2.hook_event_name;
|
|
32142
32197
|
const call = reviewCall(input2);
|
|
32143
|
-
const event = call ? "review" : name === "SessionStart" ? "session_start" : name === "UserPromptSubmit" ? "prompt" : name === "PreToolUse" ? input2.tool_name === "Read" || input2.tool_name === "Bash" && !(host === "codex" && shellPatch(input2)) ? "pre_read" : "pre_edit" : null;
|
|
32198
|
+
const event = call ? "review" : name === "SessionStart" || name === "SubagentStart" ? "session_start" : name === "UserPromptSubmit" ? "prompt" : name === "PreToolUse" ? input2.tool_name === "Read" || input2.tool_name === "Bash" && !(host === "codex" && shellPatch(input2)) ? "pre_read" : "pre_edit" : null;
|
|
32144
32199
|
if (!event || !input2.session_id)
|
|
32145
32200
|
return "";
|
|
32146
32201
|
if (event === "prompt" && !isOwnerTurn(input2, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined))
|
|
@@ -32174,19 +32229,38 @@ async function deliver(input2, host = "claude-code", file2 = dbFile()) {
|
|
|
32174
32229
|
return "";
|
|
32175
32230
|
}
|
|
32176
32231
|
if (event === "session_start" && input2.source === "resume") {
|
|
32177
|
-
const said = await db.selectFrom("delivery").select("id").where("session_id", "=", sessionId(pid, host, input2.session_id)).where("event", "=", "session_start").where("outcome", "=", "emitted").executeTakeFirst();
|
|
32232
|
+
const said = await db.selectFrom("delivery").select("id").where("session_id", "=", sessionId(pid, host, input2.session_id)).where("agent_id", "is", null).where("event", "=", "session_start").where((eb) => eb.or([eb("reason", "is", null), eb("reason", "!=", "subagent")])).where("outcome", "=", "emitted").executeTakeFirst();
|
|
32178
32233
|
if (said)
|
|
32179
32234
|
return "";
|
|
32180
32235
|
}
|
|
32181
|
-
const
|
|
32182
|
-
|
|
32183
|
-
|
|
32236
|
+
const reader = db;
|
|
32237
|
+
const session = input2.session_id;
|
|
32238
|
+
const make = async () => event === "pre_edit" ? await beforeEdit(reader, pid, rels) : event === "pre_read" ? await beforeRead(reader, pid, rels, sessionId(pid, host, session), agentOf(input2), shell ? "named" : "reading") : event === "prompt" ? await onPrompt(reader, pid, place.root, input2.prompt ?? "") : call ? await beforeReview(reader, pid, place.root, call) : {
|
|
32239
|
+
...await atStart(reader, pid, branchOf(place.root), {
|
|
32240
|
+
file: file2,
|
|
32241
|
+
key: place.key,
|
|
32242
|
+
host,
|
|
32243
|
+
subagent: name === "SubagentStart",
|
|
32244
|
+
owner: name !== "SubagentStart" && isOwnerTurn(input2, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined)
|
|
32245
|
+
}),
|
|
32246
|
+
reason: name === "SubagentStart" ? "subagent" : input2.source && START_SOURCES.has(input2.source) ? input2.source : null
|
|
32247
|
+
};
|
|
32248
|
+
const entry = {
|
|
32249
|
+
projectId: pid,
|
|
32184
32250
|
host,
|
|
32185
|
-
|
|
32186
|
-
|
|
32251
|
+
external: session,
|
|
32252
|
+
agent: agentOf(input2),
|
|
32253
|
+
event,
|
|
32254
|
+
branch: branchOf(place.root)
|
|
32255
|
+
};
|
|
32256
|
+
const keep = (p) => !(onPath && !p.text);
|
|
32257
|
+
if (event === "pre_read" || event === "session_start")
|
|
32258
|
+
return (await lockedPlan(file2, entry, make, keep)).text;
|
|
32259
|
+
const plan = await make();
|
|
32187
32260
|
if (call && plan.text && toldBefore(`${host}\x00${input2.session_id}`, plan.once ?? plan.text))
|
|
32188
32261
|
return "";
|
|
32189
|
-
|
|
32262
|
+
if (keep(plan))
|
|
32263
|
+
await log(file2, entry, plan).catch(() => {});
|
|
32190
32264
|
return plan.text;
|
|
32191
32265
|
} catch (e) {
|
|
32192
32266
|
if (e instanceof RevisionMismatch)
|
package/dist/mcp-record.js
CHANGED
|
@@ -45869,7 +45869,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
45869
45869
|
|
|
45870
45870
|
// server/src/sqlite.ts
|
|
45871
45871
|
var SCHEMA_GENERATION = 2;
|
|
45872
|
-
var SCHEMA_REVISION =
|
|
45872
|
+
var SCHEMA_REVISION = 6;
|
|
45873
45873
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
45874
45874
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
45875
45875
|
function requireRuntime() {
|
|
@@ -45881,10 +45881,10 @@ function requireFile(file2) {
|
|
|
45881
45881
|
if (!fs2.existsSync(file2))
|
|
45882
45882
|
throw new Error(`No database at ${file2}. Create it with \`sphica init\`.`);
|
|
45883
45883
|
}
|
|
45884
|
-
function prepare(raw, check2) {
|
|
45884
|
+
function prepare(raw, check2, busyMs = 5000) {
|
|
45885
45885
|
raw.enableDefensive(true);
|
|
45886
45886
|
raw.exec("pragma foreign_keys = on");
|
|
45887
|
-
raw.exec(
|
|
45887
|
+
raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
|
|
45888
45888
|
if (check2 === "none")
|
|
45889
45889
|
return;
|
|
45890
45890
|
checkGeneration(raw);
|
|
@@ -46005,16 +46005,24 @@ var DDL = () => ddl ??= new Set([
|
|
|
46005
46005
|
return code;
|
|
46006
46006
|
}));
|
|
46007
46007
|
var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
|
|
46008
|
-
var CAPTURE_VIEWS = new Set([
|
|
46008
|
+
var CAPTURE_VIEWS = new Set([
|
|
46009
|
+
"capture_session",
|
|
46010
|
+
"capture_message",
|
|
46011
|
+
"capture_edit",
|
|
46012
|
+
"capture_delivery",
|
|
46013
|
+
"capture_delivery_scoped"
|
|
46014
|
+
]);
|
|
46009
46015
|
var TRIGGER_WRITES = {
|
|
46010
46016
|
capture_session_insert: new Set(["session"]),
|
|
46011
46017
|
capture_message_insert: new Set(["source"]),
|
|
46012
46018
|
capture_edit_insert: new Set(["edit_observation"]),
|
|
46013
46019
|
capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
|
|
46020
|
+
capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
|
|
46014
46021
|
source_fts_ai: new Set(["source_fts"])
|
|
46015
46022
|
};
|
|
46016
46023
|
var TRIGGER_FUNCTIONS = {
|
|
46017
|
-
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
46024
|
+
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
|
|
46025
|
+
capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
46018
46026
|
};
|
|
46019
46027
|
var CAPTURE_READS = {
|
|
46020
46028
|
project: new Set(["id", "key", "name"]),
|
|
@@ -46159,13 +46167,13 @@ function forgetAuthorizer(action, p1, p2) {
|
|
|
46159
46167
|
return C2.SQLITE_OK;
|
|
46160
46168
|
return C2.SQLITE_DENY;
|
|
46161
46169
|
}
|
|
46162
|
-
function connectWriter(role, file2 = dbFile(), create = false) {
|
|
46170
|
+
function connectWriter(role, file2 = dbFile(), create = false, busyMs) {
|
|
46163
46171
|
requireRuntime();
|
|
46164
46172
|
if (!create)
|
|
46165
46173
|
requireFile(file2);
|
|
46166
46174
|
const raw = new DatabaseSync2(file2);
|
|
46167
46175
|
try {
|
|
46168
|
-
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
|
|
46176
|
+
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation", busyMs);
|
|
46169
46177
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
46170
46178
|
} catch (e) {
|
|
46171
46179
|
raw.close();
|
|
@@ -46193,8 +46201,8 @@ function connectWriter(role, file2 = dbFile(), create = false) {
|
|
|
46193
46201
|
}
|
|
46194
46202
|
return raw;
|
|
46195
46203
|
}
|
|
46196
|
-
function openWriter(role, file2 = dbFile()) {
|
|
46197
|
-
return kyselyOn(() => connectWriter(role, file2));
|
|
46204
|
+
function openWriter(role, file2 = dbFile(), busyMs) {
|
|
46205
|
+
return kyselyOn(() => connectWriter(role, file2, false, busyMs));
|
|
46198
46206
|
}
|
|
46199
46207
|
|
|
46200
46208
|
// server/src/extract.ts
|
package/dist/mcp.js
CHANGED
|
@@ -46030,7 +46030,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
46030
46030
|
|
|
46031
46031
|
// server/src/sqlite.ts
|
|
46032
46032
|
var SCHEMA_GENERATION = 2;
|
|
46033
|
-
var SCHEMA_REVISION =
|
|
46033
|
+
var SCHEMA_REVISION = 6;
|
|
46034
46034
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
46035
46035
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
46036
46036
|
function requireRuntime() {
|
|
@@ -46042,10 +46042,10 @@ function requireFile(file2) {
|
|
|
46042
46042
|
if (!fs2.existsSync(file2))
|
|
46043
46043
|
throw new Error(`No database at ${file2}. Create it with \`sphica init\`.`);
|
|
46044
46044
|
}
|
|
46045
|
-
function prepare(raw, check2) {
|
|
46045
|
+
function prepare(raw, check2, busyMs = 5000) {
|
|
46046
46046
|
raw.enableDefensive(true);
|
|
46047
46047
|
raw.exec("pragma foreign_keys = on");
|
|
46048
|
-
raw.exec(
|
|
46048
|
+
raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
|
|
46049
46049
|
if (check2 === "none")
|
|
46050
46050
|
return;
|
|
46051
46051
|
checkGeneration(raw);
|
package/hooks/codex.json
CHANGED
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
]
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
|
+
"SubagentStart": [
|
|
22
|
+
{
|
|
23
|
+
"hooks": [
|
|
24
|
+
{
|
|
25
|
+
"type": "command",
|
|
26
|
+
"command": "node \"${PLUGIN_ROOT}/dist/deliver.js\" codex",
|
|
27
|
+
"commandWindows": "powershell.exe -NoProfile -NonInteractive -Command node $env:PLUGIN_ROOT/dist/deliver.js codex",
|
|
28
|
+
"timeout": 10
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
21
33
|
"UserPromptSubmit": [
|
|
22
34
|
{
|
|
23
35
|
"hooks": [
|
package/hooks/hooks.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sphica",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.17",
|
|
4
4
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|