synomem 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -2
- package/CHANGELOG.md +37 -1
- package/README.md +28 -15
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +311 -70
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +96 -17
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +333 -76
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +8 -8
- package/dist/import.d.ts +207 -13
- package/dist/import.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +156 -31
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp-server.js +54 -8
- package/dist/mcp-server.js.map +1 -1
- package/dist/ports/repository.d.ts +18 -1
- package/dist/ports/repository.d.ts.map +1 -1
- package/dist/projections.d.ts +18 -1
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +137 -44
- package/dist/projections.js.map +1 -1
- package/dist/remote.d.ts +57 -9
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +43 -2
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +302 -21
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +142 -26
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +57 -10
- package/dist/service.d.ts.map +1 -1
- package/dist/skill-install.d.ts +8 -2
- package/dist/skill-install.d.ts.map +1 -1
- package/dist/skill-install.js +6 -11
- package/dist/skill-install.js.map +1 -1
- package/dist/storage.d.ts +41 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +254 -36
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +213 -38
- package/dist/types.d.ts.map +1 -1
- package/docs/cli.md +53 -20
- package/docs/examples.md +4 -4
- package/docs/mcp.md +12 -4
- package/docs/skill.md +10 -7
- package/docs/storage-format.md +4 -4
- package/openapi/synomem-v1.yaml +24 -24
- package/package.json +1 -1
- package/skills/synomem/SKILL.md +24 -8
- package/skills/synomem/agents/openai.yaml +1 -1
- package/skills/synomem/references/examples.md +1 -1
- package/src/cli.ts +572 -173
- package/src/client.ts +391 -79
- package/src/config.ts +8 -8
- package/src/index.ts +11 -1
- package/src/mcp/index.ts +189 -30
- package/src/mcp-server.ts +58 -8
- package/src/ports/repository.ts +16 -0
- package/src/projections.ts +139 -44
- package/src/remote.ts +118 -8
- package/src/schemas.ts +147 -26
- package/src/service.ts +59 -7
- package/src/skill-install.ts +14 -17
- package/src/storage.ts +326 -34
- package/src/types.ts +228 -39
package/dist/storage.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import type { SynomemRepository } from './ports/repository.js';
|
|
3
3
|
import { SynomemError } from './errors.js';
|
|
4
|
-
import type { ActorIdentity, SynomemConfig, SynomemConfigOverrides, AgentProfile, ChangePage, SynomemEvent, ItemListInput, ItemSummary, RecordKind, KudosListInput, KudosSummary, Page } from './types.js';
|
|
4
|
+
import type { ActorIdentity, SynomemConfig, SynomemConfigOverrides, AgentProfile, AgentRuntimeBinding, ChangePage, JsonValue, SynomemEvent, ItemListInput, ItemSummary, RecordKind, KudosListInput, KudosSummary, Page } from './types.js';
|
|
5
5
|
interface EventRow {
|
|
6
6
|
id: string;
|
|
7
7
|
payload: string;
|
|
@@ -80,6 +80,46 @@ export declare class SynomemStorage implements SynomemRepository {
|
|
|
80
80
|
insertAgent(profile: AgentProfile): void;
|
|
81
81
|
updateAgent(profile: AgentProfile, updatedAt: string): void;
|
|
82
82
|
getAgent(idOrAlias: string): AgentProfile | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Resolves a name to a canonical agent, reporting ambiguity rather than
|
|
85
|
+
* guessing.
|
|
86
|
+
*
|
|
87
|
+
* The plan is specific: `mycroft`, `Mycroft` and `Mike` may all resolve to one
|
|
88
|
+
* agent, but a name two visible agents both claim must return candidates. An
|
|
89
|
+
* alias that collides with a different agent's canonical ID is exactly that
|
|
90
|
+
* case — silently preferring the ID would attribute work to the wrong agent,
|
|
91
|
+
* and the person who typed the name would never know.
|
|
92
|
+
*/
|
|
93
|
+
resolveAgent(query: string): {
|
|
94
|
+
match?: AgentProfile;
|
|
95
|
+
candidates: AgentProfile[];
|
|
96
|
+
};
|
|
97
|
+
listRuntimeBindings(agentId: string): AgentRuntimeBinding[];
|
|
98
|
+
bindRuntime(binding: {
|
|
99
|
+
id: string;
|
|
100
|
+
agentId: string;
|
|
101
|
+
installationId?: string;
|
|
102
|
+
runtime: string;
|
|
103
|
+
profile?: string;
|
|
104
|
+
capabilities?: Record<string, JsonValue>;
|
|
105
|
+
boundAt: string;
|
|
106
|
+
}): void;
|
|
107
|
+
unbindRuntime(bindingId: string): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Advisory only. Records that Synomem observed this binding act — never that
|
|
110
|
+
* the runtime is reachable now, and never that a delivery succeeded.
|
|
111
|
+
*/
|
|
112
|
+
touchRuntimeBinding(agentId: string, runtime: string, at: string): void;
|
|
113
|
+
/**
|
|
114
|
+
* Writes an agent's aliases, refusing any that would make a name ambiguous.
|
|
115
|
+
*
|
|
116
|
+
* Two collisions matter and both are rejected here rather than at lookup: an
|
|
117
|
+
* alias another agent already claims, and an alias equal to a different
|
|
118
|
+
* agent's canonical ID. Catching them at write means the person adding the
|
|
119
|
+
* alias sees the conflict, instead of a later reader silently getting one of
|
|
120
|
+
* two possible agents.
|
|
121
|
+
*/
|
|
122
|
+
private insertAliases;
|
|
83
123
|
listAgents(): AgentProfile[];
|
|
84
124
|
replaceProjectionManifest(paths: string[], generatedAt: string): void;
|
|
85
125
|
replaceAgentProjectionManifest(agentId: string, paths: string[], generatedAt: string): void;
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAkB,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAkB,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3D,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,sBAAsB,EACtB,YAAY,EACZ,mBAAmB,EACnB,UAAU,EACV,SAAS,EAET,YAAY,EAEZ,aAAa,EACb,WAAW,EACX,UAAU,EACV,cAAc,EACd,YAAY,EACZ,IAAI,EACL,MAAM,YAAY,CAAC;AAEpB,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAyCD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,YAAY,CAAA;KAAE,CAAC,CAAC;CACrD;AA4SD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,sBAAsB,CAAC;CACjC;AAED,qBAAa,cAAe,YAAW,iBAAiB;IACtD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,MAAM,EAAE,aAAa,CAAiB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAyB;IAC1D,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,eAAe,CAAoC;gBAE/C,OAAO,EAAE,cAAc;IASnC,IAAI,IAAI,IAAI;IA4EZ,OAAO,CAAC,OAAO;IA2Ef,OAAO,CAAC,qBAAqB;IAkB7B,EAAE,IAAI,YAAY;IAMlB,cAAc,IAAI,IAAI;IAItB,WAAW,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAgC3D,eAAe,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC;IAwBzC,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAsDtC,OAAO,CAAC,mBAAmB;IAuC3B,OAAO,CAAC,iBAAiB;IAyKzB,wBAAwB,IAAI,IAAI;IAchC,wBAAwB,IAAI,IAAI;IAchC,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAYjD,kBAAkB,CAChB,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAC,GAAG,cAAc,EAC1E,MAAM,EAAE,aAAa,GACpB,IAAI,CAAC,YAAY,CAAC;IA0FrB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,UAAU;IAmF7F,iBAAiB,CACf,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAC,GAAG,aAAa,EACxE,MAAM,EAAE,aAAa,GACpB,IAAI,CAAC,WAAW,CAAC;IAyIpB,eAAe,CACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,EACrB,KAAK,CAAC,EAAE,UAAU,EAAE,GACnB,UAAU;IAiFb,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAOnD,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,EAAE;IAejD,kBAAkB,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE;IAiCjF,eAAe,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAkBvD,cAAc,IAAI;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,EAAE,CAAA;KAAE;IActE,sBAAsB,IAAI,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAUnE,OAAO,CAAC,gBAAgB;IAUxB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAM9C,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAUhG,SAAS,IAAI,YAAY,EAAE;IAM3B,iBAAiB,IAAI,YAAY,EAAE;IAInC,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE;IAmBzD,UAAU,IAAI,SAAS;IAevB,wBAAwB,IAAI,IAAI;IAwBhC,YAAY,IAAI,QAAQ,EAAE;IAM1B,OAAO,CAAC,UAAU;IA8ElB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAgBxC,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAS3D,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAKrD;;;;;;;;;OASG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,YAAY,CAAC;QAAC,UAAU,EAAE,YAAY,EAAE,CAAA;KAAE;IAiBjF,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,EAAE;IA6B3D,WAAW,CAAC,OAAO,EAAE;QACnB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QACzC,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,IAAI;IAoBR,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAOzC;;;OAGG;IACH,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAQvE;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IA2BrB,UAAU,IAAI,YAAY,EAAE;IAO5B,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAUrE,8BAA8B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAY3F,kBAAkB,IAAI,MAAM,EAAE;IAQ9B,cAAc,IAAI,MAAM,EAAE;IAQ1B,WAAW,IAAI,MAAM;IAKf,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BlD,OAAO,CAAC,qBAAqB;IAW7B,KAAK,IAAI,IAAI;CAId"}
|
package/dist/storage.js
CHANGED
|
@@ -5,6 +5,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
5
5
|
import { defaultConfig, mergeConfig } from './config.js';
|
|
6
6
|
import { asSynomemError, SynomemError } from './errors.js';
|
|
7
7
|
import { assertNoSymlinkEscape, atomicWriteFile, ensureDirectory, readJsonFile, } from './fs-utils.js';
|
|
8
|
+
import { dueInstant } from './projections.js';
|
|
8
9
|
import { actorSchema, eventSchema, profileSchema } from './schemas.js';
|
|
9
10
|
const migrationV1 = `
|
|
10
11
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
@@ -96,6 +97,52 @@ CREATE INDEX kudos_current_recipient ON kudos_current(recipient_agent_id, given_
|
|
|
96
97
|
CREATE INDEX kudos_current_actor ON kudos_current(actor_kind, actor_id, given_sequence DESC);
|
|
97
98
|
CREATE INDEX kudos_current_status ON kudos_current(status, revocation_status, given_sequence DESC);
|
|
98
99
|
`;
|
|
100
|
+
/**
|
|
101
|
+
* v4 records a task's or todo's deadline on the item index.
|
|
102
|
+
*
|
|
103
|
+
* The plan asks for "accepted Tasks past their due date" as a bounded query.
|
|
104
|
+
* Answering that from the event log means replaying every task on every call,
|
|
105
|
+
* so the deadline is projected alongside the rest of the summary. It is stored
|
|
106
|
+
* as an ISO instant: a date-only due date resolves to the end of that day, so
|
|
107
|
+
* "overdue" means the day has actually passed rather than merely started.
|
|
108
|
+
*/
|
|
109
|
+
const migrationV4 = `
|
|
110
|
+
ALTER TABLE items_current ADD COLUMN due_at TEXT;
|
|
111
|
+
CREATE INDEX items_current_due ON items_current(kind, status, due_at);
|
|
112
|
+
`;
|
|
113
|
+
/**
|
|
114
|
+
* v5 makes alias lookup case-insensitive and unambiguous.
|
|
115
|
+
*
|
|
116
|
+
* The plan requires that `mycroft`, `Mycroft` and `Mike` may all resolve to one
|
|
117
|
+
* canonical agent, while a name two agents both claim must return candidates
|
|
118
|
+
* rather than guessing. A normalized column with a unique index enforces the
|
|
119
|
+
* second half at write time: the collision is refused when the alias is added,
|
|
120
|
+
* not discovered later by whoever happens to look it up first.
|
|
121
|
+
*
|
|
122
|
+
* Runtime bindings arrive here too. An agent is not one harness forever —
|
|
123
|
+
* Mycroft may run Hermes on two machines, or move between harnesses — so the
|
|
124
|
+
* binding is its own row keyed by agent, installation, runtime and profile
|
|
125
|
+
* rather than a field on the agent.
|
|
126
|
+
*/
|
|
127
|
+
const migrationV5 = `
|
|
128
|
+
ALTER TABLE aliases ADD COLUMN normalized_alias TEXT;
|
|
129
|
+
UPDATE aliases SET normalized_alias = lower(alias);
|
|
130
|
+
CREATE UNIQUE INDEX aliases_normalized ON aliases(normalized_alias);
|
|
131
|
+
|
|
132
|
+
CREATE TABLE agent_runtime_bindings (
|
|
133
|
+
id TEXT PRIMARY KEY,
|
|
134
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
135
|
+
installation_id TEXT,
|
|
136
|
+
runtime TEXT NOT NULL,
|
|
137
|
+
profile TEXT,
|
|
138
|
+
capabilities_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(capabilities_json)),
|
|
139
|
+
bound_at TEXT NOT NULL,
|
|
140
|
+
last_seen_at TEXT
|
|
141
|
+
) STRICT;
|
|
142
|
+
CREATE UNIQUE INDEX agent_runtime_bindings_unique
|
|
143
|
+
ON agent_runtime_bindings(agent_id, runtime, COALESCE(profile, ''), COALESCE(installation_id, ''));
|
|
144
|
+
CREATE INDEX agent_runtime_bindings_agent ON agent_runtime_bindings(agent_id);
|
|
145
|
+
`;
|
|
99
146
|
const migrationV3 = `
|
|
100
147
|
DROP TRIGGER IF EXISTS events_append_only_update;
|
|
101
148
|
DROP TRIGGER IF EXISTS events_append_only_delete;
|
|
@@ -203,6 +250,13 @@ function itemSummaryFromRow(row) {
|
|
|
203
250
|
...(row.assignee_agent_id ? { assigneeAgentId: row.assignee_agent_id } : {}),
|
|
204
251
|
};
|
|
205
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Resolves a due value to a comparable instant.
|
|
255
|
+
*
|
|
256
|
+
* A date-only deadline resolves to the END of that day, so "overdue" means the
|
|
257
|
+
* day has passed rather than merely begun. Treating 2026-09-15 as midnight
|
|
258
|
+
* would report a task due today as already late.
|
|
259
|
+
*/
|
|
206
260
|
function eventKind(event) {
|
|
207
261
|
if (event.type.startsWith('kudos.'))
|
|
208
262
|
return 'kudos';
|
|
@@ -210,6 +264,8 @@ function eventKind(event) {
|
|
|
210
264
|
return 'memo';
|
|
211
265
|
if (event.type.startsWith('note.'))
|
|
212
266
|
return 'note';
|
|
267
|
+
if (event.type.startsWith('task.'))
|
|
268
|
+
return 'task';
|
|
213
269
|
if (event.type.startsWith('todo.'))
|
|
214
270
|
return 'todo';
|
|
215
271
|
return undefined;
|
|
@@ -321,7 +377,7 @@ export class SynomemStorage {
|
|
|
321
377
|
migrate() {
|
|
322
378
|
const db = this.db();
|
|
323
379
|
const version = Number(db.prepare('PRAGMA user_version').get().user_version);
|
|
324
|
-
if (version >
|
|
380
|
+
if (version > 5) {
|
|
325
381
|
throw new SynomemError('UNSUPPORTED_SCHEMA', `Database schema version ${version} is newer than this package supports.`);
|
|
326
382
|
}
|
|
327
383
|
if (version === 0) {
|
|
@@ -344,19 +400,38 @@ export class SynomemStorage {
|
|
|
344
400
|
if (afterV2 === 2) {
|
|
345
401
|
this.transactionSync(() => {
|
|
346
402
|
db.exec(migrationV3);
|
|
347
|
-
|
|
403
|
+
// The item index is left empty here and populated by v4, which adds the
|
|
404
|
+
// due-date column the projection writes. Rebuilding before that column
|
|
405
|
+
// exists fails on the first task.
|
|
348
406
|
db.prepare('INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)').run(3, new Date().toISOString());
|
|
349
407
|
db.exec('PRAGMA user_version = 3');
|
|
350
408
|
});
|
|
351
409
|
}
|
|
410
|
+
const afterV3 = Number(db.prepare('PRAGMA user_version').get().user_version);
|
|
411
|
+
if (afterV3 === 3) {
|
|
412
|
+
this.transactionSync(() => {
|
|
413
|
+
db.exec(migrationV4);
|
|
414
|
+
this.rebuildItemsCurrentIndex();
|
|
415
|
+
db.prepare('INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)').run(4, new Date().toISOString());
|
|
416
|
+
db.exec('PRAGMA user_version = 4');
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const afterV4 = Number(db.prepare('PRAGMA user_version').get().user_version);
|
|
420
|
+
if (afterV4 === 4) {
|
|
421
|
+
this.transactionSync(() => {
|
|
422
|
+
db.exec(migrationV5);
|
|
423
|
+
db.prepare('INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)').run(5, new Date().toISOString());
|
|
424
|
+
db.exec('PRAGMA user_version = 5');
|
|
425
|
+
});
|
|
426
|
+
}
|
|
352
427
|
}
|
|
353
428
|
assertSchemaSupported() {
|
|
354
429
|
const version = Number(this.db().prepare('PRAGMA user_version').get().user_version);
|
|
355
|
-
if (version !==
|
|
356
|
-
if (version
|
|
357
|
-
throw new SynomemError('UNSUPPORTED_SCHEMA',
|
|
430
|
+
if (version !== 5) {
|
|
431
|
+
if (version >= 1 && version <= 4) {
|
|
432
|
+
throw new SynomemError('UNSUPPORTED_SCHEMA', `Database schema version ${version} requires migration. Open this home once with readOnly: false, then retry the read-only client.`);
|
|
358
433
|
}
|
|
359
|
-
throw new SynomemError('UNSUPPORTED_SCHEMA', `Expected database schema version
|
|
434
|
+
throw new SynomemError('UNSUPPORTED_SCHEMA', `Expected database schema version 5; found ${version}.`);
|
|
360
435
|
}
|
|
361
436
|
}
|
|
362
437
|
db() {
|
|
@@ -477,9 +552,9 @@ export class SynomemStorage {
|
|
|
477
552
|
item_id, kind, created_sequence, updated_sequence, created_at, updated_at,
|
|
478
553
|
actor_kind, actor_id, actor_display_name, title, tags_json, visibility, status,
|
|
479
554
|
recipient_agent_id, recipient_display_name, owner_agent_id, owner_display_name,
|
|
480
|
-
assignee_agent_id, assignee_display_name
|
|
481
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
482
|
-
.run(event.aggregateId, values.kind, sequence, sequence, event.createdAt, event.createdAt, event.actor.kind, event.actor.id, event.actor.displayName ?? null, values.title, JSON.stringify(values.tags ?? []), values.visibility, values.status, values.recipientAgentId ?? null, values.recipientDisplayName ?? null, values.ownerAgentId ?? null, values.ownerDisplayName ?? null, values.assigneeAgentId ?? null, values.assigneeDisplayName ?? null);
|
|
555
|
+
assignee_agent_id, assignee_display_name, due_at
|
|
556
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
557
|
+
.run(event.aggregateId, values.kind, sequence, sequence, event.createdAt, event.createdAt, event.actor.kind, event.actor.id, event.actor.displayName ?? null, values.title, JSON.stringify(values.tags ?? []), values.visibility, values.status, values.recipientAgentId ?? null, values.recipientDisplayName ?? null, values.ownerAgentId ?? null, values.ownerDisplayName ?? null, values.assigneeAgentId ?? null, values.assigneeDisplayName ?? null, values.dueAt ?? null);
|
|
483
558
|
};
|
|
484
559
|
const updateStatus = (status) => {
|
|
485
560
|
this.db()
|
|
@@ -535,33 +610,63 @@ export class SynomemStorage {
|
|
|
535
610
|
}
|
|
536
611
|
else if (event.type === 'note.archived')
|
|
537
612
|
updateStatus('archived');
|
|
538
|
-
else if (event.type === '
|
|
613
|
+
else if (event.type === 'task.created') {
|
|
539
614
|
insert({
|
|
540
|
-
kind: '
|
|
615
|
+
kind: 'task',
|
|
541
616
|
title: event.title,
|
|
542
617
|
tags: event.tags,
|
|
543
618
|
visibility: event.visibility,
|
|
544
619
|
status: event.requiresAcceptance ? 'assigned' : 'open',
|
|
545
620
|
assigneeAgentId: event.assigneeAgentId,
|
|
546
621
|
assigneeDisplayName: event.assigneeDisplayName,
|
|
622
|
+
...((due) => (due ? { dueAt: due } : {}))(dueInstant(event.due)),
|
|
547
623
|
});
|
|
548
624
|
}
|
|
549
|
-
else if (event.type === '
|
|
625
|
+
else if (event.type === 'task.updated') {
|
|
550
626
|
this.db()
|
|
551
|
-
.prepare(`UPDATE items_current SET title = ?, tags_json = ?, visibility = ?,
|
|
627
|
+
.prepare(`UPDATE items_current SET title = ?, tags_json = ?, visibility = ?, due_at = ?,
|
|
552
628
|
updated_sequence = ?, updated_at = ? WHERE item_id = ?`)
|
|
553
|
-
.run(event.title, JSON.stringify(event.tags ?? []), event.visibility, sequence, event.createdAt, event.aggregateId);
|
|
629
|
+
.run(event.title, JSON.stringify(event.tags ?? []), event.visibility, dueInstant(event.due) ?? null, sequence, event.createdAt, event.aggregateId);
|
|
554
630
|
}
|
|
555
|
-
else if (event.type === '
|
|
631
|
+
else if (event.type === 'task.accepted')
|
|
556
632
|
updateStatus('open');
|
|
557
|
-
else if (event.type === '
|
|
633
|
+
else if (event.type === 'task.rejected')
|
|
558
634
|
updateStatus('rejected');
|
|
635
|
+
else if (event.type === 'task.completed')
|
|
636
|
+
updateStatus('completed');
|
|
637
|
+
else if (event.type === 'task.reopened')
|
|
638
|
+
updateStatus('open');
|
|
639
|
+
else if (event.type === 'task.canceled')
|
|
640
|
+
updateStatus('canceled');
|
|
641
|
+
else if (event.type === 'todo.created') {
|
|
642
|
+
// A Todo's owner IS its author, and it is always private. Recording the
|
|
643
|
+
// owner explicitly rather than inferring it from the actor keeps the
|
|
644
|
+
// owner-scoped read filters uniform across notes and todos.
|
|
645
|
+
insert({
|
|
646
|
+
kind: 'todo',
|
|
647
|
+
title: event.title,
|
|
648
|
+
tags: event.tags,
|
|
649
|
+
visibility: 'private',
|
|
650
|
+
status: 'open',
|
|
651
|
+
ownerAgentId: event.actor.id,
|
|
652
|
+
...(event.actor.displayName ? { ownerDisplayName: event.actor.displayName } : {}),
|
|
653
|
+
...((due) => (due ? { dueAt: due } : {}))(dueInstant(event.due)),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
else if (event.type === 'todo.updated') {
|
|
657
|
+
this.db()
|
|
658
|
+
.prepare(`UPDATE items_current SET title = ?, tags_json = ?, due_at = ?,
|
|
659
|
+
updated_sequence = ?, updated_at = ? WHERE item_id = ?`)
|
|
660
|
+
.run(event.title, JSON.stringify(event.tags ?? []), dueInstant(event.due) ?? null, sequence, event.createdAt, event.aggregateId);
|
|
661
|
+
}
|
|
559
662
|
else if (event.type === 'todo.completed')
|
|
560
663
|
updateStatus('completed');
|
|
561
664
|
else if (event.type === 'todo.reopened')
|
|
562
665
|
updateStatus('open');
|
|
563
666
|
else if (event.type === 'todo.canceled')
|
|
564
667
|
updateStatus('canceled');
|
|
668
|
+
else if (event.type === 'todo.archived')
|
|
669
|
+
updateStatus('archived');
|
|
565
670
|
}
|
|
566
671
|
rebuildItemsCurrentIndex() {
|
|
567
672
|
this.db().prepare('DELETE FROM items_current').run();
|
|
@@ -776,7 +881,7 @@ export class SynomemStorage {
|
|
|
776
881
|
if (input.pending) {
|
|
777
882
|
add(`((kind = 'kudos' AND status = 'unacknowledged') OR
|
|
778
883
|
(kind = 'memo' AND status = 'unread') OR
|
|
779
|
-
(kind = '
|
|
884
|
+
(kind = 'task' AND status IN ('assigned', 'open')))`);
|
|
780
885
|
}
|
|
781
886
|
if (input.visibility)
|
|
782
887
|
add('visibility = ?', input.visibility);
|
|
@@ -784,6 +889,31 @@ export class SynomemStorage {
|
|
|
784
889
|
add('created_at >= ?', input.from);
|
|
785
890
|
if (input.to)
|
|
786
891
|
add('created_at <= ?', input.to);
|
|
892
|
+
// Unanswered discovery: items still waiting for somebody to respond. Kept
|
|
893
|
+
// distinct from `pending`, which also counts accepted-and-in-progress work.
|
|
894
|
+
if (input.awaitingResponse) {
|
|
895
|
+
add(`((kind = 'kudos' AND status = 'unacknowledged') OR
|
|
896
|
+
(kind = 'memo' AND status = 'unread') OR
|
|
897
|
+
(kind = 'task' AND status = 'assigned'))`);
|
|
898
|
+
}
|
|
899
|
+
if (input.awaitingSince)
|
|
900
|
+
add('created_at <= ?', input.awaitingSince);
|
|
901
|
+
// Overdue discovery: a deadline that has passed, on work still open. A
|
|
902
|
+
// completed or canceled item is not overdue, however late it was.
|
|
903
|
+
if (input.overdueAsOf) {
|
|
904
|
+
add(`(due_at IS NOT NULL AND due_at < ? AND status IN ('assigned', 'open'))`, input.overdueAsOf);
|
|
905
|
+
}
|
|
906
|
+
/*
|
|
907
|
+
* Private todos are owner-only for EVERY viewer, including a human.
|
|
908
|
+
*
|
|
909
|
+
* The visibility rules below exempt human actors, on the reasoning that a
|
|
910
|
+
* person operating a local home is its operator. That does not extend to
|
|
911
|
+
* todos: the plan is explicit that a todo is visible only to its owner
|
|
912
|
+
* except through an explicitly authorized administrative capability, and
|
|
913
|
+
* this release has no such capability. Without this clause a human actor
|
|
914
|
+
* would see every agent's private reminders in an item list.
|
|
915
|
+
*/
|
|
916
|
+
add(`(kind != 'todo' OR (owner_agent_id = ? AND ? = 'agent') OR (actor_id = ? AND actor_kind = ?))`, viewer.id, viewer.kind, viewer.id, viewer.kind);
|
|
787
917
|
if (viewer.kind !== 'human') {
|
|
788
918
|
if (viewer.kind === 'agent') {
|
|
789
919
|
add(`(visibility != 'private' OR (actor_kind = ? AND actor_id = ?) OR
|
|
@@ -954,7 +1084,7 @@ export class SynomemStorage {
|
|
|
954
1084
|
itemIndexHealth() {
|
|
955
1085
|
const created = Number(this.db()
|
|
956
1086
|
.prepare(`SELECT COUNT(*) AS count FROM events WHERE type IN
|
|
957
|
-
('kudos.given', 'memo.sent', 'note.created', '
|
|
1087
|
+
('kudos.given', 'memo.sent', 'note.created', 'task.created')`)
|
|
958
1088
|
.get().count);
|
|
959
1089
|
const indexed = Number(this.db().prepare('SELECT COUNT(*) AS count FROM items_current').get()
|
|
960
1090
|
.count);
|
|
@@ -1071,13 +1201,19 @@ export class SynomemStorage {
|
|
|
1071
1201
|
'note.created',
|
|
1072
1202
|
'note.revised',
|
|
1073
1203
|
'note.archived',
|
|
1204
|
+
'task.created',
|
|
1205
|
+
'task.updated',
|
|
1206
|
+
'task.completed',
|
|
1207
|
+
'task.reopened',
|
|
1208
|
+
'task.accepted',
|
|
1209
|
+
'task.rejected',
|
|
1210
|
+
'task.canceled',
|
|
1074
1211
|
'todo.created',
|
|
1075
1212
|
'todo.updated',
|
|
1076
1213
|
'todo.completed',
|
|
1077
1214
|
'todo.reopened',
|
|
1078
|
-
'todo.accepted',
|
|
1079
|
-
'todo.rejected',
|
|
1080
1215
|
'todo.canceled',
|
|
1216
|
+
'todo.archived',
|
|
1081
1217
|
]);
|
|
1082
1218
|
if ((typeof candidate.schemaVersion === 'number' && candidate.schemaVersion > 1) ||
|
|
1083
1219
|
(typeof candidate.type === 'string' && !supportedTypes.has(candidate.type))) {
|
|
@@ -1091,7 +1227,7 @@ export class SynomemStorage {
|
|
|
1091
1227
|
legacy.kudosId ??
|
|
1092
1228
|
legacy.memoId ??
|
|
1093
1229
|
legacy.noteId ??
|
|
1094
|
-
legacy.
|
|
1230
|
+
legacy.taskId ??
|
|
1095
1231
|
(typeof legacy.agentId === 'string' ? legacy.agentId : undefined) ??
|
|
1096
1232
|
(typeof legacy.agent === 'object' && legacy.agent !== null
|
|
1097
1233
|
? legacy.agent.id
|
|
@@ -1125,9 +1261,7 @@ export class SynomemStorage {
|
|
|
1125
1261
|
this.db()
|
|
1126
1262
|
.prepare('INSERT INTO agents(id, display_name, profile_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?)')
|
|
1127
1263
|
.run(parsed.id, parsed.displayName, JSON.stringify(parsed), parsed.createdAt, parsed.createdAt);
|
|
1128
|
-
|
|
1129
|
-
this.db().prepare('INSERT INTO aliases(alias, agent_id) VALUES (?, ?)').run(alias, parsed.id);
|
|
1130
|
-
}
|
|
1264
|
+
this.insertAliases(parsed.id, parsed.aliases ?? []);
|
|
1131
1265
|
}
|
|
1132
1266
|
updateAgent(profile, updatedAt) {
|
|
1133
1267
|
const parsed = profileSchema.parse(profile);
|
|
@@ -1135,20 +1269,104 @@ export class SynomemStorage {
|
|
|
1135
1269
|
.prepare('UPDATE agents SET display_name = ?, profile_json = ?, updated_at = ? WHERE id = ?')
|
|
1136
1270
|
.run(parsed.displayName, JSON.stringify(parsed), updatedAt, parsed.id);
|
|
1137
1271
|
this.db().prepare('DELETE FROM aliases WHERE agent_id = ?').run(parsed.id);
|
|
1138
|
-
|
|
1139
|
-
this.db().prepare('INSERT INTO aliases(alias, agent_id) VALUES (?, ?)').run(alias, parsed.id);
|
|
1140
|
-
}
|
|
1272
|
+
this.insertAliases(parsed.id, parsed.aliases ?? []);
|
|
1141
1273
|
}
|
|
1142
1274
|
getAgent(idOrAlias) {
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1275
|
+
const resolved = this.resolveAgent(idOrAlias);
|
|
1276
|
+
return resolved.match;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Resolves a name to a canonical agent, reporting ambiguity rather than
|
|
1280
|
+
* guessing.
|
|
1281
|
+
*
|
|
1282
|
+
* The plan is specific: `mycroft`, `Mycroft` and `Mike` may all resolve to one
|
|
1283
|
+
* agent, but a name two visible agents both claim must return candidates. An
|
|
1284
|
+
* alias that collides with a different agent's canonical ID is exactly that
|
|
1285
|
+
* case — silently preferring the ID would attribute work to the wrong agent,
|
|
1286
|
+
* and the person who typed the name would never know.
|
|
1287
|
+
*/
|
|
1288
|
+
resolveAgent(query) {
|
|
1289
|
+
const normalized = query.trim().toLowerCase();
|
|
1290
|
+
const rows = this.db()
|
|
1291
|
+
.prepare(`SELECT DISTINCT a.profile_json
|
|
1292
|
+
FROM agents a
|
|
1293
|
+
LEFT JOIN aliases x ON x.agent_id = a.id
|
|
1294
|
+
WHERE lower(a.id) = ? OR x.normalized_alias = ?
|
|
1295
|
+
ORDER BY a.id ASC`)
|
|
1296
|
+
.all(normalized, normalized);
|
|
1297
|
+
const candidates = rows.map((row) => profileSchema.parse(JSON.parse(row.profile_json)));
|
|
1298
|
+
return candidates.length === 1 ? { match: candidates[0], candidates } : { candidates };
|
|
1299
|
+
}
|
|
1300
|
+
/* ------------------------------------------------------- runtime bindings */
|
|
1301
|
+
listRuntimeBindings(agentId) {
|
|
1302
|
+
const rows = this.db()
|
|
1303
|
+
.prepare(`SELECT id, agent_id, installation_id, runtime, profile, capabilities_json,
|
|
1304
|
+
bound_at, last_seen_at
|
|
1305
|
+
FROM agent_runtime_bindings WHERE agent_id = ? ORDER BY bound_at ASC`)
|
|
1306
|
+
.all(agentId);
|
|
1307
|
+
return rows.map((row) => ({
|
|
1308
|
+
id: row.id,
|
|
1309
|
+
agentId: row.agent_id,
|
|
1310
|
+
...(row.installation_id ? { installationId: row.installation_id } : {}),
|
|
1311
|
+
runtime: row.runtime,
|
|
1312
|
+
...(row.profile ? { profile: row.profile } : {}),
|
|
1313
|
+
capabilities: JSON.parse(row.capabilities_json),
|
|
1314
|
+
boundAt: row.bound_at,
|
|
1315
|
+
...(row.last_seen_at ? { lastSeenAt: row.last_seen_at } : {}),
|
|
1316
|
+
}));
|
|
1317
|
+
}
|
|
1318
|
+
bindRuntime(binding) {
|
|
1319
|
+
this.db()
|
|
1320
|
+
.prepare(`INSERT INTO agent_runtime_bindings(
|
|
1321
|
+
id, agent_id, installation_id, runtime, profile, capabilities_json, bound_at
|
|
1322
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1323
|
+
ON CONFLICT(agent_id, runtime, COALESCE(profile, ''), COALESCE(installation_id, ''))
|
|
1324
|
+
DO UPDATE SET capabilities_json = excluded.capabilities_json`)
|
|
1325
|
+
.run(binding.id, binding.agentId, binding.installationId ?? null, binding.runtime, binding.profile ?? null, JSON.stringify(binding.capabilities ?? {}), binding.boundAt);
|
|
1326
|
+
}
|
|
1327
|
+
unbindRuntime(bindingId) {
|
|
1328
|
+
const result = this.db()
|
|
1329
|
+
.prepare('DELETE FROM agent_runtime_bindings WHERE id = ?')
|
|
1330
|
+
.run(bindingId);
|
|
1331
|
+
return Number(result.changes) > 0;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Advisory only. Records that Synomem observed this binding act — never that
|
|
1335
|
+
* the runtime is reachable now, and never that a delivery succeeded.
|
|
1336
|
+
*/
|
|
1337
|
+
touchRuntimeBinding(agentId, runtime, at) {
|
|
1338
|
+
this.db()
|
|
1339
|
+
.prepare('UPDATE agent_runtime_bindings SET last_seen_at = ? WHERE agent_id = ? AND runtime = ?')
|
|
1340
|
+
.run(at, agentId, runtime);
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Writes an agent's aliases, refusing any that would make a name ambiguous.
|
|
1344
|
+
*
|
|
1345
|
+
* Two collisions matter and both are rejected here rather than at lookup: an
|
|
1346
|
+
* alias another agent already claims, and an alias equal to a different
|
|
1347
|
+
* agent's canonical ID. Catching them at write means the person adding the
|
|
1348
|
+
* alias sees the conflict, instead of a later reader silently getting one of
|
|
1349
|
+
* two possible agents.
|
|
1350
|
+
*/
|
|
1351
|
+
insertAliases(agentId, aliases) {
|
|
1352
|
+
for (const alias of aliases) {
|
|
1353
|
+
const normalized = alias.trim().toLowerCase();
|
|
1354
|
+
const conflictingAgent = this.db()
|
|
1355
|
+
.prepare('SELECT id FROM agents WHERE lower(id) = ? AND id != ?')
|
|
1356
|
+
.get(normalized, agentId);
|
|
1357
|
+
if (conflictingAgent) {
|
|
1358
|
+
throw new SynomemError('ALIAS_CONFLICT', `Alias "${alias}" is already the canonical ID of agent ${conflictingAgent.id}. Aliases must resolve to exactly one agent.`);
|
|
1359
|
+
}
|
|
1360
|
+
const conflictingAlias = this.db()
|
|
1361
|
+
.prepare('SELECT agent_id FROM aliases WHERE normalized_alias = ? AND agent_id != ?')
|
|
1362
|
+
.get(normalized, agentId);
|
|
1363
|
+
if (conflictingAlias) {
|
|
1364
|
+
throw new SynomemError('ALIAS_CONFLICT', `Alias "${alias}" already belongs to agent ${conflictingAlias.agent_id}. Aliases must resolve to exactly one agent.`);
|
|
1365
|
+
}
|
|
1366
|
+
this.db()
|
|
1367
|
+
.prepare('INSERT INTO aliases(alias, agent_id, normalized_alias) VALUES (?, ?, ?)')
|
|
1368
|
+
.run(alias, agentId, normalized);
|
|
1369
|
+
}
|
|
1152
1370
|
}
|
|
1153
1371
|
listAgents() {
|
|
1154
1372
|
const rows = this.db()
|