@inflexa-ai/harness 0.36.4 → 0.37.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.
|
@@ -74,4 +74,19 @@ export interface CortexMessage {
|
|
|
74
74
|
* zero is a figure, thus it stays a value and it never reads as an absence.
|
|
75
75
|
*/
|
|
76
76
|
durationMs?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Who sent this message — the identity the host named at the append site.
|
|
79
|
+
* Carried on a `user` message only: `role` already names the sender of a
|
|
80
|
+
* reply, and the harness holds no per-turn model name at the store boundary.
|
|
81
|
+
* Absent means that nothing recorded a sender.
|
|
82
|
+
*/
|
|
83
|
+
author?: string;
|
|
84
|
+
/**
|
|
85
|
+
* When the store wrote this message: the START time of the append
|
|
86
|
+
* transaction, thus every message of one append carries the same value and
|
|
87
|
+
* no order holds between two appends. An ISO 8601 string rather than a
|
|
88
|
+
* `Date`, because the wire shape is JSON. Absent means that no stored row
|
|
89
|
+
* backs the message — a live surface builds one before anything is written.
|
|
90
|
+
*/
|
|
91
|
+
createdAt?: string;
|
|
77
92
|
}
|
|
@@ -23,7 +23,20 @@ export function storedMessagesToCortex(messages) {
|
|
|
23
23
|
let append = null;
|
|
24
24
|
for (const row of messages) {
|
|
25
25
|
if (row.displayEnvelope) {
|
|
26
|
-
|
|
26
|
+
// The author and the creation time are facts about the ROW, the same as
|
|
27
|
+
// the rollup below, and the projection holds neither. Both ride the row
|
|
28
|
+
// that opens the append, and the author reaches the `user` messages
|
|
29
|
+
// alone, because `role` already names the sender of a reply. Spread
|
|
30
|
+
// conditionally: an absent value must leave no key, or a consumer that
|
|
31
|
+
// spreads the message acquires one that overwrites a real value. `Date`
|
|
32
|
+
// does not survive the JSON crossing typed, hence the ISO string here.
|
|
33
|
+
const createdAt = row.createdAt === undefined ? {} : { createdAt: row.createdAt.toISOString() };
|
|
34
|
+
const author = row.author === undefined ? {} : { author: row.author };
|
|
35
|
+
append = conversationUIToCortexMessages(row.displayEnvelope.messages).map((message) => ({
|
|
36
|
+
...message,
|
|
37
|
+
...createdAt,
|
|
38
|
+
...(message.role === "user" ? author : {}),
|
|
39
|
+
}));
|
|
27
40
|
out.push(...append);
|
|
28
41
|
}
|
|
29
42
|
// The reported rollup of the turn and its duration both ride the model row that
|
|
@@ -55,6 +55,20 @@ export interface StoredMessage {
|
|
|
55
55
|
* reported no quantity still reads back with the duration that it took.
|
|
56
56
|
*/
|
|
57
57
|
readonly durationMs?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Who sent the user message of the append this row OPENED — present only on a
|
|
60
|
+
* genuine-user-start row of a turn appended with one. A row written before the
|
|
61
|
+
* column existed carries none, because no sender was ever recorded for it.
|
|
62
|
+
*/
|
|
63
|
+
readonly author?: string;
|
|
64
|
+
/**
|
|
65
|
+
* When the store wrote this row: the START time of the append transaction,
|
|
66
|
+
* thus every row of one append holds the same value, and `seq` alone orders
|
|
67
|
+
* rows. The read sets it on every row, because the column is `NOT NULL`. It is
|
|
68
|
+
* optional because a fake or a fixture builds a `StoredMessage` with no row
|
|
69
|
+
* behind it.
|
|
70
|
+
*/
|
|
71
|
+
readonly createdAt?: Date;
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* One atomic append: the exact provider-facing history, the display projection of
|
|
@@ -86,6 +100,16 @@ export interface ConversationTurn {
|
|
|
86
100
|
* reported no quantity still keeps the duration that it took.
|
|
87
101
|
*/
|
|
88
102
|
readonly turnDurationMs?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Who sent the user message of this turn — the identity the host holds at the
|
|
105
|
+
* append site. Stored on the FIRST row of the append, and only when that row
|
|
106
|
+
* is a genuine user start: that row also carries the display projection, thus
|
|
107
|
+
* the write and the transcript replay read one row under one rule. A record
|
|
108
|
+
* append opens on a synthetic row, so it stores no author however a caller
|
|
109
|
+
* spreads one in. The value is NUL-stripped on the way in, the same as the
|
|
110
|
+
* envelope, because a `text` parameter carrying 0x00 fails the statement.
|
|
111
|
+
*/
|
|
112
|
+
readonly author?: string;
|
|
89
113
|
}
|
|
90
114
|
/**
|
|
91
115
|
* The append a host uses to record out-of-band work in a thread — an analysis
|
|
@@ -181,7 +181,7 @@ export const EVICTION_BLOCK_TURNS = 4;
|
|
|
181
181
|
*/
|
|
182
182
|
export function createThreadHistory(pool, logger) {
|
|
183
183
|
function appendTurn(threadId, turn) {
|
|
184
|
-
const { modelMessages: messages, displayMessages, turnUsage, turnDurationMs } = turn;
|
|
184
|
+
const { modelMessages: messages, displayMessages, turnUsage, turnDurationMs, author } = turn;
|
|
185
185
|
if (messages.length === 0)
|
|
186
186
|
return okVoid();
|
|
187
187
|
// The display projection rides the append's FIRST row, so one SELECT of a
|
|
@@ -206,6 +206,13 @@ export function createThreadHistory(pool, logger) {
|
|
|
206
206
|
// the turn. -1 (a turn that persisted no reply — an abort before any output)
|
|
207
207
|
// matches no index below, so nothing is written and the append still succeeds.
|
|
208
208
|
const lastAssistantRow = messages.reduce((last, m, i) => (m.role === "assistant" ? i : last), -1);
|
|
209
|
+
// The author rides the row the display projection rides, and only when that
|
|
210
|
+
// row is a message a person actually sent — an assistant row, a tool row,
|
|
211
|
+
// and the synthetic opening row of a record append are not. NUL is dropped
|
|
212
|
+
// rather than refused: a 0x00 byte in a `text` parameter fails the
|
|
213
|
+
// statement, and one byte in a label would roll the whole turn back.
|
|
214
|
+
const turnAuthor = author === undefined ? null : stripNulCharacters(author);
|
|
215
|
+
const authorRow = messages[0] !== undefined && isGenuineUserStart(messages[0]) ? 0 : -1;
|
|
209
216
|
return withTransaction(pool, "thread-history.appendTurn", (client) =>
|
|
210
217
|
// Serialize concurrent appends on this thread — without the lock, two
|
|
211
218
|
// transactions can both read the same MAX(seq) and collide on the
|
|
@@ -224,14 +231,15 @@ export function createThreadHistory(pool, logger) {
|
|
|
224
231
|
// `message_envelope::json`, never `::jsonb`; `display_envelope` and
|
|
225
232
|
// `reported_usage::jsonb`, never `::json` — see the column comments
|
|
226
233
|
// in the state-init DDL.
|
|
227
|
-
`INSERT INTO messages (thread_id, seq, message_envelope, display_envelope, tokens, reported_usage, turn_duration_ms)
|
|
228
|
-
VALUES ($1, $2, $3::json, $4::jsonb, $5, $6::jsonb, $7)
|
|
234
|
+
`INSERT INTO messages (thread_id, seq, message_envelope, display_envelope, tokens, reported_usage, turn_duration_ms, author)
|
|
235
|
+
VALUES ($1, $2, $3::json, $4::jsonb, $5, $6::jsonb, $7, $8)
|
|
229
236
|
ON CONFLICT (thread_id, seq) DO UPDATE
|
|
230
237
|
SET message_envelope = EXCLUDED.message_envelope,
|
|
231
238
|
display_envelope = EXCLUDED.display_envelope,
|
|
232
239
|
tokens = EXCLUDED.tokens,
|
|
233
240
|
reported_usage = EXCLUDED.reported_usage,
|
|
234
|
-
turn_duration_ms = EXCLUDED.turn_duration_ms
|
|
241
|
+
turn_duration_ms = EXCLUDED.turn_duration_ms,
|
|
242
|
+
author = EXCLUDED.author`, [
|
|
235
243
|
threadId,
|
|
236
244
|
startSeq + i,
|
|
237
245
|
serializeEnvelope(message),
|
|
@@ -239,6 +247,7 @@ export function createThreadHistory(pool, logger) {
|
|
|
239
247
|
countTokens(message.content),
|
|
240
248
|
i === lastAssistantRow ? rollup : null,
|
|
241
249
|
i === lastAssistantRow ? duration : null,
|
|
250
|
+
i === authorRow ? turnAuthor : null,
|
|
242
251
|
])).map(() => undefined)), okVoid()))
|
|
243
252
|
.andThen(() =>
|
|
244
253
|
// Thread listings sort on `cortex_analysis_threads.updated_at`, and
|
|
@@ -368,12 +377,14 @@ export function createThreadHistory(pool, logger) {
|
|
|
368
377
|
// `seq::text AS seq` output alias (Postgres resolves an unqualified
|
|
369
378
|
// ORDER BY name to the output column), sorting the bigint as text:
|
|
370
379
|
// "10" before "2". The qualified name forces the bigint column.
|
|
371
|
-
`SELECT seq::text AS seq, message_envelope, display_envelope, reported_usage, turn_duration_ms::text AS turn_duration_ms
|
|
380
|
+
`SELECT seq::text AS seq, message_envelope, display_envelope, reported_usage, turn_duration_ms::text AS turn_duration_ms, author, created_at
|
|
372
381
|
FROM messages WHERE thread_id = $1
|
|
373
382
|
ORDER BY messages.seq ASC`, [threadId]);
|
|
374
383
|
// Spread rather than `usage: r.reported_usage ?? undefined`, so a row with
|
|
375
384
|
// no rollup carries no `usage` KEY at all — absent, not present-and-undefined.
|
|
376
|
-
// The duration
|
|
385
|
+
// The duration and the author obey the same rule, each from its own column.
|
|
386
|
+
// The creation time is unconditional: its column is NOT NULL, thus it has no
|
|
387
|
+
// absence to represent.
|
|
377
388
|
const stored = await Promise.all(rows.map(async (r) => {
|
|
378
389
|
const envelope = parseStoredMessageEnvelope(r.message_envelope, `${threadId}/${r.seq}`);
|
|
379
390
|
const displayEnvelope = r.display_envelope == null ? undefined : await parseStoredDisplayEnvelope(r.display_envelope, `${threadId}/${r.seq}/display`, logger);
|
|
@@ -384,6 +395,8 @@ export function createThreadHistory(pool, logger) {
|
|
|
384
395
|
...(displayEnvelope ? { displayEnvelope } : {}),
|
|
385
396
|
...(r.reported_usage === null ? {} : { usage: r.reported_usage }),
|
|
386
397
|
...(r.turn_duration_ms === null ? {} : { durationMs: Number(r.turn_duration_ms) }),
|
|
398
|
+
...(r.author === null ? {} : { author: r.author }),
|
|
399
|
+
createdAt: r.created_at,
|
|
387
400
|
};
|
|
388
401
|
}));
|
|
389
402
|
return groupTurns(stored, (row) => isGenuineUserStart(row.message));
|
package/dist/state/init.js
CHANGED
|
@@ -270,6 +270,13 @@ CREATE INDEX IF NOT EXISTS idx_cortex_regulatory_chunks_metadata
|
|
|
270
270
|
-- Nullable with no default: absent means that nobody measured the turn, and a
|
|
271
271
|
-- zero would claim that a turn took no time. A row written before this column
|
|
272
272
|
-- existed is absent for the same reason, thus the migration needs no backfill.
|
|
273
|
+
--
|
|
274
|
+
-- author names the identity that sent the user message of a turn, as the host
|
|
275
|
+
-- knew it at the append site. It is written on the genuine-user-start row of an
|
|
276
|
+
-- append only -- the row that carries display_envelope -- and never on an
|
|
277
|
+
-- assistant, tool, or synthetic row. Nullable with no default and no backfill:
|
|
278
|
+
-- absent means that no sender was recorded, which is the honest value both for a
|
|
279
|
+
-- row that predates the column and for a turn appended without one.
|
|
273
280
|
CREATE TABLE IF NOT EXISTS messages (
|
|
274
281
|
thread_id TEXT NOT NULL,
|
|
275
282
|
seq BIGINT NOT NULL,
|
|
@@ -282,6 +289,7 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
282
289
|
tokens INTEGER NOT NULL,
|
|
283
290
|
reported_usage JSONB,
|
|
284
291
|
turn_duration_ms BIGINT,
|
|
292
|
+
author TEXT,
|
|
285
293
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
286
294
|
PRIMARY KEY (thread_id, seq)
|
|
287
295
|
);
|
|
@@ -547,6 +555,9 @@ export async function initCortexState(pool, injected) {
|
|
|
547
555
|
// and every earlier message reads back with no duration — which is the
|
|
548
556
|
// honest value, because nobody measured those turns.
|
|
549
557
|
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS turn_duration_ms BIGINT",
|
|
558
|
+
// Who sent the user message of the turn (see the CREATE TABLE comment).
|
|
559
|
+
// Purely additive: nullable with no default, thus the column rewrites no row.
|
|
560
|
+
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS author TEXT",
|
|
550
561
|
// Demote an existing JSONB envelope column to JSON (see the CREATE
|
|
551
562
|
// TABLE comment). Type-guarded because ALTER ... TYPE rewrites the
|
|
552
563
|
// whole table and this runs at every boot; scoped to
|
package/package.json
CHANGED