@sixb/pg 0.1.20 → 0.1.22

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 (139) hide show
  1. package/README.md +1 -0
  2. package/dist/agents/agent-storage.d.ts +8 -2
  3. package/dist/agents/agent-storage.d.ts.map +1 -1
  4. package/dist/agents/approvals.d.ts +48 -0
  5. package/dist/agents/approvals.d.ts.map +1 -0
  6. package/dist/agents/checkpoints.d.ts +1 -0
  7. package/dist/agents/checkpoints.d.ts.map +1 -1
  8. package/dist/agents/index.d.ts +1 -0
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/agents/memory.d.ts +19 -0
  11. package/dist/agents/memory.d.ts.map +1 -0
  12. package/dist/agents/messages.d.ts.map +1 -1
  13. package/dist/agents/rows.d.ts +15 -1
  14. package/dist/agents/rows.d.ts.map +1 -1
  15. package/dist/agents/runs.d.ts +3 -1
  16. package/dist/agents/runs.d.ts.map +1 -1
  17. package/dist/agents/settings.d.ts +29 -0
  18. package/dist/agents/settings.d.ts.map +1 -0
  19. package/dist/agents/threads.d.ts +10 -1
  20. package/dist/agents/threads.d.ts.map +1 -1
  21. package/dist/ai-cost-groups.d.ts.map +1 -1
  22. package/dist/auth-storage/auth-storage.d.ts +3 -2
  23. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  24. package/dist/auth-storage/sessions.d.ts +2 -1
  25. package/dist/auth-storage/sessions.d.ts.map +1 -1
  26. package/dist/auth-storage/users.d.ts +8 -4
  27. package/dist/auth-storage/users.d.ts.map +1 -1
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6001 -2923
  31. package/dist/index.js.map +45 -37
  32. package/dist/migrations.d.ts.map +1 -1
  33. package/dist/objects/conditional-reader.d.ts +17 -0
  34. package/dist/objects/conditional-reader.d.ts.map +1 -0
  35. package/dist/objects/query-compiler.d.ts +27 -1
  36. package/dist/objects/query-compiler.d.ts.map +1 -1
  37. package/dist/objects/query-indexes.d.ts.map +1 -1
  38. package/dist/objects/read-scope.d.ts +10 -1
  39. package/dist/objects/read-scope.d.ts.map +1 -1
  40. package/dist/objects/reader.d.ts +2 -0
  41. package/dist/objects/reader.d.ts.map +1 -1
  42. package/dist/objects/scoped-reader.d.ts +12 -0
  43. package/dist/objects/scoped-reader.d.ts.map +1 -0
  44. package/dist/objects/selected-reader.d.ts +1 -0
  45. package/dist/objects/selected-reader.d.ts.map +1 -1
  46. package/dist/objects/storage.d.ts +7 -1
  47. package/dist/objects/storage.d.ts.map +1 -1
  48. package/dist/ontology-storage/index.d.ts +2 -0
  49. package/dist/ontology-storage/index.d.ts.map +1 -1
  50. package/dist/ontology-storage/materialization-session.d.ts +6 -0
  51. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  52. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  53. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  54. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  55. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
  56. package/dist/ontology-storage/sources.d.ts +2 -1
  57. package/dist/ontology-storage/sources.d.ts.map +1 -1
  58. package/dist/ontology-storage/unique-values.d.ts +60 -0
  59. package/dist/ontology-storage/unique-values.d.ts.map +1 -0
  60. package/dist/pg-action-run-storage.d.ts +4 -0
  61. package/dist/pg-action-run-storage.d.ts.map +1 -1
  62. package/dist/pg-activity-storage.d.ts +22 -0
  63. package/dist/pg-activity-storage.d.ts.map +1 -0
  64. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  65. package/dist/pg-ai-usage-storage.d.ts +8 -1
  66. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  67. package/dist/pg-artifact-storage.d.ts +26 -0
  68. package/dist/pg-artifact-storage.d.ts.map +1 -0
  69. package/dist/pg-execution-storage.d.ts +3 -1
  70. package/dist/pg-execution-storage.d.ts.map +1 -1
  71. package/dist/pg-workflow-run-storage.d.ts +4 -0
  72. package/dist/pg-workflow-run-storage.d.ts.map +1 -1
  73. package/dist/share-grant-storage.d.ts +2 -1
  74. package/dist/share-grant-storage.d.ts.map +1 -1
  75. package/dist/storage-errors.d.ts +2 -0
  76. package/dist/storage-errors.d.ts.map +1 -1
  77. package/package.json +3 -3
  78. package/src/agents/agent-storage.ts +11 -2
  79. package/src/agents/approvals.ts +400 -0
  80. package/src/agents/checkpoints.ts +8 -4
  81. package/src/agents/index.ts +1 -0
  82. package/src/agents/memory.ts +175 -0
  83. package/src/agents/messages.ts +14 -7
  84. package/src/agents/rows.ts +46 -1
  85. package/src/agents/runs.ts +154 -22
  86. package/src/agents/settings.ts +105 -0
  87. package/src/agents/threads.ts +379 -3
  88. package/src/ai-cost-groups.ts +11 -1
  89. package/src/auth-storage/auth-storage.ts +45 -13
  90. package/src/auth-storage/sessions.ts +20 -0
  91. package/src/auth-storage/users.ts +105 -43
  92. package/src/index.ts +10 -0
  93. package/src/migrations/063-artifacts.sql +67 -0
  94. package/src/migrations/064-drop-output-diagnostics.sql +14 -0
  95. package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
  96. package/src/migrations/066-activity.sql +61 -0
  97. package/src/migrations/067-activity-timeline.sql +15 -0
  98. package/src/migrations/068-activity-interactions.sql +20 -0
  99. package/src/migrations/069-artifact-kinds.sql +4 -0
  100. package/src/migrations/070-artifact-state.sql +16 -0
  101. package/src/migrations/071-agent-thread-participants.sql +16 -0
  102. package/src/migrations/072-forked-context-checkpoints.sql +23 -0
  103. package/src/migrations/073-unique-values.sql +25 -0
  104. package/src/migrations/074-generated-media-usage.sql +21 -0
  105. package/src/migrations/075-model-call-files.sql +11 -0
  106. package/src/migrations/076-agent-memory.sql +33 -0
  107. package/src/migrations/077-artifact-threads.sql +21 -0
  108. package/src/migrations/078-agent-approvals.sql +58 -0
  109. package/src/migrations/079-agent-approval-resume.sql +20 -0
  110. package/src/migrations/080-subagent-threads.sql +145 -0
  111. package/src/migrations/081-subagent-questions.sql +6 -0
  112. package/src/migrations.ts +52 -0
  113. package/src/objects/conditional-reader.ts +44 -0
  114. package/src/objects/prepare-queries.ts +1 -1
  115. package/src/objects/query-compiler.ts +96 -33
  116. package/src/objects/query-indexes.ts +29 -10
  117. package/src/objects/read-scope.ts +140 -25
  118. package/src/objects/reader.ts +50 -6
  119. package/src/objects/scoped-reader.ts +101 -0
  120. package/src/objects/selected-reader.ts +16 -76
  121. package/src/objects/storage.ts +24 -5
  122. package/src/objects/text-count-index.ts +1 -0
  123. package/src/ontology-storage/index.ts +3 -0
  124. package/src/ontology-storage/materialization-session.ts +8 -0
  125. package/src/ontology-storage/materialization-writer.ts +76 -1
  126. package/src/ontology-storage/materializations.ts +20 -1
  127. package/src/ontology-storage/replacement-plans.ts +25 -7
  128. package/src/ontology-storage/sources.ts +37 -0
  129. package/src/ontology-storage/unique-values.ts +274 -0
  130. package/src/pg-action-run-storage.ts +23 -0
  131. package/src/pg-activity-storage.ts +486 -0
  132. package/src/pg-ai-cost-storage.ts +62 -12
  133. package/src/pg-ai-usage-storage.ts +90 -1
  134. package/src/pg-artifact-storage.ts +521 -0
  135. package/src/pg-execution-storage.ts +83 -0
  136. package/src/pg-workflow-run-storage.ts +23 -0
  137. package/src/share-grant-storage.ts +21 -0
  138. package/src/storage-errors.ts +7 -0
  139. package/src/transactions.ts +1 -1
