sphica 0.6.16 → 0.6.18
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/migrations/0007.sql +49 -0
- package/db/schema.sql +35 -5
- package/dist/capture.js +19 -3
- package/dist/cli.js +19 -3
- package/dist/deliver.js +109 -33
- package/dist/mcp-record.js +19 -3
- package/dist/mcp.js +1 -1
- 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.18",
|
|
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;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
-- Revision 6 → 7 of generation 2: capture prunes the deliveries of sessions idle past a cutoff, so delivery_unit loses its cascade from
|
|
2
|
+
-- delivery (capture may not delete through one) and delivery_ad removes the units. Runs in one transaction with foreign keys off; every
|
|
3
|
+
-- statement matches db/schema.sql at revision 7, and server/test/migrate.test.ts compares the two.
|
|
4
|
+
|
|
5
|
+
drop trigger capture_delivery_insert;
|
|
6
|
+
drop trigger capture_delivery_scoped_insert;
|
|
7
|
+
create table delivery_unit_new (
|
|
8
|
+
delivery_id integer not null references delivery (id),
|
|
9
|
+
unit_id integer not null references unit (id) on delete cascade,
|
|
10
|
+
primary key (delivery_id, unit_id)
|
|
11
|
+
) strict;
|
|
12
|
+
insert into delivery_unit_new (delivery_id, unit_id) select delivery_id, unit_id from delivery_unit;
|
|
13
|
+
drop table delivery_unit;
|
|
14
|
+
alter table delivery_unit_new rename to delivery_unit;
|
|
15
|
+
create index delivery_unit_unit on delivery_unit (unit_id);
|
|
16
|
+
create trigger delivery_ad after delete on delivery begin
|
|
17
|
+
delete from delivery_unit where delivery_id = old.id;
|
|
18
|
+
end;
|
|
19
|
+
create index delivery_at on delivery (at);
|
|
20
|
+
create trigger capture_delivery_insert instead of insert on capture_delivery begin
|
|
21
|
+
select raise(abort, 'the unit and the delivered session belong to different projects')
|
|
22
|
+
where exists (select 1 from json_each(coalesce(new.units, '[]')) j
|
|
23
|
+
where (select project_id from unit where id = j.value) is not (select project_id from session where id = new.session_id));
|
|
24
|
+
insert into delivery (session_id, event, outcome, reason, path, eligible, omitted, chars, at)
|
|
25
|
+
values (new.session_id, new.event, new.outcome, new.reason, new.path, coalesce(new.eligible, 0), coalesce(new.omitted, 0),
|
|
26
|
+
coalesce(new.chars, 0), new.at);
|
|
27
|
+
insert into delivery_unit (delivery_id, unit_id)
|
|
28
|
+
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
29
|
+
end;
|
|
30
|
+
create trigger capture_delivery_scoped_insert instead of insert on capture_delivery_scoped begin
|
|
31
|
+
select raise(abort, 'the unit and the delivered session belong to different projects')
|
|
32
|
+
where exists (select 1 from json_each(coalesce(new.units, '[]')) j
|
|
33
|
+
where (select project_id from unit where id = j.value) is not (select project_id from session where id = new.session_id));
|
|
34
|
+
insert into delivery (session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at)
|
|
35
|
+
values (new.session_id, new.agent_id, new.event, new.outcome, new.reason, new.path, coalesce(new.eligible, 0),
|
|
36
|
+
coalesce(new.omitted, 0), coalesce(new.chars, 0), new.at);
|
|
37
|
+
insert into delivery_unit (delivery_id, unit_id)
|
|
38
|
+
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
39
|
+
end;
|
|
40
|
+
create view capture_delivery_prune as select null as cutoff, null as session_id;
|
|
41
|
+
create trigger capture_delivery_prune_insert instead of insert on capture_delivery_prune begin
|
|
42
|
+
delete from delivery where session_id = new.session_id
|
|
43
|
+
and not exists (select 1 from delivery n where n.session_id = new.session_id and n.at >= new.cutoff);
|
|
44
|
+
delete from delivery where id in (select d.id from delivery d where d.at < new.cutoff
|
|
45
|
+
and (d.session_id is null or not exists (select 1 from delivery n where n.session_id = d.session_id and n.at >= new.cutoff))
|
|
46
|
+
order by d.at, d.id limit 200);
|
|
47
|
+
end;
|
|
48
|
+
|
|
49
|
+
pragma user_version = 7;
|
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,14 +948,19 @@ 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);
|
|
953
|
+
create index delivery_at on delivery (at);
|
|
954
|
+
-- A delivery's units go by delivery_ad, not a cascade: capture prunes deliveries, and its authorizer passes a delete only from a named trigger
|
|
952
955
|
create table delivery_unit (
|
|
953
|
-
delivery_id integer not null references delivery (id)
|
|
956
|
+
delivery_id integer not null references delivery (id),
|
|
954
957
|
unit_id integer not null references unit (id) on delete cascade,
|
|
955
958
|
primary key (delivery_id, unit_id)
|
|
956
959
|
) strict;
|
|
957
960
|
create index delivery_unit_unit on delivery_unit (unit_id);
|
|
961
|
+
create trigger delivery_ad after delete on delivery begin
|
|
962
|
+
delete from delivery_unit where delivery_id = old.id;
|
|
963
|
+
end;
|
|
958
964
|
|
|
959
965
|
-- The one way the record server writes a source. It has no session columns, so it can never write a session message: those are the
|
|
960
966
|
-- owner's own words, and only capture writes them. A view insert returns no id; the writer looks the row up by its item key
|
|
@@ -1024,5 +1030,29 @@ create trigger capture_delivery_insert instead of insert on capture_delivery beg
|
|
|
1024
1030
|
insert into delivery_unit (delivery_id, unit_id)
|
|
1025
1031
|
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
1026
1032
|
end;
|
|
1033
|
+
-- The same as capture_delivery with the subagent the delivery ran in. capture_delivery keeps its columns, since an older plugin's
|
|
1034
|
+
-- capture writes through it into a database of this revision
|
|
1035
|
+
create view capture_delivery_scoped as
|
|
1036
|
+
select session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at, null as units from delivery;
|
|
1037
|
+
create trigger capture_delivery_scoped_insert instead of insert on capture_delivery_scoped begin
|
|
1038
|
+
select raise(abort, 'the unit and the delivered session belong to different projects')
|
|
1039
|
+
where exists (select 1 from json_each(coalesce(new.units, '[]')) j
|
|
1040
|
+
where (select project_id from unit where id = j.value) is not (select project_id from session where id = new.session_id));
|
|
1041
|
+
insert into delivery (session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at)
|
|
1042
|
+
values (new.session_id, new.agent_id, new.event, new.outcome, new.reason, new.path, coalesce(new.eligible, 0),
|
|
1043
|
+
coalesce(new.omitted, 0), coalesce(new.chars, 0), new.at);
|
|
1044
|
+
insert into delivery_unit (delivery_id, unit_id)
|
|
1045
|
+
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
1046
|
+
end;
|
|
1047
|
+
-- Deletes the deliveries of sessions whose last delivery is before cutoff: all of session_id's, then up to 200 of the oldest others (a row
|
|
1048
|
+
-- without a session goes by its own time). Capture inserts here before each delivery it logs, naming that delivery's session
|
|
1049
|
+
create view capture_delivery_prune as select null as cutoff, null as session_id;
|
|
1050
|
+
create trigger capture_delivery_prune_insert instead of insert on capture_delivery_prune begin
|
|
1051
|
+
delete from delivery where session_id = new.session_id
|
|
1052
|
+
and not exists (select 1 from delivery n where n.session_id = new.session_id and n.at >= new.cutoff);
|
|
1053
|
+
delete from delivery where id in (select d.id from delivery d where d.at < new.cutoff
|
|
1054
|
+
and (d.session_id is null or not exists (select 1 from delivery n where n.session_id = d.session_id and n.at >= new.cutoff))
|
|
1055
|
+
order by d.at, d.id limit 200);
|
|
1056
|
+
end;
|
|
1027
1057
|
|
|
1028
|
-
pragma user_version =
|
|
1058
|
+
pragma user_version = 7;
|
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 = 7;
|
|
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() {
|
|
@@ -11245,16 +11245,29 @@ 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
|
+
"capture_delivery_prune"
|
|
11255
|
+
]);
|
|
11249
11256
|
var TRIGGER_WRITES = {
|
|
11250
11257
|
capture_session_insert: new Set(["session"]),
|
|
11251
11258
|
capture_message_insert: new Set(["source"]),
|
|
11252
11259
|
capture_edit_insert: new Set(["edit_observation"]),
|
|
11253
11260
|
capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
|
|
11261
|
+
capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
|
|
11254
11262
|
source_fts_ai: new Set(["source_fts"])
|
|
11255
11263
|
};
|
|
11264
|
+
var TRIGGER_DELETES = {
|
|
11265
|
+
capture_delivery_prune_insert: new Set(["delivery"]),
|
|
11266
|
+
delivery_ad: new Set(["delivery_unit"])
|
|
11267
|
+
};
|
|
11256
11268
|
var TRIGGER_FUNCTIONS = {
|
|
11257
|
-
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
11269
|
+
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
|
|
11270
|
+
capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
11258
11271
|
};
|
|
11259
11272
|
var CAPTURE_READS = {
|
|
11260
11273
|
project: new Set(["id", "key", "name"]),
|
|
@@ -11271,6 +11284,8 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
|
11271
11284
|
return C2.SQLITE_OK;
|
|
11272
11285
|
return fts ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11273
11286
|
}
|
|
11287
|
+
if (action === C2.SQLITE_DELETE && triggerOrView !== null && TRIGGER_DELETES[triggerOrView]?.has(table))
|
|
11288
|
+
return C2.SQLITE_OK;
|
|
11274
11289
|
if (action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE)
|
|
11275
11290
|
return fts ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11276
11291
|
if (action === C2.SQLITE_READ) {
|
|
@@ -11327,6 +11342,7 @@ var INGEST_TRIGGER_WRITES = {
|
|
|
11327
11342
|
source_fts_ad: ["delete source_fts"],
|
|
11328
11343
|
unit_state_apply: ["update unit"],
|
|
11329
11344
|
unit_state_restore: ["insert unit_state"],
|
|
11345
|
+
delivery_ad: ["delete delivery_unit"],
|
|
11330
11346
|
...Object.fromEntries([
|
|
11331
11347
|
"evidence_i",
|
|
11332
11348
|
"evidence_u",
|
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 = 7;
|
|
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() {
|
|
@@ -14712,16 +14712,29 @@ 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
|
+
"capture_delivery_prune"
|
|
14722
|
+
]);
|
|
14716
14723
|
var TRIGGER_WRITES = {
|
|
14717
14724
|
capture_session_insert: new Set(["session"]),
|
|
14718
14725
|
capture_message_insert: new Set(["source"]),
|
|
14719
14726
|
capture_edit_insert: new Set(["edit_observation"]),
|
|
14720
14727
|
capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
|
|
14728
|
+
capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
|
|
14721
14729
|
source_fts_ai: new Set(["source_fts"])
|
|
14722
14730
|
};
|
|
14731
|
+
var TRIGGER_DELETES = {
|
|
14732
|
+
capture_delivery_prune_insert: new Set(["delivery"]),
|
|
14733
|
+
delivery_ad: new Set(["delivery_unit"])
|
|
14734
|
+
};
|
|
14723
14735
|
var TRIGGER_FUNCTIONS = {
|
|
14724
|
-
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
14736
|
+
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
|
|
14737
|
+
capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
14725
14738
|
};
|
|
14726
14739
|
var CAPTURE_READS = {
|
|
14727
14740
|
project: new Set(["id", "key", "name"]),
|
|
@@ -14738,6 +14751,8 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
|
14738
14751
|
return C2.SQLITE_OK;
|
|
14739
14752
|
return fts ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14740
14753
|
}
|
|
14754
|
+
if (action === C2.SQLITE_DELETE && triggerOrView !== null && TRIGGER_DELETES[triggerOrView]?.has(table))
|
|
14755
|
+
return C2.SQLITE_OK;
|
|
14741
14756
|
if (action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE)
|
|
14742
14757
|
return fts ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14743
14758
|
if (action === C2.SQLITE_READ) {
|
|
@@ -14794,6 +14809,7 @@ var INGEST_TRIGGER_WRITES = {
|
|
|
14794
14809
|
source_fts_ad: ["delete source_fts"],
|
|
14795
14810
|
unit_state_apply: ["update unit"],
|
|
14796
14811
|
unit_state_restore: ["insert unit_state"],
|
|
14812
|
+
delivery_ad: ["delete delivery_unit"],
|
|
14797
14813
|
...Object.fromEntries([
|
|
14798
14814
|
"evidence_i",
|
|
14799
14815
|
"evidence_u",
|
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 = 7;
|
|
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() {
|
|
@@ -11294,16 +11294,29 @@ 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
|
+
"capture_delivery_prune"
|
|
11304
|
+
]);
|
|
11298
11305
|
var TRIGGER_WRITES = {
|
|
11299
11306
|
capture_session_insert: new Set(["session"]),
|
|
11300
11307
|
capture_message_insert: new Set(["source"]),
|
|
11301
11308
|
capture_edit_insert: new Set(["edit_observation"]),
|
|
11302
11309
|
capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
|
|
11310
|
+
capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
|
|
11303
11311
|
source_fts_ai: new Set(["source_fts"])
|
|
11304
11312
|
};
|
|
11313
|
+
var TRIGGER_DELETES = {
|
|
11314
|
+
capture_delivery_prune_insert: new Set(["delivery"]),
|
|
11315
|
+
delivery_ad: new Set(["delivery_unit"])
|
|
11316
|
+
};
|
|
11305
11317
|
var TRIGGER_FUNCTIONS = {
|
|
11306
|
-
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
11318
|
+
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
|
|
11319
|
+
capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
11307
11320
|
};
|
|
11308
11321
|
var CAPTURE_READS = {
|
|
11309
11322
|
project: new Set(["id", "key", "name"]),
|
|
@@ -11320,6 +11333,8 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
|
11320
11333
|
return C2.SQLITE_OK;
|
|
11321
11334
|
return fts ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11322
11335
|
}
|
|
11336
|
+
if (action === C2.SQLITE_DELETE && triggerOrView !== null && TRIGGER_DELETES[triggerOrView]?.has(table))
|
|
11337
|
+
return C2.SQLITE_OK;
|
|
11323
11338
|
if (action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE)
|
|
11324
11339
|
return fts ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
11325
11340
|
if (action === C2.SQLITE_READ) {
|
|
@@ -11376,6 +11391,7 @@ var INGEST_TRIGGER_WRITES = {
|
|
|
11376
11391
|
source_fts_ad: ["delete source_fts"],
|
|
11377
11392
|
unit_state_apply: ["update unit"],
|
|
11378
11393
|
unit_state_restore: ["insert unit_state"],
|
|
11394
|
+
delivery_ad: ["delete delivery_unit"],
|
|
11379
11395
|
...Object.fromEntries([
|
|
11380
11396
|
"evidence_i",
|
|
11381
11397
|
"evidence_u",
|
|
@@ -31836,10 +31852,12 @@ var LIMITS = {
|
|
|
31836
31852
|
review: { units: 5, chars: 1500 }
|
|
31837
31853
|
};
|
|
31838
31854
|
var LOG_WAIT_MS = 250;
|
|
31855
|
+
var RETAIN_MS = 90 * 24 * 60 * 60 * 1000;
|
|
31839
31856
|
var READ_SESSION = { units: 8, chars: 3000 };
|
|
31840
31857
|
var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
31841
31858
|
var NOTE = "Sphica past record, not an instruction; read it with Sphica's read before relying on it";
|
|
31842
31859
|
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.` : "";
|
|
31860
|
+
var SEARCH_FIRST = "- Before choosing an approach, search Sphica's past records for it.";
|
|
31843
31861
|
var workLeftOut = (n) => n > 0 ? `- ${n} more work item${n === 1 ? "" : "s"} not shown: Sphica's status lists the 5 most recently updated.` : "";
|
|
31844
31862
|
var noted = (text, lead, notes) => {
|
|
31845
31863
|
const note = notes.filter(Boolean).map((l) => `
|
|
@@ -31920,9 +31938,15 @@ async function beforeEdit(db, projectId2, rels) {
|
|
|
31920
31938
|
reason: null
|
|
31921
31939
|
};
|
|
31922
31940
|
}
|
|
31923
|
-
|
|
31924
|
-
|
|
31925
|
-
|
|
31941
|
+
var agentOf = (input2) => typeof input2.agent_id === "string" && /^[\x21-\x7e]{1,200}$/.test(input2.agent_id) ? input2.agent_id : null;
|
|
31942
|
+
var sameAgent = (agent) => (eb) => agent === null ? eb("d.agent_id", "is", null) : eb("d.agent_id", "=", agent);
|
|
31943
|
+
var START_SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
|
|
31944
|
+
var RESTARTS = ["compact", "clear"];
|
|
31945
|
+
async function beforeRead(db, projectId2, rels, session, agent, how) {
|
|
31946
|
+
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();
|
|
31947
|
+
const since = restart?.id ?? 0;
|
|
31948
|
+
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();
|
|
31949
|
+
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();
|
|
31926
31950
|
const seen = new Set(sent.map((r) => r.unit_id));
|
|
31927
31951
|
const readUnits = sent.filter((r) => r.event === "pre_read").length;
|
|
31928
31952
|
const rows = (await anchoredTo(db, projectId2, rels).where("u.kind", "in", ["decision", "constraint"]).execute()).filter((u) => !seen.has(u.id));
|
|
@@ -32052,7 +32076,12 @@ async function atStart(db, projectId2, branch, place) {
|
|
|
32052
32076
|
const workLeft = Math.max((workTotal ?? 0) - f.kept.filter((i) => i < work.length).length, 0);
|
|
32053
32077
|
const broadLeft = Math.max((broadTotal ?? 0) - shownUnits.length, 0);
|
|
32054
32078
|
return {
|
|
32055
|
-
...noted(f.text, lead, [
|
|
32079
|
+
...noted(f.text, lead, [
|
|
32080
|
+
leftOut(broadLeft),
|
|
32081
|
+
workLeftOut(workLeft),
|
|
32082
|
+
await waiting(db, projectId2, place),
|
|
32083
|
+
place.subagent ? SEARCH_FIRST : ""
|
|
32084
|
+
]),
|
|
32056
32085
|
units: shownUnits,
|
|
32057
32086
|
eligible: (workTotal ?? 0) + (broadTotal ?? 0),
|
|
32058
32087
|
omitted: workLeft + broadLeft,
|
|
@@ -32120,34 +32149,64 @@ function markOnce(kind, key) {
|
|
|
32120
32149
|
function onceUnavailable(session) {
|
|
32121
32150
|
return markOnce("unavailable", session);
|
|
32122
32151
|
}
|
|
32123
|
-
async function
|
|
32152
|
+
async function write2(trx, e, plan) {
|
|
32153
|
+
const id = sessionId(e.projectId, e.host, e.external);
|
|
32154
|
+
const t = Date.now();
|
|
32155
|
+
const now = iso(t);
|
|
32156
|
+
await trx.insertInto("capture_delivery_prune").values({ cutoff: iso(t - RETAIN_MS), session_id: id }).execute();
|
|
32157
|
+
await trx.insertInto("capture_session").values({
|
|
32158
|
+
id,
|
|
32159
|
+
project_id: e.projectId,
|
|
32160
|
+
host: e.host,
|
|
32161
|
+
external_id: e.external,
|
|
32162
|
+
branch: e.branch,
|
|
32163
|
+
started_at: now
|
|
32164
|
+
}).execute();
|
|
32165
|
+
await trx.insertInto("capture_delivery_scoped").values({
|
|
32166
|
+
session_id: id,
|
|
32167
|
+
agent_id: e.agent,
|
|
32168
|
+
event: e.event,
|
|
32169
|
+
outcome: plan.text ? "emitted" : "nothing",
|
|
32170
|
+
reason: plan.reason,
|
|
32171
|
+
path: plan.path,
|
|
32172
|
+
eligible: plan.eligible,
|
|
32173
|
+
omitted: plan.omitted,
|
|
32174
|
+
chars: plan.text.length - plan.note.length,
|
|
32175
|
+
at: now,
|
|
32176
|
+
units: JSON.stringify(plan.units)
|
|
32177
|
+
}).execute();
|
|
32178
|
+
}
|
|
32179
|
+
async function log(file2, e, plan) {
|
|
32124
32180
|
const cap = openWriter("capture", file2, LOG_WAIT_MS);
|
|
32125
32181
|
try {
|
|
32126
|
-
|
|
32127
|
-
|
|
32182
|
+
await inTransaction(cap, (trx) => write2(trx, e, plan));
|
|
32183
|
+
} finally {
|
|
32184
|
+
await cap.destroy().catch(() => {});
|
|
32185
|
+
}
|
|
32186
|
+
}
|
|
32187
|
+
async function lockedPlan(file2, e, make, keep) {
|
|
32188
|
+
const cap = openWriter("capture", file2, LOG_WAIT_MS);
|
|
32189
|
+
let held = false;
|
|
32190
|
+
let plan = null;
|
|
32191
|
+
try {
|
|
32128
32192
|
await inTransaction(cap, async (trx) => {
|
|
32129
|
-
|
|
32130
|
-
await
|
|
32131
|
-
|
|
32132
|
-
|
|
32133
|
-
outcome,
|
|
32134
|
-
reason: plan.reason,
|
|
32135
|
-
path: plan.path,
|
|
32136
|
-
eligible: plan.eligible,
|
|
32137
|
-
omitted: plan.omitted,
|
|
32138
|
-
chars: plan.text.length - plan.note.length,
|
|
32139
|
-
at: now,
|
|
32140
|
-
units: JSON.stringify(plan.units)
|
|
32141
|
-
}).execute();
|
|
32193
|
+
held = true;
|
|
32194
|
+
plan = await make();
|
|
32195
|
+
if (keep(plan))
|
|
32196
|
+
await write2(trx, e, plan);
|
|
32142
32197
|
});
|
|
32198
|
+
} catch (err) {
|
|
32199
|
+
if (held && !plan)
|
|
32200
|
+
throw err;
|
|
32143
32201
|
} finally {
|
|
32144
32202
|
await cap.destroy().catch(() => {});
|
|
32145
32203
|
}
|
|
32204
|
+
return plan ?? make();
|
|
32146
32205
|
}
|
|
32147
32206
|
async function deliver(input2, host = "claude-code", file2 = dbFile()) {
|
|
32148
32207
|
const name = input2.hook_event_name;
|
|
32149
32208
|
const call = reviewCall(input2);
|
|
32150
|
-
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;
|
|
32209
|
+
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;
|
|
32151
32210
|
if (!event || !input2.session_id)
|
|
32152
32211
|
return "";
|
|
32153
32212
|
if (event === "prompt" && !isOwnerTurn(input2, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined))
|
|
@@ -32181,21 +32240,38 @@ async function deliver(input2, host = "claude-code", file2 = dbFile()) {
|
|
|
32181
32240
|
return "";
|
|
32182
32241
|
}
|
|
32183
32242
|
if (event === "session_start" && input2.source === "resume") {
|
|
32184
|
-
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();
|
|
32243
|
+
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();
|
|
32185
32244
|
if (said)
|
|
32186
32245
|
return "";
|
|
32187
32246
|
}
|
|
32188
|
-
const
|
|
32189
|
-
|
|
32190
|
-
|
|
32247
|
+
const reader = db;
|
|
32248
|
+
const session = input2.session_id;
|
|
32249
|
+
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) : {
|
|
32250
|
+
...await atStart(reader, pid, branchOf(place.root), {
|
|
32251
|
+
file: file2,
|
|
32252
|
+
key: place.key,
|
|
32253
|
+
host,
|
|
32254
|
+
subagent: name === "SubagentStart",
|
|
32255
|
+
owner: name !== "SubagentStart" && isOwnerTurn(input2, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined)
|
|
32256
|
+
}),
|
|
32257
|
+
reason: name === "SubagentStart" ? "subagent" : input2.source && START_SOURCES.has(input2.source) ? input2.source : null
|
|
32258
|
+
};
|
|
32259
|
+
const entry = {
|
|
32260
|
+
projectId: pid,
|
|
32191
32261
|
host,
|
|
32192
|
-
|
|
32193
|
-
|
|
32262
|
+
external: session,
|
|
32263
|
+
agent: agentOf(input2),
|
|
32264
|
+
event,
|
|
32265
|
+
branch: branchOf(place.root)
|
|
32266
|
+
};
|
|
32267
|
+
const keep = (p) => !(onPath && !p.text);
|
|
32268
|
+
if (event === "pre_read" || event === "session_start")
|
|
32269
|
+
return (await lockedPlan(file2, entry, make, keep)).text;
|
|
32270
|
+
const plan = await make();
|
|
32194
32271
|
if (call && plan.text && toldBefore(`${host}\x00${input2.session_id}`, plan.once ?? plan.text))
|
|
32195
32272
|
return "";
|
|
32196
|
-
if (
|
|
32197
|
-
|
|
32198
|
-
await log(file2, pid, host, input2.session_id, event, plan, plan.text ? "emitted" : "nothing", branchOf(place.root)).catch(() => {});
|
|
32273
|
+
if (keep(plan))
|
|
32274
|
+
await log(file2, entry, plan).catch(() => {});
|
|
32199
32275
|
return plan.text;
|
|
32200
32276
|
} catch (e) {
|
|
32201
32277
|
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 = 7;
|
|
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() {
|
|
@@ -46005,16 +46005,29 @@ 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
|
+
"capture_delivery_prune"
|
|
46015
|
+
]);
|
|
46009
46016
|
var TRIGGER_WRITES = {
|
|
46010
46017
|
capture_session_insert: new Set(["session"]),
|
|
46011
46018
|
capture_message_insert: new Set(["source"]),
|
|
46012
46019
|
capture_edit_insert: new Set(["edit_observation"]),
|
|
46013
46020
|
capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
|
|
46021
|
+
capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
|
|
46014
46022
|
source_fts_ai: new Set(["source_fts"])
|
|
46015
46023
|
};
|
|
46024
|
+
var TRIGGER_DELETES = {
|
|
46025
|
+
capture_delivery_prune_insert: new Set(["delivery"]),
|
|
46026
|
+
delivery_ad: new Set(["delivery_unit"])
|
|
46027
|
+
};
|
|
46016
46028
|
var TRIGGER_FUNCTIONS = {
|
|
46017
|
-
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
46029
|
+
capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
|
|
46030
|
+
capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
|
|
46018
46031
|
};
|
|
46019
46032
|
var CAPTURE_READS = {
|
|
46020
46033
|
project: new Set(["id", "key", "name"]),
|
|
@@ -46031,6 +46044,8 @@ function captureAuthorizer(own2, action, p1, p2, triggerOrView) {
|
|
|
46031
46044
|
return C2.SQLITE_OK;
|
|
46032
46045
|
return fts ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
46033
46046
|
}
|
|
46047
|
+
if (action === C2.SQLITE_DELETE && triggerOrView !== null && TRIGGER_DELETES[triggerOrView]?.has(table))
|
|
46048
|
+
return C2.SQLITE_OK;
|
|
46034
46049
|
if (action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE)
|
|
46035
46050
|
return fts ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
46036
46051
|
if (action === C2.SQLITE_READ) {
|
|
@@ -46087,6 +46102,7 @@ var INGEST_TRIGGER_WRITES = {
|
|
|
46087
46102
|
source_fts_ad: ["delete source_fts"],
|
|
46088
46103
|
unit_state_apply: ["update unit"],
|
|
46089
46104
|
unit_state_restore: ["insert unit_state"],
|
|
46105
|
+
delivery_ad: ["delete delivery_unit"],
|
|
46090
46106
|
...Object.fromEntries([
|
|
46091
46107
|
"evidence_i",
|
|
46092
46108
|
"evidence_u",
|
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 = 7;
|
|
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() {
|
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.18",
|
|
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",
|