@inflexa-ai/harness 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +5 -2
- package/dist/agents/sandbox/shared.d.ts +3 -0
- package/dist/agents/sandbox/shared.js +5 -0
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/app/chat-turn.d.ts +2 -0
- package/dist/app/chat-turn.js +11 -2
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +2 -1
- package/dist/citations/__fixtures__/resolver.d.ts +3 -0
- package/dist/citations/__fixtures__/resolver.js +9 -0
- package/dist/citations/aggregate.d.ts +3 -0
- package/dist/citations/aggregate.js +44 -0
- package/dist/citations/clients/arxiv.d.ts +6 -0
- package/dist/citations/clients/arxiv.js +34 -0
- package/dist/citations/clients/common.d.ts +11 -0
- package/dist/citations/clients/common.js +43 -0
- package/dist/citations/clients/crossref.d.ts +8 -0
- package/dist/citations/clients/crossref.js +98 -0
- package/dist/citations/clients/doi-registry.d.ts +3 -0
- package/dist/citations/clients/doi-registry.js +95 -0
- package/dist/citations/clients/pubmed.d.ts +6 -0
- package/dist/citations/clients/pubmed.js +83 -0
- package/dist/citations/clients/semantic-scholar.d.ts +6 -0
- package/dist/citations/clients/semantic-scholar.js +60 -0
- package/dist/citations/compare.d.ts +9 -0
- package/dist/citations/compare.js +72 -0
- package/dist/citations/match.d.ts +11 -0
- package/dist/citations/match.js +173 -0
- package/dist/citations/normalize.d.ts +31 -0
- package/dist/citations/normalize.js +187 -0
- package/dist/citations/plan.d.ts +2 -0
- package/dist/citations/plan.js +59 -0
- package/dist/citations/resolve.d.ts +28 -0
- package/dist/citations/resolve.js +238 -0
- package/dist/citations/types.d.ts +583 -0
- package/dist/citations/types.js +129 -0
- package/dist/contracts/chat-events.d.ts +16 -0
- package/dist/contracts/message.d.ts +13 -8
- package/dist/contracts/schemas/chat-events.d.ts +7 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/schemas/chat-parts.d.ts +8 -8
- package/dist/contracts/target-dossier.d.ts +20 -20
- package/dist/execution/run-synthesis.d.ts +3 -0
- package/dist/execution/run-synthesis.js +1 -0
- package/dist/index.d.ts +14 -10
- package/dist/index.js +20 -16
- package/dist/lib/cache.d.ts +8 -0
- package/dist/lib/cache.js +35 -0
- package/dist/lib/rate-limit.d.ts +35 -0
- package/dist/lib/rate-limit.js +121 -0
- package/dist/literature/sources/arxiv.d.ts +22 -0
- package/dist/literature/sources/arxiv.js +103 -0
- package/dist/literature/sources/http.d.ts +28 -0
- package/dist/literature/sources/http.js +97 -0
- package/dist/literature/sources/pubmed.d.ts +68 -0
- package/dist/literature/sources/pubmed.js +266 -0
- package/dist/literature/sources/semantic-scholar.d.ts +24 -0
- package/dist/literature/sources/semantic-scholar.js +71 -0
- package/dist/memory/content-to-cortex.d.ts +12 -11
- package/dist/memory/content-to-cortex.js +17 -14
- package/dist/memory/conversation-display-backfill.d.ts +25 -0
- package/dist/memory/conversation-display-backfill.js +97 -0
- package/dist/memory/conversation-display-recorder.d.ts +17 -0
- package/dist/memory/conversation-display-recorder.js +156 -0
- package/dist/memory/conversation-display-replay.d.ts +19 -0
- package/dist/memory/conversation-display-replay.js +39 -0
- package/dist/memory/conversation-display-storage.d.ts +86 -0
- package/dist/memory/conversation-display-storage.js +168 -0
- package/dist/memory/reconstruct-cards.d.ts +7 -1
- package/dist/memory/reconstruct-cards.js +5 -1
- package/dist/memory/thread-history.d.ts +46 -11
- package/dist/memory/thread-history.js +55 -13
- package/dist/memory/thread-store.d.ts +142 -25
- package/dist/memory/thread-store.js +228 -40
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +2 -0
- package/dist/runtime/assemble.d.ts +50 -4
- package/dist/runtime/assemble.js +47 -3
- package/dist/runtime/boot.d.ts +8 -2
- package/dist/runtime/boot.js +20 -2
- package/dist/state/init.js +50 -1
- package/dist/state/schema.d.ts +2 -2
- package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
- package/dist/tools/bio/keys.d.ts +1 -0
- package/dist/tools/bio/pubmed.js +4 -4
- package/dist/tools/lib/ncbi-utils.d.ts +3 -58
- package/dist/tools/lib/ncbi-utils.js +2 -286
- package/dist/tools/lib/pubmed-client.d.ts +5 -12
- package/dist/tools/lib/pubmed-client.js +20 -79
- package/dist/tools/research/generate-analogy-report.js +2 -2
- package/dist/tools/research/index.d.ts +1 -0
- package/dist/tools/research/index.js +1 -0
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +2 -0
- package/dist/tools/research/resolve-citation.d.ts +3 -0
- package/dist/tools/research/resolve-citation.js +15 -0
- package/dist/tools/research/search-arxiv.d.ts +3 -19
- package/dist/tools/research/search-arxiv.js +9 -105
- package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
- package/dist/tools/research/search-semantic-scholar.js +29 -88
- package/dist/tools/sandbox/list-available-refs.js +1 -2
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +1 -0
- package/dist/workflows/sandbox-step.d.ts +5 -0
- package/dist/workflows/sandbox-step.js +1 -0
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2 -2
- package/dist/workflows/target-assessment/schemas.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
|
@@ -20,17 +20,36 @@
|
|
|
20
20
|
* is the atomic unit; `appendTurn` writes one atomically and `loadRecent`
|
|
21
21
|
* rounds the budget walk to turn boundaries.
|
|
22
22
|
*/
|
|
23
|
+
import { randomUUID } from "node:crypto";
|
|
23
24
|
import { metrics } from "@opentelemetry/api";
|
|
24
25
|
import { ResultAsync, ok, okAsync } from "neverthrow";
|
|
25
26
|
import { stripNulCharacters } from "../input-sanitization.js";
|
|
26
27
|
import { tryMutation, tryQuery, withTransaction } from "../lib/db-result.js";
|
|
27
28
|
import { hasReportedUsage } from "../loop/metrics.js";
|
|
28
29
|
import { countTokens } from "./count-tokens.js";
|
|
29
|
-
import { HARNESS_PROVIDER_NAMESPACE, SYNTHETIC_MESSAGE_KEY, envelopeMessage, isSyntheticUserMessage, parseStoredMessageEnvelope, } from "./ai-sdk-message-storage.js";
|
|
30
|
+
import { HARNESS_PROVIDER_NAMESPACE, SYNTHETIC_MESSAGE_KEY, envelopeMessage, isSyntheticUserMessage, parseStoredMessageEnvelope, syntheticRecordMessage, } from "./ai-sdk-message-storage.js";
|
|
31
|
+
import { envelopeDisplayMessages, parseStoredDisplayEnvelope } from "./conversation-display-storage.js";
|
|
30
32
|
/** A resolved `ok(undefined)` ResultAsync — the empty/seed transaction step. */
|
|
31
33
|
function okVoid() {
|
|
32
34
|
return new ResultAsync(Promise.resolve(ok(undefined)));
|
|
33
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The append a host uses to record out-of-band work in a thread — an analysis
|
|
38
|
+
* run's outcome, typically.
|
|
39
|
+
*
|
|
40
|
+
* Both projections are built here because a record has no live turn behind it:
|
|
41
|
+
* no recorder observed it, so nothing else would produce its display projection,
|
|
42
|
+
* and a record appended without one would be stored, read by the model, and
|
|
43
|
+
* never shown. The harness owns the record-to-`system` mapping for the same
|
|
44
|
+
* reason it owns the marker — a host that hand-assembled either could produce a
|
|
45
|
+
* message the turn-boundary predicates fail to recognise.
|
|
46
|
+
*/
|
|
47
|
+
export function conversationRecordTurn(text) {
|
|
48
|
+
return {
|
|
49
|
+
modelMessages: [syntheticRecordMessage(text)],
|
|
50
|
+
displayMessages: [{ id: randomUUID(), role: "system", parts: [{ type: "text", text, state: "done" }] }],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
34
53
|
/**
|
|
35
54
|
* A turn starts on a `user` message that a human actually sent. Two kinds of
|
|
36
55
|
* message carry the `user` role without opening a turn, and both are excluded
|
|
@@ -156,9 +175,16 @@ export const EVICTION_BLOCK_TURNS = 4;
|
|
|
156
175
|
* provisioned by the project's state-init DDL.
|
|
157
176
|
*/
|
|
158
177
|
export function createThreadHistory(pool) {
|
|
159
|
-
function appendTurn(threadId,
|
|
178
|
+
function appendTurn(threadId, turn) {
|
|
179
|
+
const { modelMessages: messages, displayMessages, turnUsage } = turn;
|
|
160
180
|
if (messages.length === 0)
|
|
161
181
|
return okVoid();
|
|
182
|
+
// The display projection rides the append's FIRST row, so one SELECT of a
|
|
183
|
+
// thread's rows yields every envelope in order with no join and no grouping.
|
|
184
|
+
// Whichever row that is — a genuine user message opening a turn, or the lone
|
|
185
|
+
// record row of an out-of-band append — is the row a tail retraction removes
|
|
186
|
+
// the envelope with, because it is the row the projection describes.
|
|
187
|
+
const displayEnvelope = displayMessages.length > 0 ? JSON.stringify(envelopeDisplayMessages(displayMessages)) : null;
|
|
162
188
|
// A rollup that reports no quantity is stored as absent, not as a rollup of
|
|
163
189
|
// absences, so "no figure" has exactly one representation in storage.
|
|
164
190
|
// `hasReportedUsage` is the loop's own predicate for the same question,
|
|
@@ -185,14 +211,23 @@ export function createThreadHistory(pool) {
|
|
|
185
211
|
// re-throws it to force ROLLBACK (a returned `err` that does not
|
|
186
212
|
// reach `withTransaction` would COMMIT silently).
|
|
187
213
|
messages.reduce((chain, message, i) => chain.andThen(() => tryMutation("thread-history.appendTurn.insert", () => client.query(
|
|
188
|
-
// `message_envelope::json`, never `::jsonb
|
|
189
|
-
// never `::json` — see the column comments
|
|
190
|
-
|
|
191
|
-
|
|
214
|
+
// `message_envelope::json`, never `::jsonb`; `display_envelope` and
|
|
215
|
+
// `reported_usage::jsonb`, never `::json` — see the column comments
|
|
216
|
+
// in the state-init DDL.
|
|
217
|
+
`INSERT INTO messages (thread_id, seq, message_envelope, display_envelope, tokens, reported_usage)
|
|
218
|
+
VALUES ($1, $2, $3::json, $4::jsonb, $5, $6::jsonb)
|
|
192
219
|
ON CONFLICT (thread_id, seq) DO UPDATE
|
|
193
220
|
SET message_envelope = EXCLUDED.message_envelope,
|
|
221
|
+
display_envelope = EXCLUDED.display_envelope,
|
|
194
222
|
tokens = EXCLUDED.tokens,
|
|
195
|
-
reported_usage = EXCLUDED.reported_usage`, [
|
|
223
|
+
reported_usage = EXCLUDED.reported_usage`, [
|
|
224
|
+
threadId,
|
|
225
|
+
startSeq + i,
|
|
226
|
+
serializeEnvelope(message),
|
|
227
|
+
i === 0 ? displayEnvelope : null,
|
|
228
|
+
countTokens(message.content),
|
|
229
|
+
i === rollupRow ? rollup : null,
|
|
230
|
+
])).map(() => undefined)), okVoid()))
|
|
196
231
|
.andThen(() =>
|
|
197
232
|
// Thread listings sort on `cortex_analysis_threads.updated_at`, and
|
|
198
233
|
// the only other writer of it is the title update — so without this
|
|
@@ -311,15 +346,22 @@ export function createThreadHistory(pool) {
|
|
|
311
346
|
// `seq::text AS seq` output alias (Postgres resolves an unqualified
|
|
312
347
|
// ORDER BY name to the output column), sorting the bigint as text:
|
|
313
348
|
// "10" before "2". The qualified name forces the bigint column.
|
|
314
|
-
`SELECT seq::text AS seq, message_envelope, reported_usage
|
|
349
|
+
`SELECT seq::text AS seq, message_envelope, display_envelope, reported_usage
|
|
315
350
|
FROM messages WHERE thread_id = $1
|
|
316
351
|
ORDER BY messages.seq ASC`, [threadId]);
|
|
317
|
-
|
|
352
|
+
// Spread rather than `usage: r.reported_usage ?? undefined`, so a row with
|
|
353
|
+
// no rollup carries no `usage` KEY at all — absent, not present-and-undefined.
|
|
354
|
+
const stored = await Promise.all(rows.map(async (r) => {
|
|
318
355
|
const envelope = parseStoredMessageEnvelope(r.message_envelope, `${threadId}/${r.seq}`);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
356
|
+
const displayEnvelope = r.display_envelope == null ? undefined : await parseStoredDisplayEnvelope(r.display_envelope, `${threadId}/${r.seq}/display`);
|
|
357
|
+
return {
|
|
358
|
+
seq: Number(r.seq),
|
|
359
|
+
envelope,
|
|
360
|
+
message: envelope.message,
|
|
361
|
+
...(displayEnvelope ? { displayEnvelope } : {}),
|
|
362
|
+
...(r.reported_usage === null ? {} : { usage: r.reported_usage }),
|
|
363
|
+
};
|
|
364
|
+
}));
|
|
323
365
|
const turns = groupTurns(stored, (row) => isGenuineUserStart(row.message));
|
|
324
366
|
const total = turns.length;
|
|
325
367
|
const offset = safePage * safePerPage;
|
|
@@ -10,20 +10,24 @@
|
|
|
10
10
|
* the vocabulary is conversation-shaped on purpose — reaching for it inside
|
|
11
11
|
* a workflow step feels immediately wrong.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* A thread may name another as its parent, so the rows under one analysis form
|
|
14
|
+
* a forest rather than a flat list. The three lifecycle verbs each answer that
|
|
15
|
+
* edge differently, and the differences are the substance of this module.
|
|
16
|
+
*
|
|
17
|
+
* `archiveThread` is recoverable: it stamps `deleted_at` on the named thread
|
|
18
|
+
* and on every descendant, and because `getThread` and the default
|
|
15
19
|
* `listThreads` filter `deleted_at IS NULL` an archived thread is
|
|
16
20
|
* indistinguishable from an absent one while its row and every one of its
|
|
17
|
-
* `messages` rows stay in storage. `unarchiveThread` clears the stamp
|
|
18
|
-
* thread reads as it did; a listing widened with `includeArchived`
|
|
19
|
-
* way to obtain the id it takes, so the recovery is reachable by a
|
|
20
|
-
* holds no thread ids it obtained elsewhere.
|
|
21
|
+
* `messages` rows stay in storage. `unarchiveThread` clears the stamp on ONE
|
|
22
|
+
* row and the thread reads as it did; a listing widened with `includeArchived`
|
|
23
|
+
* is the only way to obtain the id it takes, so the recovery is reachable by a
|
|
24
|
+
* host that holds no thread ids it obtained elsewhere.
|
|
21
25
|
* `purgeThread` is not recoverable: it removes the `messages` rows and the
|
|
22
|
-
* metadata
|
|
23
|
-
* thread stripped of its transcript, nor a transcript
|
|
24
|
-
* It is the thread-scoped member of this package's
|
|
25
|
-
* `purgeAnalysis` reclaims an analysis's whole
|
|
26
|
-
* `purgeThread` one
|
|
26
|
+
* metadata rows of the whole subtree in one transaction, so a failure partway
|
|
27
|
+
* leaves both — never a thread stripped of its transcript, nor a transcript
|
|
28
|
+
* with nothing naming it. It is the thread-scoped member of this package's
|
|
29
|
+
* reclamation vocabulary — `purgeAnalysis` reclaims an analysis's whole
|
|
30
|
+
* persisted footprint, `purgeThread` one subtree's.
|
|
27
31
|
*
|
|
28
32
|
* A purge creates no orphan of its own, but it is NOT serialized against a
|
|
29
33
|
* concurrent `appendTurn`: `messages` carries no foreign key to
|
|
@@ -31,17 +35,51 @@
|
|
|
31
35
|
* metadata row. A turn committing after the purge therefore persists messages
|
|
32
36
|
* under a `thread_id` that no longer resolves to an analysis, leaving them
|
|
33
37
|
* unreachable by any later thread- or analysis-scoped reclamation. Stop writes
|
|
34
|
-
* to
|
|
35
|
-
* store cannot observe a host's in-flight turns,
|
|
38
|
+
* to every thread in the subtree — unbind each from any live conversation —
|
|
39
|
+
* before purging its root; this store cannot observe a host's in-flight turns,
|
|
40
|
+
* so it does not enforce that.
|
|
36
41
|
*/
|
|
37
42
|
import { type ResultAsync } from "neverthrow";
|
|
38
43
|
import type { Pool } from "pg";
|
|
39
44
|
import { type DbError } from "../lib/db-result.js";
|
|
45
|
+
/**
|
|
46
|
+
* The kind of session a thread holds. `conversation` is the analysis
|
|
47
|
+
* conversation the product has always had; `report` is a report session spawned
|
|
48
|
+
* from one.
|
|
49
|
+
*
|
|
50
|
+
* The set is closed rather than free-form because a thread's type is what
|
|
51
|
+
* selects the agent that runs it. That resolution is a separate capability, and
|
|
52
|
+
* it can only be exhaustive over a membership something can enumerate — a
|
|
53
|
+
* free-form column pushes an unmatched value into a fallback no reader has the
|
|
54
|
+
* list to write.
|
|
55
|
+
*
|
|
56
|
+
* The array is the single declaration and the union is derived from it, so the
|
|
57
|
+
* run-time check `createThread` needs — for a value that reached the store from
|
|
58
|
+
* outside the compiler's reach — cannot drift from the type callers program
|
|
59
|
+
* against.
|
|
60
|
+
*/
|
|
61
|
+
declare const THREAD_TYPES: readonly ["conversation", "report"];
|
|
62
|
+
export type ThreadType = (typeof THREAD_TYPES)[number];
|
|
40
63
|
/** One conversation thread's metadata, as returned by the store. */
|
|
41
64
|
export interface Thread {
|
|
42
65
|
readonly threadId: string;
|
|
43
66
|
readonly analysisId: string;
|
|
44
67
|
readonly title: string | null;
|
|
68
|
+
/** Which kind of session this row holds. A thread created without one is a `conversation`. */
|
|
69
|
+
readonly threadType: ThreadType;
|
|
70
|
+
/** The thread this one was spawned from — `null` on a thread that stands on its own. */
|
|
71
|
+
readonly parentThreadId: string | null;
|
|
72
|
+
/**
|
|
73
|
+
* The parent's `messages.seq` at the moment of the spawn: the frozen point
|
|
74
|
+
* in the parent's transcript this thread was built on. `null` exactly when
|
|
75
|
+
* `parentThreadId` is — half the pair names nothing a consumer can resolve.
|
|
76
|
+
*
|
|
77
|
+
* It records the spawn point, not a promise that the prefix still reads the
|
|
78
|
+
* same: `retractLastTurn` can cut the parent's tail behind an anchor, so a
|
|
79
|
+
* reader must treat an anchor past the parent's current end as a normal
|
|
80
|
+
* state rather than a corruption.
|
|
81
|
+
*/
|
|
82
|
+
readonly parentSeq: number | null;
|
|
45
83
|
readonly createdAt: Date;
|
|
46
84
|
readonly updatedAt: Date;
|
|
47
85
|
/**
|
|
@@ -56,7 +94,51 @@ export interface CreateThreadInput {
|
|
|
56
94
|
readonly threadId: string;
|
|
57
95
|
readonly analysisId: string;
|
|
58
96
|
readonly title?: string | null;
|
|
97
|
+
/** Omitted is `conversation` — what a thread is when nothing says otherwise. */
|
|
98
|
+
readonly type?: ThreadType;
|
|
99
|
+
/**
|
|
100
|
+
* The thread this one is spawned from. It must belong to the same analysis
|
|
101
|
+
* as the child; whether it exists at all is the foreign key's verdict, not
|
|
102
|
+
* this store's.
|
|
103
|
+
*/
|
|
104
|
+
readonly parentThreadId?: string;
|
|
105
|
+
/**
|
|
106
|
+
* The parent's `messages.seq` at the spawn. Supply it with `parentThreadId`
|
|
107
|
+
* or neither: the two are one fact — which transcript, and how much of it —
|
|
108
|
+
* and either half alone describes no place.
|
|
109
|
+
*/
|
|
110
|
+
readonly parentSeq?: number;
|
|
59
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* A create the store refuses on its own, before any row is written. Distinct
|
|
114
|
+
* from `DbError`: nothing failed at the driver — the request described a thread
|
|
115
|
+
* the store will not write, and each variant carries the identifiers that say
|
|
116
|
+
* which values disagreed.
|
|
117
|
+
*
|
|
118
|
+
* Only the rules the store enforces itself live here. A `parentThreadId` naming
|
|
119
|
+
* no row is not one of them: the foreign key refuses that insert and
|
|
120
|
+
* `tryMutation` classifies the refusal as the `constraint_violation` variant of
|
|
121
|
+
* `DbError`.
|
|
122
|
+
*/
|
|
123
|
+
export type ThreadInputError = {
|
|
124
|
+
readonly type: "parent_analysis_mismatch";
|
|
125
|
+
readonly op: string;
|
|
126
|
+
readonly threadId: string;
|
|
127
|
+
readonly analysisId: string;
|
|
128
|
+
readonly parentThreadId: string;
|
|
129
|
+
readonly parentAnalysisId: string;
|
|
130
|
+
} | {
|
|
131
|
+
readonly type: "parent_anchor_unpaired";
|
|
132
|
+
readonly op: string;
|
|
133
|
+
readonly threadId: string;
|
|
134
|
+
readonly parentThreadId: string | null;
|
|
135
|
+
readonly parentSeq: number | null;
|
|
136
|
+
} | {
|
|
137
|
+
readonly type: "unknown_thread_type";
|
|
138
|
+
readonly op: string;
|
|
139
|
+
readonly threadId: string;
|
|
140
|
+
readonly threadType: string;
|
|
141
|
+
};
|
|
60
142
|
export interface ListThreadsInput {
|
|
61
143
|
readonly analysisId: string;
|
|
62
144
|
readonly page?: number;
|
|
@@ -68,6 +150,20 @@ export interface ListThreadsInput {
|
|
|
68
150
|
* widened back out of.
|
|
69
151
|
*/
|
|
70
152
|
readonly includeArchived?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Narrow to one kind of session, exact match. Omitting it narrows NOTHING —
|
|
155
|
+
* a caller that supplies no filter receives every type, which is what a
|
|
156
|
+
* session picker wants: a user browsing an analysis expects to reach a
|
|
157
|
+
* report session directly.
|
|
158
|
+
*
|
|
159
|
+
* That is the opposite polarity to `includeArchived`, which widens a listing
|
|
160
|
+
* that is narrow by default, and the difference is deliberate. An archived
|
|
161
|
+
* thread is hidden state — showing it takes an explicit ask. A report
|
|
162
|
+
* session is not hidden state, so nothing has to ask for it.
|
|
163
|
+
*/
|
|
164
|
+
readonly type?: ThreadType;
|
|
165
|
+
/** Narrow to one thread's direct children, exact match. Same polarity as `type`: omitting it narrows nothing. */
|
|
166
|
+
readonly parentThreadId?: string;
|
|
71
167
|
}
|
|
72
168
|
export interface ThreadPage {
|
|
73
169
|
readonly threads: Thread[];
|
|
@@ -80,9 +176,16 @@ export interface ThreadStore {
|
|
|
80
176
|
/**
|
|
81
177
|
* Create a thread row. Idempotent on `thread_id` — a second create for an
|
|
82
178
|
* existing id is a no-op that preserves the existing row (including its
|
|
83
|
-
* `created_at`)
|
|
179
|
+
* `created_at`) and reads none of the supplied type, parent, or anchor. A
|
|
180
|
+
* caller that needs those persisted compares them against the returned row.
|
|
181
|
+
* Returns the live row.
|
|
182
|
+
*
|
|
183
|
+
* Refused with a `ThreadInputError`, no row written: a parent belonging to
|
|
184
|
+
* another analysis, a parent supplied without its anchor or an anchor
|
|
185
|
+
* without its parent, and a type outside the closed set. A parent naming no
|
|
186
|
+
* row is the foreign key's refusal and arrives as a `DbError`.
|
|
84
187
|
*/
|
|
85
|
-
createThread(input: CreateThreadInput): ResultAsync<Thread, DbError>;
|
|
188
|
+
createThread(input: CreateThreadInput): ResultAsync<Thread, DbError | ThreadInputError>;
|
|
86
189
|
/** The live thread by id, or `null` if absent or archived. */
|
|
87
190
|
getThread(threadId: string): ResultAsync<Thread | null, DbError>;
|
|
88
191
|
/**
|
|
@@ -91,26 +194,39 @@ export interface ThreadStore {
|
|
|
91
194
|
*/
|
|
92
195
|
updateTitle(threadId: string, title: string): ResultAsync<Thread | null, DbError>;
|
|
93
196
|
/**
|
|
94
|
-
* Soft-delete: stamp `deleted_at`
|
|
95
|
-
*
|
|
96
|
-
*
|
|
197
|
+
* Soft-delete the subtree: stamp `deleted_at` on the named thread and on
|
|
198
|
+
* every descendant reachable through `parent_thread_id`, at any depth, so a
|
|
199
|
+
* hidden thread never leaves a visible child behind. Every row and every one
|
|
200
|
+
* of their messages remain, and any thread in the subtree that was already
|
|
201
|
+
* archived keeps the stamp it got the first time.
|
|
97
202
|
*/
|
|
98
203
|
archiveThread(threadId: string): ResultAsync<void, DbError>;
|
|
99
204
|
/**
|
|
100
|
-
* Clear `deleted_at
|
|
101
|
-
* as they read before the archive
|
|
205
|
+
* Clear `deleted_at` on the named thread ALONE, returning it and its
|
|
206
|
+
* messages to view exactly as they read before the archive; every descendant
|
|
207
|
+
* is left as it was found. No-op on a live or absent thread.
|
|
208
|
+
*
|
|
209
|
+
* The asymmetry against `archiveThread` is what keeps the schema honest. A
|
|
210
|
+
* symmetric cascade would restore a child the user had archived on its own
|
|
211
|
+
* beforehand, so a row would have to record whether a cascade or a
|
|
212
|
+
* deliberate action set its tombstone — a fourth column carrying nothing
|
|
213
|
+
* else. With the asymmetry no row needs that distinction, and every archived
|
|
214
|
+
* thread is recovered the same way: by naming it, which a listing widened
|
|
215
|
+
* with `includeArchived` supplies.
|
|
102
216
|
*/
|
|
103
217
|
unarchiveThread(threadId: string): ResultAsync<void, DbError>;
|
|
104
218
|
/**
|
|
105
|
-
* Reclaim the
|
|
106
|
-
* `
|
|
107
|
-
*
|
|
108
|
-
*
|
|
219
|
+
* Reclaim the subtree's whole footprint: the named thread, every descendant
|
|
220
|
+
* reachable through `parent_thread_id` at any depth, and the `messages` rows
|
|
221
|
+
* of every one of them, removed together in one transaction. Unrecoverable —
|
|
222
|
+
* nothing survives, and no tombstone marks that any of it existed. A
|
|
223
|
+
* `thread_id` with no row succeeds as a no-op.
|
|
109
224
|
*/
|
|
110
225
|
purgeThread(threadId: string): ResultAsync<void, DbError>;
|
|
111
226
|
/**
|
|
112
227
|
* Threads for one analysis, newest-updated first, paginated. Live threads
|
|
113
|
-
* only unless `includeArchived` widens the set
|
|
228
|
+
* only unless `includeArchived` widens the set, and every type under every
|
|
229
|
+
* parent unless `type` / `parentThreadId` narrow it; `total` and `hasMore`
|
|
114
230
|
* always describe whichever set the page was drawn from.
|
|
115
231
|
*/
|
|
116
232
|
listThreads(input: ListThreadsInput): ResultAsync<ThreadPage, DbError>;
|
|
@@ -121,3 +237,4 @@ export interface ThreadStore {
|
|
|
121
237
|
* `cortex_analysis_threads` table is provisioned by the state-init DDL.
|
|
122
238
|
*/
|
|
123
239
|
export declare function createThreadStore(pool: Pool): ThreadStore;
|
|
240
|
+
export {};
|