@danypops/papyrus 0.42.0 → 0.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  3. package/src/adapters/sqlite-artifact-store.ts +7 -5
  4. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  5. package/src/adapters/sqlite-gate-runner.ts +1 -1
  6. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  7. package/src/adapters/sqlite-log-store.ts +36 -17
  8. package/src/adapters/sqlite-note-event-store.ts +20 -16
  9. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  10. package/src/adapters/sqlite-task-event-store.ts +29 -21
  11. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  12. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  13. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  14. package/src/artifact-relationship-view.ts +3 -3
  15. package/src/artifact-subtree.ts +4 -2
  16. package/src/authority-registry.ts +2 -1
  17. package/src/cli.ts +785 -179
  18. package/src/client.ts +46 -20
  19. package/src/constants.ts +15 -4
  20. package/src/daemon-state.ts +4 -12
  21. package/src/daemon.ts +31 -9
  22. package/src/db.ts +119 -98
  23. package/src/discussion-service.ts +109 -44
  24. package/src/domain/artifact-event.ts +17 -4
  25. package/src/domain/artifact.ts +3 -1
  26. package/src/domain/blueprint-definition.ts +26 -31
  27. package/src/domain/checklist.ts +20 -17
  28. package/src/domain/discussion.ts +37 -18
  29. package/src/domain/gate.ts +7 -7
  30. package/src/domain/log-entry.ts +1 -1
  31. package/src/domain/note-event.ts +20 -7
  32. package/src/domain/task-event.ts +17 -7
  33. package/src/domain-services.ts +241 -110
  34. package/src/graph-projection-service.ts +34 -8
  35. package/src/id-migration.ts +17 -4
  36. package/src/index.ts +16 -11
  37. package/src/log-service.ts +6 -5
  38. package/src/log.ts +19 -0
  39. package/src/modules/discuss.ts +63 -28
  40. package/src/modules/docs.ts +74 -17
  41. package/src/modules/graph-projection.ts +20 -9
  42. package/src/modules/logs.ts +33 -21
  43. package/src/modules/notes.ts +66 -28
  44. package/src/modules/playbooks.ts +88 -29
  45. package/src/modules/rules.ts +57 -15
  46. package/src/modules/session-identity.ts +6 -2
  47. package/src/modules/tasks.ts +142 -67
  48. package/src/note-service.ts +11 -7
  49. package/src/ops.ts +131 -68
  50. package/src/playbook-definition.ts +56 -17
  51. package/src/playbook-execution.ts +13 -3
  52. package/src/ports/note-event-store.ts +6 -4
  53. package/src/ports/task-event-store.ts +9 -6
  54. package/src/ports/task-focus-store.ts +17 -4
  55. package/src/ports/task-lease-store.ts +9 -4
  56. package/src/ports/task-scope-store.ts +3 -1
  57. package/src/service.ts +143 -89
  58. package/src/session-identity-service.ts +10 -2
  59. package/src/task-context.ts +28 -16
  60. package/src/task-execution.ts +4 -12
  61. package/src/task-graph-view.ts +12 -12
  62. package/src/task-relationship-view.ts +1 -3
  63. package/src/task-service.ts +167 -72
  64. package/src/vehicle/artifact-trash-vehicle.ts +25 -13
  65. package/src/vehicle/artifact-vehicle-shared.ts +28 -7
  66. package/src/vehicle/docs-vehicle.ts +48 -16
  67. package/src/vehicle/notes-vehicle.ts +24 -6
  68. package/src/vehicle/papyrus-vehicle.ts +14 -3
  69. package/src/vehicle/playbooks-vehicle.ts +87 -18
  70. package/src/vehicle/rules-vehicle.ts +58 -21
  71. package/src/vehicle/tasks-vehicle.ts +366 -54
  72. package/src/version.ts +1 -1
  73. package/src/workflow-execution.ts +71 -52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.42.0",
3
+ "version": "0.42.2",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -36,6 +36,6 @@
36
36
  "dependencies": {
37
37
  "@danypops/vehicle-core": "^0.2.0",
38
38
  "@danypops/vehicle-client": "^0.1.1",
39
- "@danypops/vehicle-server": "^0.3.2"
39
+ "@danypops/vehicle-server": "^0.4.1"
40
40
  }
41
41
  }