@@ -0,0 +1,58 @@
1
+ -- Calls of the Agent that wait for whoever asked for the turn. A call's run id is its key: a
2
+ -- thread holds at most one pending, approved or denied approval per key, and a cancelled,
3
+ -- superseded or expired one lets the same call ask again.
4
+ CREATE TABLE agent_approvals (
5
+ project_id TEXT NOT NULL,
6
+ id TEXT NOT NULL CHECK (length(trim(id)) > 0),
7
+ thread_id TEXT NOT NULL,
8
+ run_id TEXT NOT NULL CHECK (length(run_id) > 0),
9
+ caller_run_id TEXT NOT NULL CHECK (length(caller_run_id) > 0),
10
+ requested_by_type TEXT NOT NULL CHECK (requested_by_type IN ('user', 'serviceAccount', 'system')),
11
+ requested_by_id TEXT NOT NULL CHECK (length(requested_by_id) > 0),
12
+ mode TEXT NOT NULL CHECK (mode IN ('manual', 'accept-edits')),
13
+ call_key TEXT NOT NULL CHECK (length(call_key) > 0),
14
+ call JSONB NOT NULL,
15
+ status TEXT NOT NULL
16
+ CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'superseded', 'expired')),
17
+ created_at TIMESTAMPTZ NOT NULL,
18
+ expires_at TIMESTAMPTZ NOT NULL,
19
+ decided_at TIMESTAMPTZ,
20
+ decided_by_type TEXT CHECK (decided_by_type IN ('user', 'serviceAccount', 'system')),
21
+ decided_by_id TEXT,
22
+ outcome JSONB,
23
+ reported_in_message_id TEXT,
24
+ PRIMARY KEY (project_id, id),
25
+ FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
26
+ );
27
+
28
+ CREATE UNIQUE INDEX idx_agent_approvals_live_call
29
+ ON agent_approvals (project_id, thread_id, call_key)
30
+ WHERE status IN ('pending', 'approved', 'denied');
31
+
32
+ CREATE INDEX idx_agent_approvals_thread ON agent_approvals (project_id, thread_id, created_at);
33
+ CREATE INDEX idx_agent_approvals_run ON agent_approvals (project_id, run_id, status);
34
+ CREATE INDEX idx_agent_approvals_caller_run ON agent_approvals (project_id, caller_run_id, status);
35
+ CREATE INDEX idx_agent_approvals_requester
36
+ ON agent_approvals (project_id, requested_by_type, requested_by_id, status);
37
+
38
+ -- What each user lets the Agent change without asking: a default, then their own choice in a
39
+ -- thread. Absent rows fall back, in that order.
40
+ CREATE TABLE agent_user_settings (
41
+ project_id TEXT NOT NULL,
42
+ principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
43
+ principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
44
+ approval_mode TEXT NOT NULL CHECK (approval_mode IN ('manual', 'accept-edits', 'auto')),
45
+ updated_at TIMESTAMPTZ NOT NULL,
46
+ PRIMARY KEY (project_id, principal_type, principal_id)
47
+ );
48
+
49
+ CREATE TABLE agent_thread_user_settings (
50
+ project_id TEXT NOT NULL,
51
+ thread_id TEXT NOT NULL,
52
+ principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
53
+ principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
54
+ approval_mode TEXT NOT NULL CHECK (approval_mode IN ('manual', 'accept-edits', 'auto')),
55
+ updated_at TIMESTAMPTZ NOT NULL,
56
+ PRIMARY KEY (project_id, thread_id, principal_type, principal_id),
57
+ FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
58
+ );
@@ -0,0 +1,20 @@
1
+ -- A paused Agent turn resumes once its requester decided on every call it held. What that needs:
2
+ -- the mode "Always allow" raised the turn to, whether the turn may still resume on a call, and the
3
+ -- claim a process takes before it runs an approved call left without its outcome.
4
+ ALTER TABLE agent_approvals
5
+ ADD COLUMN raised_mode TEXT CHECK (raised_mode IN ('manual', 'accept-edits', 'auto')),
6
+ ADD COLUMN turn_stopped_at TIMESTAMPTZ,
7
+ ADD COLUMN execution_claimed_at TIMESTAMPTZ;
8
+
9
+ -- The repair pass: calls past their time, approved calls without an outcome, and decided calls of
10
+ -- turns that may still resume. Each index covers only the rows that are still work.
11
+ CREATE INDEX idx_agent_approvals_due
12
+ ON agent_approvals (project_id, expires_at)
13
+ WHERE status = 'pending';
14
+ CREATE INDEX idx_agent_approvals_unfinished
15
+ ON agent_approvals (project_id, created_at, id)
16
+ WHERE status = 'approved' AND outcome IS NULL;
17
+ CREATE INDEX idx_agent_approvals_resumable
18
+ ON agent_approvals (project_id, created_at, id)
19
+ WHERE status IN ('approved', 'denied') AND reported_in_message_id IS NULL
20
+ AND turn_stopped_at IS NULL;
@@ -0,0 +1,145 @@
1
+ -- A sub-agent works in a thread of its own: its task and its work are that thread's messages, and
2
+ -- each of its runs has the thread and a trigger message, as a turn does. The thread's parent is the
3
+ -- conversation, which every read of it goes through, and its spawning turn and key name it.
4
+ ALTER TABLE agent_threads
5
+ ADD COLUMN kind TEXT NOT NULL DEFAULT 'conversation'
6
+ CHECK (kind IN ('conversation', 'subagent')),
7
+ ADD COLUMN parent_thread_id TEXT,
8
+ ADD COLUMN parent_run_id TEXT,
9
+ ADD COLUMN spawn_key TEXT,
10
+ ADD CONSTRAINT agent_threads_kind_fields CHECK (
11
+ (
12
+ kind = 'conversation'
13
+ AND parent_thread_id IS NULL
14
+ AND parent_run_id IS NULL
15
+ AND spawn_key IS NULL
16
+ )
17
+ OR
18
+ (
19
+ kind = 'subagent'
20
+ AND parent_thread_id IS NOT NULL
21
+ AND parent_run_id IS NOT NULL
22
+ AND spawn_key IS NOT NULL
23
+ AND sandbox_params IS NULL
24
+ )
25
+ ),
26
+ -- A conversation's sub-agent threads go with it.
27
+ ADD CONSTRAINT fk_agent_threads_parent
28
+ FOREIGN KEY (project_id, parent_thread_id)
29
+ REFERENCES agent_threads (project_id, id)
30
+ ON DELETE CASCADE,
31
+ ADD CONSTRAINT fk_agent_threads_parent_run
32
+ FOREIGN KEY (project_id, parent_run_id)
33
+ REFERENCES agent_runs (project_id, id)
34
+ ON DELETE RESTRICT,
35
+ ADD CONSTRAINT uq_agent_threads_spawn UNIQUE (project_id, parent_run_id, spawn_key);
36
+
37
+ ALTER TABLE agent_runs DROP CONSTRAINT agent_runs_kind_fields;
38
+
39
+ -- The children admitted before this have no thread. Each gets one named as a new child's is, with
40
+ -- its task as the only message: what it answered stays in its parent's `wait_agent` output.
41
+ CREATE TEMPORARY TABLE legacy_subagent_threads ON COMMIT DROP AS
42
+ SELECT
43
+ child.project_id,
44
+ child.id AS run_id,
45
+ 'agt_thr_child_' || regexp_replace(child.id, '^agt_run_child_', '') AS thread_id,
46
+ 'agt_msg_child_' || regexp_replace(child.id, '^agt_run_child_', '') AS message_id
47
+ FROM agent_runs AS child
48
+ WHERE child.kind = 'subagent' AND child.thread_id IS NULL;
49
+
50
+ INSERT INTO agent_threads (
51
+ project_id, id, kind, parent_thread_id, parent_run_id, spawn_key, owner_principal_type,
52
+ owner_principal_id, status, active_run_id, last_message_at, message_count, created_at,
53
+ updated_at
54
+ )
55
+ SELECT
56
+ child.project_id,
57
+ legacy.thread_id,
58
+ 'subagent',
59
+ parent.thread_id,
60
+ child.parent_run_id,
61
+ child.spawn_key,
62
+ conversation.owner_principal_type,
63
+ conversation.owner_principal_id,
64
+ 'active',
65
+ CASE WHEN child.status IN ('queued', 'running') THEN child.id END,
66
+ child.created_at,
67
+ 1,
68
+ child.created_at,
69
+ COALESCE(child.completed_at, child.created_at)
70
+ FROM legacy_subagent_threads AS legacy
71
+ JOIN agent_runs AS child ON child.project_id = legacy.project_id AND child.id = legacy.run_id
72
+ JOIN agent_runs AS parent
73
+ ON parent.project_id = child.project_id AND parent.id = child.parent_run_id
74
+ JOIN agent_threads AS conversation
75
+ ON conversation.project_id = parent.project_id AND conversation.id = parent.thread_id;
76
+
77
+ INSERT INTO agent_messages (
78
+ project_id, id, thread_id, run_id, role, seq, parts, content_version, created_at
79
+ )
80
+ SELECT
81
+ child.project_id,
82
+ legacy.message_id,
83
+ legacy.thread_id,
84
+ child.id,
85
+ 'user',
86
+ 1,
87
+ jsonb_build_array(jsonb_build_object('type', 'text', 'text', child.spec->>'task')),
88
+ 2,
89
+ child.created_at
90
+ FROM legacy_subagent_threads AS legacy
91
+ JOIN agent_runs AS child ON child.project_id = legacy.project_id AND child.id = legacy.run_id;
92
+
93
+ UPDATE agent_runs AS child
94
+ SET thread_id = legacy.thread_id, trigger_message_id = legacy.message_id
95
+ FROM legacy_subagent_threads AS legacy
96
+ WHERE child.project_id = legacy.project_id AND child.id = legacy.run_id;
97
+
98
+ DROP TABLE legacy_subagent_threads;
99
+
100
+ ALTER TABLE agent_runs ADD CONSTRAINT agent_runs_kind_fields CHECK (
101
+ (
102
+ kind = 'conversation'
103
+ AND thread_id IS NOT NULL
104
+ AND trigger_message_id IS NOT NULL
105
+ AND parent_run_id IS NULL
106
+ AND spawn_key IS NULL
107
+ AND result IS NULL
108
+ )
109
+ OR
110
+ (
111
+ kind = 'subagent'
112
+ AND thread_id IS NOT NULL
113
+ AND trigger_message_id IS NOT NULL
114
+ AND parent_run_id IS NOT NULL
115
+ AND spawn_key IS NOT NULL
116
+ AND spec IS NOT NULL
117
+ )
118
+ );
119
+
120
+ -- Lists read conversations only, so a conversation's children never weigh on them.
121
+ DROP INDEX idx_agent_threads_project_activity;
122
+ DROP INDEX idx_agent_threads_project_status_activity;
123
+ DROP INDEX idx_agent_threads_project_owner_activity;
124
+
125
+ CREATE INDEX idx_agent_threads_project_activity
126
+ ON agent_threads (project_id, (COALESCE(last_message_at, created_at)) DESC, id DESC)
127
+ WHERE kind = 'conversation';
128
+ CREATE INDEX idx_agent_threads_project_status_activity
129
+ ON agent_threads (project_id, status, (COALESCE(last_message_at, created_at)) DESC, id DESC)
130
+ WHERE kind = 'conversation';
131
+ CREATE INDEX idx_agent_threads_project_owner_activity
132
+ ON agent_threads (
133
+ project_id,
134
+ owner_principal_type,
135
+ owner_principal_id,
136
+ (COALESCE(last_message_at, created_at)) DESC,
137
+ id DESC
138
+ )
139
+ WHERE kind = 'conversation';
140
+
141
+ -- A conversation's children, in the order they were spawned. Only a child has a parent, so the
142
+ -- index holds children only, and a read by `parent_thread_id` can use it.
143
+ CREATE INDEX idx_agent_threads_children
144
+ ON agent_threads (project_id, parent_thread_id, created_at, id)
145
+ WHERE parent_thread_id IS NOT NULL;
@@ -0,0 +1,6 @@
1
+ -- A child that pauses on its question waits for a later turn of its parent's conversation to
2
+ -- answer it. Each turn reads which of the conversation's children wait so: few do, so a partial
3
+ -- index keeps that read small however many children the conversation had.
4
+ CREATE INDEX idx_agent_runs_paused_on_question
5
+ ON agent_runs (project_id, parent_run_id)
6
+ WHERE kind = 'subagent' AND finish_reason = 'awaiting_answer';
package/src/migrations.ts CHANGED
@@ -433,6 +433,25 @@ export const postgresStorageMigrations = defineMigrations<PostgresMigrationConte
433
433
  pgSql("060-drained-source-versions", drainedSourceVersionsSql),
