@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.
- package/package.json +2 -2
- package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
- package/src/adapters/sqlite-artifact-store.ts +7 -5
- package/src/adapters/sqlite-discussion-round-store.ts +26 -17
- package/src/adapters/sqlite-gate-runner.ts +1 -1
- package/src/adapters/sqlite-graph-projection-store.ts +14 -10
- package/src/adapters/sqlite-log-store.ts +36 -17
- package/src/adapters/sqlite-note-event-store.ts +20 -16
- package/src/adapters/sqlite-session-identity-store.ts +13 -7
- package/src/adapters/sqlite-task-event-store.ts +29 -21
- package/src/adapters/sqlite-task-focus-store.ts +36 -10
- package/src/adapters/sqlite-task-lease-store.ts +12 -6
- package/src/adapters/sqlite-task-scope-store.ts +25 -14
- package/src/artifact-relationship-view.ts +3 -3
- package/src/artifact-subtree.ts +4 -2
- package/src/authority-registry.ts +2 -1
- package/src/cli.ts +785 -179
- package/src/client.ts +46 -20
- package/src/constants.ts +15 -4
- package/src/daemon-state.ts +4 -12
- package/src/daemon.ts +31 -9
- package/src/db.ts +119 -98
- package/src/discussion-service.ts +109 -44
- package/src/domain/artifact-event.ts +17 -4
- package/src/domain/artifact.ts +3 -1
- package/src/domain/blueprint-definition.ts +26 -31
- package/src/domain/checklist.ts +20 -17
- package/src/domain/discussion.ts +37 -18
- package/src/domain/gate.ts +7 -7
- package/src/domain/log-entry.ts +1 -1
- package/src/domain/note-event.ts +20 -7
- package/src/domain/task-event.ts +17 -7
- package/src/domain-services.ts +241 -110
- package/src/graph-projection-service.ts +34 -8
- package/src/id-migration.ts +17 -4
- package/src/index.ts +16 -11
- package/src/log-service.ts +6 -5
- package/src/log.ts +19 -0
- package/src/modules/discuss.ts +63 -28
- package/src/modules/docs.ts +74 -17
- package/src/modules/graph-projection.ts +20 -9
- package/src/modules/logs.ts +33 -21
- package/src/modules/notes.ts +66 -28
- package/src/modules/playbooks.ts +88 -29
- package/src/modules/rules.ts +57 -15
- package/src/modules/session-identity.ts +6 -2
- package/src/modules/tasks.ts +142 -67
- package/src/note-service.ts +11 -7
- package/src/ops.ts +131 -68
- package/src/playbook-definition.ts +56 -17
- package/src/playbook-execution.ts +13 -3
- package/src/ports/note-event-store.ts +6 -4
- package/src/ports/task-event-store.ts +9 -6
- package/src/ports/task-focus-store.ts +17 -4
- package/src/ports/task-lease-store.ts +9 -4
- package/src/ports/task-scope-store.ts +3 -1
- package/src/service.ts +143 -89
- package/src/session-identity-service.ts +10 -2
- package/src/task-context.ts +28 -16
- package/src/task-execution.ts +4 -12
- package/src/task-graph-view.ts +12 -12
- package/src/task-relationship-view.ts +1 -3
- package/src/task-service.ts +167 -72
- package/src/vehicle/artifact-trash-vehicle.ts +25 -13
- package/src/vehicle/artifact-vehicle-shared.ts +28 -7
- package/src/vehicle/docs-vehicle.ts +48 -16
- package/src/vehicle/notes-vehicle.ts +24 -6
- package/src/vehicle/papyrus-vehicle.ts +14 -3
- package/src/vehicle/playbooks-vehicle.ts +87 -18
- package/src/vehicle/rules-vehicle.ts +58 -21
- package/src/vehicle/tasks-vehicle.ts +366 -54
- package/src/version.ts +1 -1
- 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.
|
|
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.
|
|
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
|
|
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
|
-
`)
|
|
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
|
-
|
|
26
|
-
| null;
|
|
27
|
-
|
|
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 =
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
`)
|
|
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 =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
`)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
`)
|
|
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
|
|
11
|
-
|
|
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
|
|
19
|
-
|
|
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
|
|
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
|
-
`)
|
|
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
|
|
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
|
-
`)
|
|
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
|
|
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
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
`)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
`)
|
|
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
|
|
11
|
-
|
|
12
|
-
| null;
|
|
13
|
-
return row
|
|
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
|
|
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
|
-
`)
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
-
`)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
`)
|
|
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
|
|
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
|
-
`)
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|
|
17
|
-
|
|
18
|
-
|
|
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(
|
|
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
|
|
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
|
-
`)
|
|
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
|
|
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
|
|
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
|
-
`)
|
|
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)
|
|
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
|
|
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)
|
|
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
|
});
|