@@ -8,29 +8,38 @@ export class SQLiteArtifactScopeStore implements ArtifactScopeStore {
8
8
 
9
9
  assign(artifactId: string, projectRoot: string | undefined, source: TaskScopeSource): ArtifactScope {
10
10
  inTransaction(this.db, () => {
11
- this.db.prepare(`
11
+ this.db
12
+ .prepare(`
12
13
  INSERT INTO artifact_scopes (artifact_id, project_root, source, assigned_at)
13
14
  VALUES (?, ?, ?, ?)
14
15
  ON CONFLICT(artifact_id) DO UPDATE SET
15
16
  project_root = excluded.project_root,
16
17
  source = excluded.source,
17
18
  assigned_at = excluded.assigned_at
18
- `).run(artifactId, projectRoot ?? null, source, new Date().toISOString());
19
+ `)
20
+ .run(artifactId, projectRoot ?? null, source, new Date().toISOString());
19
21
  });
20
22
  return { artifactId, ...(projectRoot === undefined ? {} : { projectRoot }), source };
21
23
  }
22
24
 
23
25
  get(artifactId: string): ArtifactScope | undefined {
24
- const row = this.db.prepare("SELECT artifact_id, project_root, source FROM artifact_scopes WHERE artifact_id = ?").get(artifactId) as
25
- | { artifact_id: string; project_root: string | null; source: TaskScopeSource }
26
- | null;
27
- return row ? { artifactId: row.artifact_id, ...(row.project_root === null ? {} : { projectRoot: row.project_root }), source: row.source } : undefined;
26
+ const row = this.db.prepare("SELECT artifact_id, project_root, source FROM artifact_scopes WHERE artifact_id = ?").get(artifactId) as {
27
+ artifact_id: string;
28
+ project_root: string | null;
29
+ source: TaskScopeSource;
30
+ } | null;
31
+ return row
32
+ ? { artifactId: row.artifact_id, ...(row.project_root === null ? {} : { projectRoot: row.project_root }), source: row.source }
33
+ : undefined;
28
34
  }
29
35
 
30
36
  ids(projectRoot: string | undefined, limit: number): string[] {
31
- const rows = projectRoot === undefined
32
- ? this.db.prepare("SELECT artifact_id FROM artifact_scopes WHERE project_root IS NULL ORDER BY artifact_id LIMIT ?").all(limit)
33
- : this.db.prepare("SELECT artifact_id FROM artifact_scopes WHERE project_root = ? ORDER BY artifact_id LIMIT ?").all(projectRoot, limit);
37
+ const rows =
38
+ projectRoot === undefined
39
+ ? this.db.prepare("SELECT artifact_id FROM artifact_scopes WHERE project_root IS NULL ORDER BY artifact_id LIMIT ?").all(limit)
40
+ : this.db
41
+ .prepare("SELECT artifact_id FROM artifact_scopes WHERE project_root = ? ORDER BY artifact_id LIMIT ?")
42
+ .all(projectRoot, limit);
34
43
  return (rows as Array<{ artifact_id: string }>).map((row) => row.artifact_id);
35
44
  }
36
45
  }
@@ -1,8 +1,5 @@
1
1
  import type { Db } from "../db.ts";
2
2
  import { inTransaction } from "../db.ts";
3
- import type { AtomicArtifactStore } from "../ports/atomic-artifact-store.ts";
4
- import type { ArtifactEventReader } from "../ports/artifact-event-reader.ts";
5
- import type { ArtifactTrashStore } from "../ports/artifact-trash-store.ts";
6
3
  import type {
7
4
  Artifact,
8
5
  ArtifactEdge,
@@ -31,6 +28,9 @@ import {
31
28
  updateExtra,
32
29
  updateStatus,
33
30
  } from "../ops.ts";
31
+ import type { ArtifactEventReader } from "../ports/artifact-event-reader.ts";
32
+ import type { ArtifactTrashStore } from "../ports/artifact-trash-store.ts";
33
+ import type { AtomicArtifactStore } from "../ports/atomic-artifact-store.ts";
34
34
 
35
35
  export class SQLiteArtifactStore implements AtomicArtifactStore, ArtifactTrashStore, ArtifactEventReader {
36
36
  constructor(private readonly db: Db) {}
@@ -115,7 +115,8 @@ export class SQLiteArtifactStore implements AtomicArtifactStore, ArtifactTrashSt
115
115
  limit = "LIMIT ?";
116
116
  parameters.push(filter.limit);
117
117
  }
118
- return this.db.prepare(`
118
+ return this.db
119
+ .prepare(`
119
120
  SELECT edges.from_id AS "from", edges.relation, edges.to_id AS "to"
120
121
  FROM edges
121
122
  JOIN artifacts AS source ON source.id = edges.from_id
@@ -123,6 +124,7 @@ export class SQLiteArtifactStore implements AtomicArtifactStore, ArtifactTrashSt
123
124
  ${where}
124
125
  ORDER BY edges.rowid
125
126
  ${limit}
126
- `).all(...parameters) as ArtifactEdge[];
127
+ `)
128
+ .all(...parameters) as ArtifactEdge[];
127
129
  }
128
130
  }
@@ -1,13 +1,13 @@
1
- import type { Db } from "../db.ts";
2
1
  import { DISCUSSION_ROUNDS_DEFAULT_LIMIT, DISCUSSION_ROUNDS_MAX_LIMIT } from "../constants.ts";
2
+ import type { Db } from "../db.ts";
3
3
  import {
4
- validateDiscussionActor,
5
- validateDiscussionContent,
6
- validateDiscussionOptions,
7
4
  type AppendDiscussionRound,
8
5
  type DiscussionOptionsMode,
9
6
  type DiscussionRound,
10
7
  type DiscussionRoundQuery,
8
+ validateDiscussionActor,
9
+ validateDiscussionContent,
10
+ validateDiscussionOptions,
11
11
  } from "../domain/discussion.ts";
12
12
  import type { DiscussionRoundStore } from "../ports/discussion-round-store.ts";