434
434
  pgSql("061-terminal-action-runs", terminalActionRunsSql),
435
435
  pgSql("062-drop-auth-audience", dropAuthAudienceSql),
436
+ pgSql("063-artifacts", artifactsSql),
437
+ pgSql("064-drop-output-diagnostics", dropOutputDiagnosticsSql),
438
+ pgSql("065-plan-work-payload-statistics", planWorkPayloadStatisticsSql),
439
+ pgSql("066-activity", activitySql),
440
+ pgSql("067-activity-timeline", activityTimelineSql),
441
+ pgSql("068-activity-interactions", activityInteractionsSql),
442
+ pgSql("069-artifact-kinds", artifactKindsSql),
443
+ pgSql("070-artifact-state", artifactStateSql),
444
+ pgSql("071-agent-thread-participants", agentThreadParticipantsSql),
445
+ pgSql("072-forked-context-checkpoints", forkedContextCheckpointsSql),
446
+ pgSql("073-unique-values", uniqueValuesSql),
447
+ pgSql("074-generated-media-usage", generatedMediaUsageSql),
448
+ pgSql("075-model-call-files", modelCallFilesSql),
449
+ pgSql("076-agent-memory", agentMemorySql),
450
+ pgSql("077-artifact-threads", artifactThreadsSql),
451
+ pgSql("078-agent-approvals", agentApprovalsSql),
452
+ pgSql("079-agent-approval-resume", agentApprovalResumeSql),
453
+ pgSql("080-subagent-threads", subagentThreadsSql),
454
+ pgSql("081-subagent-questions", subagentQuestionsSql),
436
455
  ],