13
13
 
@@ -48,19 +48,26 @@ export class SQLiteDiscussionRoundStore implements DiscussionRoundStore {
48
48
  // selected isn't validated here -- it requires cross-referencing the Discussion's
49
49
  // currently pending options (extra.discussion), which this store, deliberately scoped to
50
50
  // the rounds table alone, has no access to. discussion-service.ts validates it beforehand.
51
- const posed = round.options !== undefined || round.optionsMode !== undefined
52
- ? validateDiscussionOptions(round.options ?? [], round.optionsMode ?? "", round.optionDescriptions)
53
- : undefined;
54
- const result = this.db.prepare(`
51
+ const posed =
52
+ round.options !== undefined || round.optionsMode !== undefined
53
+ ? validateDiscussionOptions(round.options ?? [], round.optionsMode ?? "", round.optionDescriptions)
54
+ : undefined;
55
+ const result = this.db
56
+ .prepare(`
55
57
  INSERT INTO discussion_rounds (discussion_id, round_number, actor, content, occurred_at, event_schema_version, options, options_mode, selected, option_descriptions)
56
58
  VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
57
- `).run(
58
- round.discussionId, round.roundNumber, actor, content, occurredAt,
59
- posed ? JSON.stringify(posed.options) : null,
60
- posed ? posed.mode : null,
61
- round.selected !== undefined ? JSON.stringify(round.selected) : null,
62
- posed?.optionDescriptions ? JSON.stringify(posed.optionDescriptions) : null,
63
- );
59
+ `)
60
+ .run(
61
+ round.discussionId,
62
+ round.roundNumber,
63
+ actor,
64
+ content,
65
+ occurredAt,
66
+ posed ? JSON.stringify(posed.options) : null,
67
+ posed ? posed.mode : null,
68
+ round.selected !== undefined ? JSON.stringify(round.selected) : null,
69
+ posed?.optionDescriptions ? JSON.stringify(posed.optionDescriptions) : null,
70
+ );
64
71
  return {
65
72
  id: Number(result.lastInsertRowid),
66
73
  discussionId: round.discussionId,
@@ -76,13 +83,15 @@ export class SQLiteDiscussionRoundStore implements DiscussionRoundStore {
76
83
 
77
84
  list(query: DiscussionRoundQuery): DiscussionRound[] {
78
85
  const limit = Math.min(DISCUSSION_ROUNDS_MAX_LIMIT, Math.max(1, Math.floor(query.limit ?? DISCUSSION_ROUNDS_DEFAULT_LIMIT)));
79
- const rows = this.db.prepare(`
86
+ const rows = this.db
87
+ .prepare(`
80
88
  SELECT id, discussion_id, round_number, actor, content, occurred_at, options, options_mode, selected, option_descriptions
81
89
  FROM discussion_rounds
82
90
  WHERE discussion_id = ? AND round_number > ?
83
91
  ORDER BY round_number ASC
84
92
  LIMIT ?
85
- `).all(query.discussionId, query.afterRound ?? 0, limit) as DiscussionRoundRow[];
93
+ `)
94
+ .all(query.discussionId, query.afterRound ?? 0, limit) as DiscussionRoundRow[];
86
95
  return rows.map(mapRow);
87
96
  }
88
97
 
@@ -1,7 +1,7 @@
1
1
  import type { Db } from "../db.ts";
2
2
  import type { GateResult, GateRunOptions } from "../domain/gate.ts";
3
- import type { GateRunner } from "../ports/gate-runner.ts";
4
3
  import { runGates, runGatesAsync } from "../ops.ts";
4
+ import type { GateRunner } from "../ports/gate-runner.ts";
5
5
 
6
6
  export class SQLiteGateRunner implements GateRunner {
7
7
  constructor(private readonly db: Db) {}
@@ -7,35 +7,39 @@ export class SQLiteGraphProjectionStore implements GraphProjectionStore {
7
7
  constructor(private readonly db: Db) {}
8
8
 
9
9
  getCheckpoint(producerId: string): ProjectionCheckpoint | null {
10
- const row = this.db.prepare("SELECT producer_id, last_sequence, last_batch_id, applied_at FROM graph_projection_checkpoints WHERE producer_id = ?").get(producerId) as
11
- | { producer_id: string; last_sequence: number; last_batch_id: string; applied_at: string }
12
- | null;
10
+ const row = this.db
11
+ .prepare("SELECT producer_id, last_sequence, last_batch_id, applied_at FROM graph_projection_checkpoints WHERE producer_id = ?")
12
+ .get(producerId) as { producer_id: string; last_sequence: number; last_batch_id: string; applied_at: string } | null;
13
13
  if (row == null) return null;
14
14
  return { producerId: row.producer_id, lastSequence: row.last_sequence, lastBatchId: row.last_batch_id, appliedAt: row.applied_at };
15
15
  }
16
16
 
17
17
  resolveIdentity(producerId: string, externalId: string): string | undefined {
18
- const row = this.db.prepare("SELECT artifact_id FROM graph_projection_identities WHERE producer_id = ? AND external_id = ?").get(producerId, externalId) as
19
- | { artifact_id: string }
20
- | null;
18
+ const row = this.db
19
+ .prepare("SELECT artifact_id FROM graph_projection_identities WHERE producer_id = ? AND external_id = ?")
20
+ .get(producerId, externalId) as { artifact_id: string } | null;
21
21
  return row?.artifact_id;
22
22
  }
23
23
 
24
24
  recordIdentity(producerId: string, externalId: string, artifactId: string): void {
25
25
  inTransaction(this.db, () => {
26
- this.db.prepare(`
26
+ this.db
27
+ .prepare(`
27
28
  INSERT INTO graph_projection_identities (producer_id, external_id, artifact_id) VALUES (?, ?, ?)
28
29
  ON CONFLICT(producer_id, external_id) DO UPDATE SET artifact_id = excluded.artifact_id
29
- `).run(producerId, externalId, artifactId);
30
+ `)
31
+ .run(producerId, externalId, artifactId);
30
32
  });
31
33
  }
32
34
 
33
35
  commitCheckpoint(checkpoint: ProjectionCheckpoint): void {
34
36
  inTransaction(this.db, () => {
35
- this.db.prepare(`
37
+ this.db
38
+ .prepare(`
36
39
  INSERT INTO graph_projection_checkpoints (producer_id, last_sequence, last_batch_id, applied_at) VALUES (?, ?, ?, ?)
37
40
  ON CONFLICT(producer_id) DO UPDATE SET last_sequence = excluded.last_sequence, last_batch_id = excluded.last_batch_id, applied_at = excluded.applied_at
38
- `).run(checkpoint.producerId, checkpoint.lastSequence, checkpoint.lastBatchId, checkpoint.appliedAt);
41
+ `)
42
+ .run(checkpoint.producerId, checkpoint.lastSequence, checkpoint.lastBatchId, checkpoint.appliedAt);
39
43
  });
40
44
  }
41
45
  }
@@ -43,34 +43,51 @@ export class SQLiteLogStore implements LogStore {
43
43
  constructor(private readonly db: Db) {}
44
44
 
45
45
  ensureSource(sourceId: string, label: string, projectRoot: string | null): LogSource {
46
- const existing = this.db.prepare("SELECT id, label, project_root, created_at FROM log_sources WHERE id = ?").get(sourceId) as SourceRow | undefined;
46
+ const existing = this.db.prepare("SELECT id, label, project_root, created_at FROM log_sources WHERE id = ?").get(sourceId) as
47
+ | SourceRow
48
+ | undefined;
47
49
  if (existing) return toSource(existing);
48
50
  const createdAt = new Date().toISOString();
49
- this.db.prepare("INSERT INTO log_sources (id, label, project_root, created_at) VALUES (?, ?, ?, ?)").run(sourceId, label, projectRoot, createdAt);
51
+ this.db
52
+ .prepare("INSERT INTO log_sources (id, label, project_root, created_at) VALUES (?, ?, ?, ?)")
53
+ .run(sourceId, label, projectRoot, createdAt);
50
54
  return { id: sourceId, label, projectRoot, createdAt };
51
55
  }
52
56
 
53
57
  findEntryByOperationId(sourceId: string, operationId: string): LogEntry | undefined {
54
- const row = this.db.prepare(
55
- "SELECT id, source_id, occurred_at, level, message, truncated, fields_json, operation_id, session_id FROM log_entries WHERE source_id = ? AND operation_id = ?",
56
- ).get(sourceId, operationId) as EntryRow | undefined;
58
+ const row = this.db
59
+ .prepare(
60
+ "SELECT id, source_id, occurred_at, level, message, truncated, fields_json, operation_id, session_id FROM log_entries WHERE source_id = ? AND operation_id = ?",
61
+ )
62
+ .get(sourceId, operationId) as EntryRow | undefined;
57
63
  return row ? toEntry(row) : undefined;
58
64
  }
59
65
 
60
66
  insertEntry(entry: LogEntry): void {
61
- this.db.prepare(
62
- `INSERT INTO log_entries (id, source_id, occurred_at, level, message, truncated, fields_json, operation_id, session_id)
67
+ this.db
68
+ .prepare(
69
+ `INSERT INTO log_entries (id, source_id, occurred_at, level, message, truncated, fields_json, operation_id, session_id)
63
70
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
64
- ).run(
65
- entry.id, entry.sourceId, entry.occurredAt, entry.level, entry.message,
66
- entry.truncated ? 1 : 0, JSON.stringify(entry.fields), entry.operationId, entry.sessionId ?? null,
67
- );
71
+ )
72
+ .run(
73
+ entry.id,
74
+ entry.sourceId,
75
+ entry.occurredAt,
76
+ entry.level,
77
+ entry.message,
78
+ entry.truncated ? 1 : 0,
79
+ JSON.stringify(entry.fields),
80
+ entry.operationId,
81
+ entry.sessionId ?? null,
82
+ );
68
83
  }
69
84
 
70
85
  entriesForSource(sourceId: string): readonly LogEntry[] {
71
- const rows = this.db.prepare(
72
- "SELECT id, source_id, occurred_at, level, message, truncated, fields_json, operation_id, session_id FROM log_entries WHERE source_id = ? ORDER BY occurred_at, id",
73
- ).all(sourceId) as EntryRow[];
86
+ const rows = this.db
87
+ .prepare(
88
+ "SELECT id, source_id, occurred_at, level, message, truncated, fields_json, operation_id, session_id FROM log_entries WHERE source_id = ? ORDER BY occurred_at, id",
89
+ )
90
+ .all(sourceId) as EntryRow[];
74
91
  return rows.map(toEntry);
75
92
  }
76
93
 
@@ -78,11 +95,13 @@ export class SQLiteLogStore implements LogStore {
78
95
  const countRow = this.db.prepare("SELECT COUNT(*) as count FROM log_entries WHERE source_id = ?").get(sourceId) as { count: number };
79
96
  const excess = countRow.count - maxEntries;
80
97
  if (excess <= 0) return 0;
81
- this.db.prepare(
82
- `DELETE FROM log_entries WHERE id IN (
98
+ this.db
99
+ .prepare(
100
+ `DELETE FROM log_entries WHERE id IN (
83
101
  SELECT id FROM log_entries WHERE source_id = ? ORDER BY occurred_at, id LIMIT ?
84
102
  )`,
85
- ).run(sourceId, excess);
103
+ )
104
+ .run(sourceId, excess);
86
105
  return excess;
87
106
  }
88
107
  }
@@ -1,12 +1,12 @@
1
1
  import type { Db } from "../db.ts";
2
2
  import {
3
- normalizeNoteHistoryQuery,
4
- validateNoteEvent,
5
3
  type AppendNoteEvent,
6
4
  type NoteEvent,
7
5
  type NoteEventType,
8
6
  type NoteHistoryPage,
9
7
  type NoteHistoryQuery,
8
+ normalizeNoteHistoryQuery,
9
+ validateNoteEvent,
10
10
  } from "../domain/note-event.ts";
11
11
  import type { NoteEventStore } from "../ports/note-event-store.ts";
12
12
 
@@ -45,21 +45,23 @@ export class SQLiteNoteEventStore implements NoteEventStore {
45
45
 
46
46
  append(input: AppendNoteEvent): NoteEvent {
47
47
  const event = validateNoteEvent(input);
48
- const result = this.db.prepare(`
48
+ const result = this.db
49
+ .prepare(`
49
50
  INSERT INTO note_events (
50
51
  note_id, occurred_at, event_type, actor, source, session_id, reason, related_id, disposition, event_schema_version
51
52
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
52
- `).run(
53
- event.noteId,
54
- new Date().toISOString(),
55
- event.type,
56
- event.actor,
57
- event.source,
58
- event.sessionId ?? null,
59
- event.reason ?? null,
60
- event.relatedId ?? null,
61
- event.disposition ?? null,
62
- );
53
+ `)
54
+ .run(
55
+ event.noteId,
56
+ new Date().toISOString(),
57
+ event.type,
58
+ event.actor,
59
+ event.source,
60
+ event.sessionId ?? null,
61
+ event.reason ?? null,
62
+ event.relatedId ?? null,
63
+ event.disposition ?? null,
64
+ );
63
65
  return mapRow(this.db.prepare("SELECT * FROM note_events WHERE id = ?").get(result.lastInsertRowid) as NoteEventRow);
64
66
  }
65
67
 
@@ -67,12 +69,14 @@ export class SQLiteNoteEventStore implements NoteEventStore {
67
69
  const { limit, direction, cursor } = normalizeNoteHistoryQuery(query);
68
70
  const comparator = direction === "desc" ? "<" : ">";
69
71
  const order = direction === "desc" ? "DESC" : "ASC";
70
- const rows = this.db.prepare(`
72
+ const rows = this.db
73
+ .prepare(`
71
74
  SELECT * FROM note_events
72
75
  WHERE note_id = ? ${cursor === undefined ? "" : `AND id ${comparator} ?`}
73
76
  ORDER BY occurred_at ${order}, id ${order}
74
77
  LIMIT ?
75
- `).all(...(cursor === undefined ? [noteId, limit + 1] : [noteId, cursor, limit + 1])) as NoteEventRow[];
78
+ `)
79
+ .all(...(cursor === undefined ? [noteId, limit + 1] : [noteId, cursor, limit + 1])) as NoteEventRow[];
76
80
  const hasMore = rows.length > limit;
77
81
  const events = rows.slice(0, limit).map(mapRow);
78
82
  return { events, ...(hasMore ? { nextCursor: events.at(-1)!.id } : {}) };
@@ -7,20 +7,24 @@ export class SQLiteSessionIdentityStore implements SessionIdentityStore {
7
7
  constructor(private readonly db: Db) {}
8
8
 
9
9
  find(sessionId: string): SessionIdentityRecord | undefined {
10
- const row = this.db.prepare("SELECT session_id, secret_hash, registered_at, last_seen_at FROM session_identities WHERE session_id = ?").get(sessionId) as
11
- | { session_id: string; secret_hash: string; registered_at: string; last_seen_at: string }
12
- | null;
13
- return row ? { sessionId: row.session_id, secretHash: row.secret_hash, registeredAt: row.registered_at, lastSeenAt: row.last_seen_at } : undefined;
10
+ const row = this.db
11
+ .prepare("SELECT session_id, secret_hash, registered_at, last_seen_at FROM session_identities WHERE session_id = ?")
12
+ .get(sessionId) as { session_id: string; secret_hash: string; registered_at: string; last_seen_at: string } | null;
13
+ return row
14
+ ? { sessionId: row.session_id, secretHash: row.secret_hash, registeredAt: row.registered_at, lastSeenAt: row.last_seen_at }
15
+ : undefined;
14
16
  }
15
17
 
16
18
  upsert(record: SessionIdentityRecord): void {
17
19
  inTransaction(this.db, () => {
18
20
  this.evictOldestBeyondCap(record.sessionId);
19
- this.db.prepare(`
21
+ this.db
22
+ .prepare(`
20
23
  INSERT INTO session_identities (session_id, secret_hash, registered_at, last_seen_at)
21
24
  VALUES (?, ?, ?, ?)
22
25
  ON CONFLICT(session_id) DO UPDATE SET secret_hash = excluded.secret_hash, registered_at = excluded.registered_at, last_seen_at = excluded.last_seen_at
23
- `).run(record.sessionId, record.secretHash, record.registeredAt, record.lastSeenAt);
26
+ `)
27
+ .run(record.sessionId, record.secretHash, record.registeredAt, record.lastSeenAt);
24
28
  });
25
29
  }
26
30
 
@@ -41,6 +45,8 @@ export class SQLiteSessionIdentityStore implements SessionIdentityStore {
41
45
  const exists = this.db.prepare("SELECT 1 FROM session_identities WHERE session_id = ?").get(sessionId);
42
46
  if (exists) return;
43
47
  if (this.count() < SESSION_IDENTITY_MAX_ROWS) return;
44
- this.db.exec("DELETE FROM session_identities WHERE session_id = (SELECT session_id FROM session_identities ORDER BY last_seen_at ASC LIMIT 1)");
48
+ this.db.exec(
49
+ "DELETE FROM session_identities WHERE session_id = (SELECT session_id FROM session_identities ORDER BY last_seen_at ASC LIMIT 1)",
50
+ );
45
51
  }
46
52
  }
@@ -1,10 +1,9 @@
1
1
  import type { Db } from "../db.ts";
2
2
  import { inTransaction } from "../db.ts";
3
3
  import {
4
+ type AppendTaskEvent,
4
5
  normalizeTaskEventFeedQuery,
5
6
  normalizeTaskHistoryQuery,
6
- validateTaskEvent,
7
- type AppendTaskEvent,
8
7
  type TaskEvent,
9
8
  type TaskEventEvidence,
10
9
  type TaskEventFeedPage,
@@ -13,6 +12,7 @@ import {
13
12
  type TaskHistoryPage,
14
13
  type TaskHistoryQuery,
15
14
  type TaskLifecycleStatus,
15
+ validateTaskEvent,
16
16
  } from "../domain/task-event.ts";
17
17
  import type { TaskEventStore } from "../ports/task-event-store.ts";
18
18
 
@@ -53,28 +53,32 @@ function mapRow(row: TaskEventRow): TaskEvent {
53
53
  export class SQLiteTaskEventStore implements TaskEventStore {
54
54
  constructor(private readonly db: Db) {}
55
55
 
56
- atomic<T>(operation: () => T): T { return inTransaction(this.db, operation); }
56
+ atomic<T>(operation: () => T): T {
57
+ return inTransaction(this.db, operation);
58
+ }
57
59
 
58
60
  append(input: AppendTaskEvent): TaskEvent {
59
61
  const event = validateTaskEvent(input);
60
- const result = this.db.prepare(`
62
+ const result = this.db
63
+ .prepare(`
61
64
  INSERT INTO task_events (
62
65
  task_id, occurred_at, event_type, actor, source, session_id, reason,
63
66
  from_status, to_status, attempt_id, evidence_json, event_schema_version
64
67
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
65
- `).run(
66
- event.taskId,
67
- new Date().toISOString(),
68
- event.type,
69
- event.actor,
70
- event.source,
71
- event.sessionId ?? null,
72
- event.reason ?? null,
73
- event.fromStatus ?? null,
74
- event.toStatus ?? null,
75
- event.attemptId ?? null,
76
- event.evidence === undefined ? null : JSON.stringify(event.evidence),
77
- );
68
+ `)
69
+ .run(
70
+ event.taskId,
71
+ new Date().toISOString(),
72
+ event.type,
73
+ event.actor,
74
+ event.source,
75
+ event.sessionId ?? null,
76
+ event.reason ?? null,
77
+ event.fromStatus ?? null,
78
+ event.toStatus ?? null,
79
+ event.attemptId ?? null,
80
+ event.evidence === undefined ? null : JSON.stringify(event.evidence),
81
+ );
78
82
  return mapRow(this.db.prepare("SELECT * FROM task_events WHERE id = ?").get(result.lastInsertRowid) as TaskEventRow);
79
83
  }
80
84
 
@@ -82,12 +86,14 @@ export class SQLiteTaskEventStore implements TaskEventStore {
82
86
  const { limit, direction, cursor } = normalizeTaskHistoryQuery(query);
83
87
  const comparator = direction === "desc" ? "<" : ">";
84
88
  const order = direction === "desc" ? "DESC" : "ASC";
85
- const rows = this.db.prepare(`
89
+ const rows = this.db
90
+ .prepare(`
86
91
  SELECT * FROM task_events
87
92
  WHERE task_id = ? ${cursor === undefined ? "" : `AND id ${comparator} ?`}
88
93
  ORDER BY occurred_at ${order}, id ${order}
89
94
  LIMIT ?
90
- `).all(...(cursor === undefined ? [taskId, limit + 1] : [taskId, cursor, limit + 1])) as TaskEventRow[];
95
+ `)
96
+ .all(...(cursor === undefined ? [taskId, limit + 1] : [taskId, cursor, limit + 1])) as TaskEventRow[];
91
97
  const hasMore = rows.length > limit;
92
98
  const events = rows.slice(0, limit).map(mapRow);
93
99
  return { events, ...(hasMore ? { nextCursor: events.at(-1)!.id } : {}) };
@@ -100,12 +106,14 @@ export class SQLiteTaskEventStore implements TaskEventStore {
100
106
  if (cursor !== undefined) params.push(cursor);
101
107
  if (eventTypes) params.push(...eventTypes);
102
108
  params.push(limit + 1);
103
- const rows = this.db.prepare(`
109
+ const rows = this.db
110
+ .prepare(`
104
111
  SELECT * FROM task_events
105
112
  WHERE 1=1 ${cursor === undefined ? "" : "AND id > ?"} ${typeFilter}
106
113
  ORDER BY id ASC
107
114
  LIMIT ?
108
- `).all(...params) as TaskEventRow[];
115
+ `)
116
+ .all(...params) as TaskEventRow[];
109
117
  const hasMore = rows.length > limit;
110
118
  const events = rows.slice(0, limit).map(mapRow);
111
119
  return { events, ...(hasMore ? { nextCursor: events.at(-1)!.id } : {}) };
@@ -7,15 +7,33 @@ export class SQLiteTaskFocusStore implements TaskFocusStore {
7
7
  constructor(private readonly db: Db) {}
8
8
 
9
9
  get(scope?: string): TaskFocusState | undefined {
10
- const row = this.db.prepare("SELECT task_id, status, pause_reason, updated_at FROM task_focus WHERE scope = ?").get(normalizeFocusScope(scope)) as
11
- | { task_id: string; status: TaskFocusStatus; pause_reason: string | null; updated_at: string }
12
- | null;
13
- return row ? { taskId: row.task_id, status: row.status, updatedAt: row.updated_at, ...(row.pause_reason ? { pauseReason: row.pause_reason } : {}) } : undefined;
10
+ const row = this.db
11
+ .prepare("SELECT task_id, status, pause_reason, updated_at FROM task_focus WHERE scope = ?")
12
+ .get(normalizeFocusScope(scope)) as {
13
+ task_id: string;
14
+ status: TaskFocusStatus;
15
+ pause_reason: string | null;
16
+ updated_at: string;
17
+ } | null;
18
+ return row
19
+ ? {
20
+ taskId: row.task_id,
21
+ status: row.status,
22
+ updatedAt: row.updated_at,
23
+ ...(row.pause_reason ? { pauseReason: row.pause_reason } : {}),
24
+ }
25
+ : undefined;
14
26
  }
15
27
 
16
- set(taskId: string, scope?: string): TaskFocusState { return this.write(taskId, "active", scope); }
17
- pause(taskId: string, reason?: string, scope?: string): TaskFocusState { return this.transition(taskId, "active", "paused", reason, scope); }
18
- unpause(taskId: string, scope?: string): TaskFocusState { return this.transition(taskId, "paused", "active", undefined, scope); }
28
+ set(taskId: string, scope?: string): TaskFocusState {
29
+ return this.write(taskId, "active", scope);
30
+ }
31
+ pause(taskId: string, reason?: string, scope?: string): TaskFocusState {
32
+ return this.transition(taskId, "active", "paused", reason, scope);
33
+ }
34
+ unpause(taskId: string, scope?: string): TaskFocusState {
35
+ return this.transition(taskId, "paused", "active", undefined, scope);
36
+ }
19
37
 
20
38
  clear(taskId?: string, scope?: string): void {
21
39
  const key = normalizeFocusScope(scope);
@@ -35,7 +53,13 @@ export class SQLiteTaskFocusStore implements TaskFocusStore {
35
53
  return this.db.prepare("DELETE FROM task_focus WHERE updated_at < ?").run(olderThanIso).changes;
36
54
  }
37
55
 
38
- private transition(taskId: string, expected: TaskFocusStatus, status: TaskFocusStatus, reason: string | undefined, scope: string | undefined): TaskFocusState {
56
+ private transition(
57
+ taskId: string,
58
+ expected: TaskFocusStatus,
59
+ status: TaskFocusStatus,
60
+ reason: string | undefined,
61
+ scope: string | undefined,
62
+ ): TaskFocusState {
39
63
  const current = this.get(scope);
40
64
  if (current?.taskId !== taskId) throw new Error(`task "${taskId}" is not focused`);
41
65
  if (current.status !== expected) throw new Error(`focus is ${current.status}, expected ${expected}`);
@@ -56,11 +80,13 @@ export class SQLiteTaskFocusStore implements TaskFocusStore {
56
80
  const updatedAt = new Date().toISOString();
57
81
  inTransaction(this.db, () => {
58
82
  this.evictOldestBeyondCap(key);
59
- this.db.prepare(`
83
+ this.db
84
+ .prepare(`
60
85
  INSERT INTO task_focus (scope, task_id, status, pause_reason, updated_at)
61
86
  VALUES (?, ?, ?, ?, ?)
62
87
  ON CONFLICT(scope) DO UPDATE SET task_id = excluded.task_id, status = excluded.status, pause_reason = excluded.pause_reason, updated_at = excluded.updated_at
63
- `).run(key, taskId, status, pauseReason ?? null, updatedAt);
88
+ `)
89
+ .run(key, taskId, status, pauseReason ?? null, updatedAt);
64
90
  });
65
91
  return { taskId, status, updatedAt, ...(pauseReason ? { pauseReason } : {}) };
66
92
  }
@@ -1,7 +1,7 @@
1
1
  import { TASK_LEASE_DEFAULT_TTL_MS } from "../constants.ts";
2
2
  import type { Db } from "../db.ts";
3
3
  import { inTransaction } from "../db.ts";
4
- import { isLeaseExpired, validateLeaseNote, validateLeaseOwner, validateLeaseTtlMs, type TaskLease } from "../domain/task-lease.ts";
4
+ import { isLeaseExpired, type TaskLease, validateLeaseNote, validateLeaseOwner, validateLeaseTtlMs } from "../domain/task-lease.ts";
5
5
  import type { TaskLeaseStore } from "../ports/task-lease-store.ts";
6
6
 
7
7
  interface TaskLeaseRow {
@@ -49,12 +49,14 @@ export class SQLiteTaskLeaseStore implements TaskLeaseStore {
49
49
  const token = renewingSameOwner ? current!.token : crypto.randomUUID();
50
50
  const claimedAt = renewingSameOwner ? current!.claimedAt : nowIso;
51
51
  const leaseExpiresAt = new Date(now.getTime() + ttlMs).toISOString();
52
- this.db.prepare(`
52
+ this.db
53
+ .prepare(`
53
54
  INSERT INTO task_leases (task_id, owner, token, claimed_at, lease_expires_at, heartbeat_at, note)
54
55
  VALUES (?, ?, ?, ?, ?, NULL, ?)
55
56
  ON CONFLICT(task_id) DO UPDATE SET owner = excluded.owner, token = excluded.token, claimed_at = excluded.claimed_at,
56
57
  lease_expires_at = excluded.lease_expires_at, heartbeat_at = NULL, note = excluded.note
57
- `).run(taskId, owner, token, claimedAt, leaseExpiresAt, note ?? null);
58
+ `)
59
+ .run(taskId, owner, token, claimedAt, leaseExpiresAt, note ?? null);
58
60
  return fromRow(this.row(taskId)!);
59
61
  });
60
62
  }
@@ -66,9 +68,12 @@ export class SQLiteTaskLeaseStore implements TaskLeaseStore {
66
68
  const existing = this.row(taskId);
67
69
  const current = existing ? fromRow(existing) : undefined;
68
70
  if (!current || isLeaseExpired(current, nowIso)) throw new Error(`task "${taskId}" has no live lease to renew`);
69
- if (current.owner !== owner || current.token !== token) throw new Error(`lease for task "${taskId}" is held by a different owner/token`);
71
+ if (current.owner !== owner || current.token !== token)
72
+ throw new Error(`lease for task "${taskId}" is held by a different owner/token`);
70
73
  const leaseExpiresAt = new Date(Date.now() + ttlMs).toISOString();
71
- this.db.prepare("UPDATE task_leases SET lease_expires_at = ?, heartbeat_at = ? WHERE task_id = ?").run(leaseExpiresAt, nowIso, taskId);
74
+ this.db
75
+ .prepare("UPDATE task_leases SET lease_expires_at = ?, heartbeat_at = ? WHERE task_id = ?")
76
+ .run(leaseExpiresAt, nowIso, taskId);
72
77
  return fromRow(this.row(taskId)!);
73
78
  });
74
79
  }
@@ -79,7 +84,8 @@ export class SQLiteTaskLeaseStore implements TaskLeaseStore {
79
84
  const existing = this.row(taskId);
80
85
  const current = existing ? fromRow(existing) : undefined;
81
86
  if (!current || isLeaseExpired(current, nowIso)) return { released: false };
82
- if (current.owner !== owner || current.token !== token) throw new Error(`lease for task "${taskId}" is held by a different owner/token`);
87
+ if (current.owner !== owner || current.token !== token)
88
+ throw new Error(`lease for task "${taskId}" is held by a different owner/token`);
83
89
  this.db.prepare("DELETE FROM task_leases WHERE task_id = ?").run(taskId);
84
90
  return { released: true };
85
91
  });