437
456
  })
438
457
 
@@ -463,3 +482,36 @@ import drainedSourceVersionsSql from "./migrations/060-drained-source-versions.s
463
482
  }
464
483
  import terminalActionRunsSql from "./migrations/061-terminal-action-runs.sql" with { type: "text" }
465
484
  import dropAuthAudienceSql from "./migrations/062-drop-auth-audience.sql" with { type: "text" }
485
+ import artifactsSql from "./migrations/063-artifacts.sql" with { type: "text" }
486
+ import dropOutputDiagnosticsSql from "./migrations/064-drop-output-diagnostics.sql" with {
487
+ type: "text",
488
+ }
489
+ import planWorkPayloadStatisticsSql from "./migrations/065-plan-work-payload-statistics.sql" with {
490
+ type: "text",
491
+ }
492
+ import activitySql from "./migrations/066-activity.sql" with { type: "text" }
493
+ import activityTimelineSql from "./migrations/067-activity-timeline.sql" with { type: "text" }
494
+ import activityInteractionsSql from "./migrations/068-activity-interactions.sql" with {
495
+ type: "text",
496
+ }
497
+ import artifactKindsSql from "./migrations/069-artifact-kinds.sql" with { type: "text" }
498
+ import artifactStateSql from "./migrations/070-artifact-state.sql" with { type: "text" }
499
+ import agentThreadParticipantsSql from "./migrations/071-agent-thread-participants.sql" with {
500
+ type: "text",
501
+ }
502
+ import forkedContextCheckpointsSql from "./migrations/072-forked-context-checkpoints.sql" with {
503
+ type: "text",
504
+ }
505
+ import uniqueValuesSql from "./migrations/073-unique-values.sql" with { type: "text" }
506
+ import generatedMediaUsageSql from "./migrations/074-generated-media-usage.sql" with {
507
+ type: "text",
508
+ }
509
+ import modelCallFilesSql from "./migrations/075-model-call-files.sql" with { type: "text" }
510
+ import agentMemorySql from "./migrations/076-agent-memory.sql" with { type: "text" }
511
+ import artifactThreadsSql from "./migrations/077-artifact-threads.sql" with { type: "text" }
512
+ import agentApprovalsSql from "./migrations/078-agent-approvals.sql" with { type: "text" }
513
+ import agentApprovalResumeSql from "./migrations/079-agent-approval-resume.sql" with {
514
+ type: "text",
515
+ }
516
+ import subagentThreadsSql from "./migrations/080-subagent-threads.sql" with { type: "text" }
517
+ import subagentQuestionsSql from "./migrations/081-subagent-questions.sql" with { type: "text" }
@@ -0,0 +1,44 @@
1
+ import type { ObjectQuery } from "@sixb/core"
2
+ import { hasVectorProfile } from "@sixb/core/internal/query"
3
+ import type {
4
+ CompiledConditionalObjectReadScope,
5
+ ObjectQueryCapabilities,
6
+ ObjectReadStorage,
7
+ } from "@sixb/core/storage"
8
+ import { type PgStoreClient, runPgRepeatableReadTransaction } from "../transactions"
9
+ import type { PgAggregateAdmission } from "./aggregate-admission"
10
+ import { compilePgConditionalObjectReadSource } from "./read-scope"
11
+ import { PgObjectReader } from "./reader"
12
+ import { createPgScopedReader } from "./scoped-reader"
13
+
14
+ /**
15
+ * Read objects and links through relations that apply the scope's conditions as they are read.
16
+ *
17
+ * Reads run as unscoped ones do, under the same aggregate admission: on the pool, and a vector
18
+ * query on one repeatable-read snapshot for its candidate probe and its rows.
19
+ */
20
+ export function createPgConditionalReader(
21
+ sql: PgStoreClient,
22
+ params: {
23
+ projectId: string
24
+ scope: CompiledConditionalObjectReadScope
25
+ },
26
+ options: {
27
+ aggregateAdmission?: PgAggregateAdmission
28
+ queryCapabilities: () => ObjectQueryCapabilities
29
+ }
30
+ ): ObjectReadStorage {
31
+ const source = compilePgConditionalObjectReadSource(params.projectId, params.scope)
32
+ const reader = new PgObjectReader(sql, source, options.aggregateAdmission)
33
+
34
+ return createPgScopedReader(
35
+ {
36
+ projectId: params.projectId,
37
+ read: (run, query?: ObjectQuery) =>
38
+ query && hasVectorProfile(query)
39
+ ? runPgRepeatableReadTransaction(sql, (tx) => run(new PgObjectReader(tx, source)))
40
+ : run(reader),
41
+ },
42
+ options.queryCapabilities
43
+ )
44
+ }
@@ -59,7 +59,7 @@ function preparationFingerprint(plan: ObjectQueryPreparationPlan): string {
59
59
  .sort((a, b) => a.objectTypeId.localeCompare(b.objectTypeId))
60
60
 
61
61
  // Bump the version when physical generation changes, even if the ontology stays the same.
62
- const definition = { version: 1, projectId: plan.projectId, objectTypes }
62
+ const definition = { version: 2, projectId: plan.projectId, objectTypes }
63
63
  return createHash("sha256").update(JSON.stringify(definition)).digest("hex")
64
64
  }
65
65
 
@@ -181,6 +181,12 @@ export interface PgObjectQuerySource {
181
181
  readonly objectsTable: string
182
182
  readonly linksTable: string
183
183
  readonly objectPropertyPermissionsTable?: string
184
+ /**
185
+ * Where to read one concrete type's objects, when the source can say it more precisely than
186
+ * `objectsTable`: a table and that type's own filter over its `properties`. A statement that
187
+ * reads a single type then holds the filter as a top-level condition, which an index can serve.
188
+ */
189
+ objectsOfType?(objectTypeId: string): PgTypedObjects
184
190
  /** Wrap a terminal SELECT without its own WITH clause; the selected source owns the only WITH. */
185
191
  wrapStatement(
186
192
  sql: string,
@@ -192,6 +198,26 @@ export interface PgObjectQuerySource {
192
198
  wrapQuery(query: CompiledPgObjectQuery): CompiledPgObjectQuery
193
199
  }
194
200
 
201
+ export interface PgTypedObjects {
202
+ readonly table: string
203
+ readonly filter?: { readonly sql: string; readonly args: readonly unknown[] }
204
+ }
205
+
206
+ /** The table and `AND ...` filter a statement reads one type's objects with. */
207
+ export function pgObjectsOfType(
208
+ source: PgObjectQuerySource,
209
+ objectTypeId: string
210
+ ): { table: string; filter: string; args: readonly unknown[] } {
211
+ const typed = source.objectsOfType?.(objectTypeId)
212
+ if (!typed) return { table: source.objectsTable, filter: "", args: [] }
213
+
214
+ return {
215
+ table: typed.table,
216
+ filter: typed.filter ? `AND (${typed.filter.sql})` : "",
217
+ args: typed.filter?.args ?? [],
218
+ }
219
+ }
220
+
195
221
  /** Prefix an optional query source, then assign PostgreSQL positional placeholders. */
196
222
  export function compilePgObjectStatement(
197
223
  sql: string,
@@ -462,10 +488,11 @@ function compileStart(
462
488
  }
463
489
 
464
490
  const order = compileOrder(identityOrderFields())
491
+ const objects = pgObjectsOfType(ctx.source, query.objectTypeId)
465
492
  const sql = `
466
493
  SELECT *, properties AS _cursor_properties
467
- FROM ${ctx.source.objectsTable}
468
- WHERE project_id = ? AND object_type_id = ?
494
+ FROM ${objects.table}
495
+ WHERE project_id = ? AND object_type_id = ? ${objects.filter}
469
496
  ${ctx.startPredicate ? `AND (${ctx.startPredicate})` : ""}
470
497
  ${ctx.unordered ? "" : `ORDER BY ${order.sql}`}
471
498
  ${ctx.isolateSortOverflow ? "OFFSET 0" : ""}
@@ -473,9 +500,9 @@ function compileStart(
473
500
 
474
501
  return {
475
502
  sql,
476
- args: [projectId, query.objectTypeId, ...(ctx.unordered ? [] : order.args)],
477
- totalSql: `SELECT COUNT(*)::bigint AS total FROM ${ctx.source.objectsTable} WHERE project_id = ? AND object_type_id = ?`,
478
- totalArgs: [projectId, query.objectTypeId],
503
+ args: [projectId, query.objectTypeId, ...objects.args, ...(ctx.unordered ? [] : order.args)],
504
+ totalSql: `SELECT COUNT(*)::bigint AS total FROM ${objects.table} WHERE project_id = ? AND object_type_id = ? ${objects.filter}`,
505
+ totalArgs: [projectId, query.objectTypeId, ...objects.args],
479
506
  order,
480
507
  hasMore: () => false,
481
508
  trimRows: identityRows,
@@ -1070,11 +1097,7 @@ function compileExpansionOrder(
1070
1097
  const direction = field.direction === "desc" ? "DESC" : "ASC"
1071
1098
  clauses.push(
1072
1099
  `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END ASC`,
1073
- `${
1074
- field.scalarKind === "decimal"
1075
- ? `(${jsonTextExpression(propertyColumn)})::numeric`
1076
- : `NULLIF(${jsonValueExpression(propertyColumn)}, 'null'::jsonb)`
1077
- } ${direction}`
1100
+ `${orderedValueExpression(field.scalarKind, propertyColumn)} ${direction}`
1078
1101
  )
1079
1102
  args.push(field.propertyId, field.propertyId, field.propertyId)
1080
1103
  }
@@ -1239,13 +1262,14 @@ function compileAggregateStart(
1239
1262
  throw new Error("[SixbPg] PostgreSQL object storage does not support start.includeSubtypes")
1240
1263
  }
1241
1264
 
1265
+ const objects = pgObjectsOfType(source, query.objectTypeId)
1242
1266
  return {
1243
1267
  sql: `
1244
1268
  SELECT project_id, object_type_id, primary_id, properties
1245
- FROM ${source.objectsTable}
1246
- WHERE project_id = ? AND object_type_id = ?
1269
+ FROM ${objects.table}
1270
+ WHERE project_id = ? AND object_type_id = ? ${objects.filter}
1247
1271
  `,
1248
- args: [projectId, query.objectTypeId],
1272
+ args: [projectId, query.objectTypeId, ...objects.args],
1249
1273
  }
1250
1274
  }
1251
1275
 
@@ -1452,7 +1476,8 @@ function compileProjectionExpression(properties: readonly string[]): CompiledPre
1452
1476
  }
1453
1477
  }
1454
1478
 
1455
- function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
1479
+ /** Compile a predicate over the unqualified `properties` column of the relation it filters. */
1480
+ export function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
1456
1481
  switch (predicate.op) {
1457
1482
  case "and":
1458
1483
  case "or": {
@@ -1467,6 +1492,8 @@ function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
1467
1492
  const item = compilePredicate(predicate.item)
1468
1493
  return negatePredicate(item)
1469
1494
  }
1495
+ case "constant":
1496
+ return { sql: predicate.value ? "true" : "false", args: [] }
1470
1497
  case "eq":
1471
1498
  return predicate.scalarKind === "decimal"
1472
1499
  ? compileDecimalEqualityPredicate(predicate.propertyId, "eq", predicate.value)
@@ -1486,6 +1513,14 @@ function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
1486
1513
  args: [predicate.propertyId, predicate.propertyId, predicate.value],
1487
1514
  }
1488
1515
  }
1516
+ if (isTextScalarKind(predicate.scalarKind)) {
1517
+ // The null rank makes the range a prefix of the property's sort index keys.
1518
+ return {
1519
+ sql: `(${nullRankExpression()} = 0 AND ${orderedValueExpression(predicate.scalarKind)} ${op} ?::text)`,
1520
+ args: [predicate.propertyId, predicate.propertyId, predicate.propertyId, predicate.value],
1521
+ }
1522
+ }
1523
+
1489
1524
  return {
1490
1525
  sql: `${jsonValueExpression()} ${op} ?::text::jsonb`,
1491
1526
  args: [predicate.propertyId, jsonbValue(predicate.value)],
@@ -1809,13 +1844,13 @@ function compileKeysetRanges(
1809
1844
  })
1810
1845
  if (value.nullish) return
1811
1846
  const expression = compiledPropertyValueExpression(field, propertyColumn)
1812
- const parameter = field.scalarKind === "decimal" ? "?::numeric" : "?::text::jsonb"
1813
- const args = [
1814
- field.propertyId,
1815
- field.scalarKind === "decimal" ? value.value : jsonbValue(value.value),
1816
- ]
1817
- addRange({ sql: `${expression} ${field.direction === "desc" ? "<" : ">"} ${parameter}`, args })
1818
- prefix.push({ sql: `${expression} = ${parameter}`, args })
1847
+ const parameter = compiledPropertyParameter(field, value.value)
1848
+ const args = [field.propertyId, parameter.value]
1849
+ addRange({
1850
+ sql: `${expression} ${field.direction === "desc" ? "<" : ">"} ${parameter.sql}`,
1851
+ args,
1852
+ })
1853
+ prefix.push({ sql: `${expression} = ${parameter.sql}`, args })
1819
1854
  })
1820
1855
  return ranges
1821
1856
  }
@@ -1884,14 +1919,11 @@ function compileFieldEquality(
1884
1919
  }
1885
1920
  }
1886
1921
 
1922
+ const parameter = compiledPropertyParameter(field, cursor.value)
1923
+
1887
1924
  return {
1888
- sql: `(${jsonTypeExpression(propertyColumn)} IS NOT NULL AND ${jsonTypeExpression(propertyColumn)} != 'null' AND ${compiledPropertyValueExpression(field, propertyColumn)} = ${field.scalarKind === "decimal" ? "?::numeric" : "?::text::jsonb"})`,
1889
- args: [
1890
- field.propertyId,
1891
- field.propertyId,
1892
- field.propertyId,
1893
- field.scalarKind === "decimal" ? cursor.value : jsonbValue(cursor.value),
1894
- ],
1925
+ sql: `(${jsonTypeExpression(propertyColumn)} IS NOT NULL AND ${jsonTypeExpression(propertyColumn)} != 'null' AND ${compiledPropertyValueExpression(field, propertyColumn)} = ${parameter.sql})`,
1926
+ args: [field.propertyId, field.propertyId, field.propertyId, parameter.value],
1895
1927
  }
1896
1928
  }
1897
1929
 
@@ -1911,16 +1943,17 @@ function compileFieldAfter(
1911
1943
 
1912
1944
  const rankSql = `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END`
1913
1945
  const valueOperator = field.direction === "desc" ? "<" : ">"
1946
+ const parameter = compiledPropertyParameter(field, cursor.value)
1914
1947
 
1915
1948
  return {
1916
- sql: `(${rankSql} > 0 OR (${rankSql} = 0 AND ${compiledPropertyValueExpression(field, propertyColumn)} ${valueOperator} ${field.scalarKind === "decimal" ? "?::numeric" : "?::text::jsonb"}))`,
1949
+ sql: `(${rankSql} > 0 OR (${rankSql} = 0 AND ${compiledPropertyValueExpression(field, propertyColumn)} ${valueOperator} ${parameter.sql}))`,
1917
1950
  args: [
1918
1951
  field.propertyId,
1919
1952
  field.propertyId,
1920
1953
  field.propertyId,
1921
1954
  field.propertyId,
1922
1955
  field.propertyId,
1923
- field.scalarKind === "decimal" ? cursor.value : jsonbValue(cursor.value),
1956
+ parameter.value,
1924
1957
  ],
1925
1958
  }
1926
1959
  }
@@ -2010,9 +2043,39 @@ function compiledPropertyValueExpression(
2010
2043
  field: Extract<CompiledOrderField, { kind: "property" }>,
2011
2044
  propertyColumn = "properties"
2012
2045
  ): string {
2013
- return field.scalarKind === "decimal"
2014
- ? `(${jsonTextExpression(propertyColumn)})::numeric`
2015
- : `NULLIF(${jsonValueExpression(propertyColumn)}, 'null'::jsonb)`
2046
+ return orderedValueExpression(field.scalarKind, propertyColumn)
2047
+ }
2048
+
2049
+ /** The parameter a cursor value binds to, typed like `compiledPropertyValueExpression`. */
2050
+ function compiledPropertyParameter(
2051
+ field: Extract<CompiledOrderField, { kind: "property" }>,
2052
+ value: unknown
2053
+ ): { sql: string; value: unknown } {
2054
+ if (field.scalarKind === "decimal") return { sql: "?::numeric", value }
2055
+ if (isTextScalarKind(field.scalarKind)) return { sql: "?::text", value }
2056
+ return { sql: "?::text::jsonb", value: jsonbValue(value) }
2057
+ }
2058
+
2059
+ /**
2060
+ * The expression a property sorts and range-compares by. Strings compare by code point, as
2061
+ * SQLite and the in-memory store do: JSONB and the database's own collation would order case and
2062
+ * accents linguistically. Sort indexes repeat this expression, collation included.
2063
+ */
2064
+ function orderedValueExpression(
2065
+ scalarKind: QueryScalarKind | undefined,
2066
+ propertyColumn = "properties"
2067
+ ): string {
2068
+ if (scalarKind === "decimal") return `(${jsonTextExpression(propertyColumn)})::numeric`
2069
+ if (isTextScalarKind(scalarKind)) return `((${jsonTextExpression(propertyColumn)}) COLLATE "C")`
2070
+ return `NULLIF(${jsonValueExpression(propertyColumn)}, 'null'::jsonb)`
2071
+ }
2072
+
2073
+ export function isTextScalarKind(scalarKind: QueryScalarKind | undefined): boolean {
2074
+ return scalarKind === "string" || scalarKind === "uuid"
2075
+ }
2076
+
2077
+ function nullRankExpression(propertyColumn = "properties"): string {
2078
+ return `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END`
2016
2079
  }
2017
2080
 
2018
2081
  function columnExpression(
@@ -4,6 +4,7 @@ import type { ReservedSQL, SQL, SQLClient } from "../pg-client"
4
4
  import { isConnectionLost } from "../storage-errors"
5
5
  import { withReservedPgConnection } from "../transactions"
6
6
  import { lockPgPreparation } from "./preparation-lock"
7
+ import { isTextScalarKind } from "./query-compiler"
7
8
 
8
9
  export interface PgObjectQueryIndexScope {
9
10
  readonly projectId: string
@@ -110,6 +111,22 @@ async function createQueryIndex(
110
111
  await connection.unsafe(
111
112
  `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${identifier(`${name}_overflow`)} ON ${identifier(schema)}.objects (primary_id) WHERE ${scope} AND NOT (${sortIndexBound(definition.index)})`
112
113
  )
114
+
115
+ // The same definition under another name is an earlier generation of this index, built
116
+ // with an expression queries no longer use. Writes would keep maintaining it.
117
+ const superseded = await connection<{ name: string }[]>`
118
+ SELECT c.relname AS name FROM pg_index i
119
+ JOIN pg_class c ON c.oid = i.indexrelid
120
+ JOIN pg_namespace n ON n.oid = c.relnamespace
121
+ WHERE n.nspname = ${schema} AND c.relname LIKE 'sixb_query_%' AND c.relname <> ${name}
122
+ AND obj_description(c.oid, 'pg_class') = ${JSON.stringify(definition.index)}`
123
+ for (const previous of superseded) {
124
+ for (const index of [previous.name, `${previous.name}_overflow`]) {
125
+ await connection.unsafe(
126
+ `DROP INDEX CONCURRENTLY IF EXISTS ${identifier(schema)}.${identifier(index)}`
127
+ )
128
+ }
129
+ }
113
130
  }
114
131
  return name
115
132
  }
@@ -150,14 +167,7 @@ function statisticsExpressions(index: PgObjectQueryIndex): string[] {
150
167
  if (index.kind === "value") expressions.push(`(properties -> ${literal(index.propertyId)})`)
151
168
  if (index.kind === "text")
152
169
  expressions.push(`lower(coalesce(properties ->> ${literal(index.propertyId)}, ''))`)
153
- if (index.kind === "sort")
154
- for (const field of index.fields) {
155
- expressions.push(
156
- field.scalarKind === "decimal"
157
- ? `((properties ->> ${literal(field.propertyId)})::numeric)`
158
- : `(NULLIF(properties -> ${literal(field.propertyId)}, 'null'::jsonb))`
159
- )
160
- }
170
+ if (index.kind === "sort") for (const field of index.fields) expressions.push(sortValue(field))
161
171
  return expressions
162
172
  }
163
173
 
@@ -220,7 +230,7 @@ function compileIndex(index: PgObjectQueryIndex): string {
220
230
  const property = `properties -> ${literal(field.propertyId)}`
221
231
  fields.push(
222
232
  `(CASE WHEN jsonb_typeof(${property}) IS NULL OR jsonb_typeof(${property}) = 'null' THEN 1 ELSE 0 END)`,
223
- `${field.scalarKind === "decimal" ? `((properties ->> ${literal(field.propertyId)})::numeric)` : `(NULLIF(${property}, 'null'::jsonb))`} ${field.direction === "desc" ? "DESC" : "ASC"}`
233
+ `${sortValue(field)} ${field.direction === "desc" ? "DESC" : "ASC"}`
224
234
  )
225
235
  }
226
236
  fields.push("primary_id")
@@ -228,6 +238,14 @@ function compileIndex(index: PgObjectQueryIndex): string {
228
238
  return `(${fields.join(", ")}) WHERE ${predicate.join(" AND ")}`
229
239
  }
230
240
 
241
+ /** The query compiler's ordered value expression; an index serves a sort only when they match. */
242
+ function sortValue(field: Extract<PgObjectQueryIndex, { kind: "sort" }>["fields"][number]): string {
243
+ const text = `(properties ->> ${literal(field.propertyId)})`
244
+ if (field.scalarKind === "decimal") return `(${text}::numeric)`
245
+ if (isTextScalarKind(field.scalarKind)) return `(${text} COLLATE "C")`
246
+ return `(NULLIF(properties -> ${literal(field.propertyId)}, 'null'::jsonb))`
247
+ }
248
+
231
249
  function literal(value: string): string {
232
250
  if (value.includes("\0"))
233
251
  throw new Error("[SixbPg] Query index names and values cannot contain NUL")
@@ -337,7 +355,8 @@ export async function findPgSortIndexBound(
337
355
  fields[i]?.kind === "property" &&
338
356
  fields[i].propertyId === field.propertyId &&
339
357
  (field.direction ?? "asc") === (fields[i].direction ?? "asc") &&
340
- (field.scalarKind === "decimal") === (fields[i].scalarKind === "decimal")
358
+ (field.scalarKind === "decimal") === (fields[i].scalarKind === "decimal") &&
359
+ isTextScalarKind(field.scalarKind) === isTextScalarKind(fields[i].scalarKind)
341
360
  )
342
361
  )
343
362
  continue