@theronap/agnoclast-mcp 0.9.146 → 0.9.148
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/lib/capture.mjs +7 -1
- package/lib/edge_extract.mjs +19 -6
- package/lib/obligation_review.mjs +23 -0
- package/lib/obligations_worker.mjs +41 -10
- package/lib/server.mjs +1 -1
- package/package.json +1 -1
package/lib/capture.mjs
CHANGED
|
@@ -404,10 +404,14 @@ async function captureWork(stdinRaw) {
|
|
|
404
404
|
// merge a deadline stated twice, reclassify whose it is. Never adds; fails open (obligation_review.mjs).
|
|
405
405
|
// Only when there are candidates, which most sessions never produce.
|
|
406
406
|
let obligations = extracted?.obligations ?? []
|
|
407
|
+
// The quotes the review retired (merged into a later statement, or not owed). The ONLY thing that lets
|
|
408
|
+
// the server delete an earlier read's proposal; a read that is merely silent about one deletes nothing.
|
|
409
|
+
let retiredObligations = []
|
|
407
410
|
if (obligations.length) {
|
|
408
|
-
const { reviewObligations, describeReview } = await import('./obligation_review.mjs')
|
|
411
|
+
const { reviewObligations, describeReview, retiredEvidence } = await import('./obligation_review.mjs')
|
|
409
412
|
const rev = reviewObligations(obligations, transcript)
|
|
410
413
|
process.stderr.write(`cortex: ${stamp} ${sid} obligations ${describeReview(obligations.length, rev)}\n`)
|
|
414
|
+
retiredObligations = retiredEvidence(obligations, rev)
|
|
411
415
|
obligations = rev.obligations
|
|
412
416
|
}
|
|
413
417
|
// `pages` is the edge's PROPOSAL of where this record belongs (ADR-0055). Free-text names, resolved
|
|
@@ -424,6 +428,8 @@ async function captureWork(stdinRaw) {
|
|
|
424
428
|
// transcript it was shown — the server cannot re-check the quote (it never receives the
|
|
425
429
|
// transcript), so these arrive already filtered. Omitted when empty, same as `pages`.
|
|
426
430
|
...(obligations.length ? { obligations } : {}),
|
|
431
|
+
// ADR-0059 §5.4: quotes the reviewer retired. Omitted when empty, so an older server sees nothing new.
|
|
432
|
+
...(retiredObligations.length ? { retiredObligations } : {}),
|
|
427
433
|
}
|
|
428
434
|
: { ...common, transcript }
|
|
429
435
|
|
package/lib/edge_extract.mjs
CHANGED
|
@@ -167,15 +167,26 @@ const PAGES_FRAGMENT =
|
|
|
167
167
|
// ⚠ `evidence` IS VERIFIED HERE, NOT SERVER-SIDE (ADR-0059 §4.1). The cloud receives only the derived
|
|
168
168
|
// digest and never the transcript, so there is nothing server-side to compare a quote against. The
|
|
169
169
|
// check is `verbatimIn()` below, run against the SAME slice the model was shown.
|
|
170
|
+
//
|
|
171
|
+
// ⚠ "THE 15TH" IS A STATED DAY (2026-09-11). The old wording — "otherwise null — do NOT invent or infer
|
|
172
|
+
// a date … that was not said" — left a model free to read a day with no month as "not said", and one
|
|
173
|
+
// run of the STRAT 421 lecture did exactly that: no due_at AND no due_phrase for "on the 15th" and
|
|
174
|
+
// "by Tuesday the 22nd", while a repeat of the same run filled both. The phrase is what the server
|
|
175
|
+
// resolves, so it is now REQUIRED whenever the evidence says when. The server also reads the verbatim
|
|
176
|
+
// quote itself when a candidate arrives with neither (resolveDueFromQuote in web/lib/engine/due_phrase.ts),
|
|
177
|
+
// so this wording lowers how often that net is needed rather than being the only thing holding the date.
|
|
170
178
|
const OBLIGATIONS_FRAGMENT =
|
|
171
179
|
'"obligations" (array of things someone must DO by a date, stated in this session. Each an object ' +
|
|
172
180
|
'with "subject" (what must be done, one short phrase), ' +
|
|
173
|
-
'"due_at" (a DATE, YYYY-MM-DD, when only a day is stated
|
|
174
|
-
'
|
|
175
|
-
'
|
|
181
|
+
'"due_at" (a DATE, YYYY-MM-DD, when only a day is stated — and a day said without its month or ' +
|
|
182
|
+
'year, like "the 15th", IS a stated day: complete it from the record\'s own date; a date-time, ' +
|
|
183
|
+
'YYYY-MM-DDTHH:MM, ONLY when a ' +
|
|
184
|
+
'time of day is actually said; null only when no day is stated at all — never invent a day or a ' +
|
|
185
|
+
'time the evidence does not state. A day with no time is kept as a bare date and treated as all-day), ' +
|
|
176
186
|
'"due_phrase" (the exact words that say WHEN it is due — "on the 15th", "by Tuesday the 22nd", ' +
|
|
177
|
-
'"Saturday", "by 5 pm tomorrow" — copied VERBATIM from the evidence sentence
|
|
178
|
-
'
|
|
187
|
+
'"Saturday", "by 5 pm tomorrow" — copied VERBATIM from the evidence sentence. REQUIRED whenever the ' +
|
|
188
|
+
'evidence says when, even when it leaves out the month or year; null ONLY if the evidence names no ' +
|
|
189
|
+
'time at all. The date itself is worked out from these words, so copy them, do not rephrase), ' +
|
|
179
190
|
'"evidence" (the sentence from the session that says so, copied EXACTLY and VERBATIM — it is ' +
|
|
180
191
|
'checked against the transcript and the whole item is discarded if it does not match), ' +
|
|
181
192
|
'"obligated_party" (exactly one of: "self" if the person whose session this is must do it; ' +
|
|
@@ -285,7 +296,9 @@ export function verbatimIn(needle, haystack) {
|
|
|
285
296
|
* `obligated_party` outside the preset is malformed and goes — the server's CHECK constraint would
|
|
286
297
|
* reject it anyway, and failing here means the record still lands with its other keys intact.
|
|
287
298
|
*/
|
|
288
|
-
|
|
299
|
+
// Punctuation is spacing here too: "Tuesday, the 22nd" is the same words as "Tuesday the 22nd", and a
|
|
300
|
+
// phrase dropped over a comma leaves the server with only due_at — the gap a phrase exists to close.
|
|
301
|
+
const normWs = (x) => x.toLowerCase().replace(/[’']/g, '').replace(/[,.;:!?"“”()]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
289
302
|
|
|
290
303
|
export function keepVerifiableObligations(items, shown) {
|
|
291
304
|
if (!Array.isArray(items)) return []
|
|
@@ -172,6 +172,29 @@ export function reviewObligations(cands, text, opts = {}) {
|
|
|
172
172
|
return { obligations: kept, reviewed: true, changes, model }
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* PURE. The quotes this review RETIRED: candidates it merged into a later statement of the same item, or
|
|
177
|
+
* dropped as not an obligation. A quote it kept is never retired, even if another decision named it.
|
|
178
|
+
*
|
|
179
|
+
* 🔴 THIS IS THE ONLY THING THAT MAY DELETE AN EARLIER READ'S PROPOSAL (ADR-0059 §5.4, 2026-09-11). A read
|
|
180
|
+
* that simply does not mention a proposal says nothing about it: the model may have missed it this run, may
|
|
181
|
+
* have paraphrased its quote so the verbatim gate dropped it, or — in a session past 400,000 characters — may
|
|
182
|
+
* never have been SHOWN it (summaryWindow elides the middle). A reviewer that read the sentence in context
|
|
183
|
+
* and judged it superseded or not owed is a statement. So the server deletes exactly these, and nothing for
|
|
184
|
+
* silence. Empty when the review did not run: an unreviewed read retires nothing.
|
|
185
|
+
*/
|
|
186
|
+
export function retiredEvidence(cands, rev) {
|
|
187
|
+
if (!rev?.reviewed || !Array.isArray(cands)) return []
|
|
188
|
+
const kept = new Set((rev.obligations ?? []).map((o) => o?.evidence))
|
|
189
|
+
const idx = [...(rev.changes?.dropped ?? []), ...(rev.changes?.merged ?? []).map(([i]) => i)]
|
|
190
|
+
const out = []
|
|
191
|
+
for (const i of idx) {
|
|
192
|
+
const ev = cands[i]?.evidence
|
|
193
|
+
if (typeof ev === 'string' && ev && !kept.has(ev) && !out.includes(ev)) out.push(ev)
|
|
194
|
+
}
|
|
195
|
+
return out
|
|
196
|
+
}
|
|
197
|
+
|
|
175
198
|
/** PURE. One log line for a review outcome. */
|
|
176
199
|
export function describeReview(before, rev) {
|
|
177
200
|
if (!rev.reviewed) return `review skipped [${rev.reason}] — ${before} candidate(s) go on unreviewed`
|
|
@@ -25,7 +25,7 @@ import { mkdirSync, openSync, readdirSync, readFileSync, writeFileSync, renameSy
|
|
|
25
25
|
import { homedir } from 'node:os'
|
|
26
26
|
import { join, dirname } from 'node:path'
|
|
27
27
|
import { fileURLToPath } from 'node:url'
|
|
28
|
-
import { reviewObligations, describeReview } from './obligation_review.mjs'
|
|
28
|
+
import { reviewObligations, describeReview, retiredEvidence } from './obligation_review.mjs'
|
|
29
29
|
|
|
30
30
|
export const MAX_PARK_DAYS = 30
|
|
31
31
|
export const FIRST_RETRY_MS = 5 * 60_000
|
|
@@ -53,19 +53,45 @@ export function classifyPost(status) {
|
|
|
53
53
|
return 'retry'
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* PURE. The log line for a recorded post: what the server did with the candidates, and what DATE
|
|
58
|
+
* information each one carried.
|
|
59
|
+
*
|
|
60
|
+
* 🔴 WHY (2026-09-11). A re-run of a lecture logged "recorded 2 of 3 as proposals" while two of the three
|
|
61
|
+
* had landed with no due date — one having lost the date an earlier read stored. Nothing on this machine
|
|
62
|
+
* said so; it took a query against the table, and what the model had sent was unrecoverable. So the line
|
|
63
|
+
* now carries the server's counts and, per candidate, its SHAPE — phrase, date, both, or neither. Never the
|
|
64
|
+
* subject or the quote: capture.log is created with default permissions, unlike the 0600 parked files,
|
|
65
|
+
* and a candidate's words are the person's content.
|
|
66
|
+
*/
|
|
67
|
+
export function describeRecorded(body, sent) {
|
|
68
|
+
const b = body ?? {}
|
|
69
|
+
const counts = [`${b.proposed ?? '?'} new`]
|
|
70
|
+
// An older server answers without these; say nothing rather than "undefined refreshed".
|
|
71
|
+
if (b.refreshed !== undefined) counts.push(`${b.refreshed} refreshed`)
|
|
72
|
+
if (b.replaced !== undefined) counts.push(`${b.replaced} replaced`)
|
|
73
|
+
const shape = (o) => (o?.due_phrase ? (o.due_at ? 'phrase+date' : 'phrase') : o?.due_at ? 'date' : 'NO date')
|
|
74
|
+
const list = Array.isArray(sent) ? sent : []
|
|
75
|
+
let line = `recorded ${list.length} on record ${b.recordId ?? '?'}: ${counts.join(', ')} — sent ${list.map((o, i) => `#${i} ${shape(o)}`).join(', ')}`
|
|
76
|
+
if (b.fromQuote) line += `; ${b.fromQuote} dated from the quote`
|
|
77
|
+
if (b.undated) line += `; ⚠ ${b.undated} left WITHOUT a date`
|
|
78
|
+
return line
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
/** PURE. The next retry delay: 5 min, doubling, capped at 6 h. */
|
|
57
82
|
export function backoffMs(attempts) {
|
|
58
83
|
return Math.min(FIRST_RETRY_MS * 2 ** Math.max(0, attempts - 1), MAX_RETRY_MS)
|
|
59
84
|
}
|
|
60
85
|
|
|
61
86
|
/** POST the candidates. Never throws: a network failure is `retry`, like a 5xx. */
|
|
62
|
-
export async function postCandidates({ base, token, target, obligations, fetchImpl }) {
|
|
87
|
+
export async function postCandidates({ base, token, target, obligations, retired = [], fetchImpl }) {
|
|
63
88
|
if (!token) return { action: 'retry', status: 0, body: { error: 'no CORTEX_TOKEN in env or wired config' } }
|
|
64
89
|
try {
|
|
65
90
|
const res = await fetchImpl(`${base}/api/obligations/candidates`, {
|
|
66
91
|
method: 'POST',
|
|
67
92
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
68
|
-
|
|
93
|
+
// retiredObligations: quotes the reviewer retired (ADR-0059 §5.4). Omitted when empty.
|
|
94
|
+
body: JSON.stringify({ ...target, obligations, ...(retired.length ? { retiredObligations: retired } : {}) }),
|
|
69
95
|
})
|
|
70
96
|
const body = await res.json().catch(() => ({}))
|
|
71
97
|
return { action: classifyPost(res.status), status: res.status, body }
|
|
@@ -113,13 +139,17 @@ export async function processJob(job, deps) {
|
|
|
113
139
|
// drop, merge or reclassify, never add; it fails open. Optional here so a caller without it is unchanged.
|
|
114
140
|
const review = deps.review ?? ((o) => ({ obligations: o, reviewed: false, reason: 'no reviewer' }))
|
|
115
141
|
const before = obligations.length
|
|
142
|
+
const cands = obligations
|
|
116
143
|
const rev = review(obligations, job.text)
|
|
117
144
|
log(describeReview(before, rev))
|
|
118
145
|
obligations = rev.obligations
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
// What the review RETIRED is the only thing that may delete an earlier read's proposal (retiredEvidence).
|
|
147
|
+
// So a read whose every candidate was retired still posts: the retirement is the whole message.
|
|
148
|
+
const retired = retiredEvidence(cands, rev)
|
|
149
|
+
if (obligations.length === 0 && retired.length === 0) return { outcome: 'none', reviewed: true }
|
|
150
|
+
const res = await post(job.target, obligations, retired)
|
|
121
151
|
if (res.action === 'recorded') {
|
|
122
|
-
log(
|
|
152
|
+
log(describeRecorded(res.body, obligations))
|
|
123
153
|
return { outcome: 'recorded', proposed: res.body?.proposed ?? null }
|
|
124
154
|
}
|
|
125
155
|
if (res.action === 'drop') {
|
|
@@ -127,7 +157,7 @@ export async function processJob(job, deps) {
|
|
|
127
157
|
return { outcome: 'dropped', status: res.status }
|
|
128
158
|
}
|
|
129
159
|
const t = now()
|
|
130
|
-
park({ target: job.target, title: job.title ?? null, obligations, parkedAt: t, attempts: 1, nextAttemptAt: t + backoffMs(1) })
|
|
160
|
+
park({ target: job.target, title: job.title ?? null, obligations, ...(retired.length ? { retired } : {}), parkedAt: t, attempts: 1, nextAttemptAt: t + backoffMs(1) })
|
|
131
161
|
log(`parked ${obligations.length} — ${res.action === 'pending' ? 'the unit is not materialised yet' : `${res.status} ${res.body?.error ?? ''}`.trim()}; retried by later captures`)
|
|
132
162
|
return { outcome: 'parked', status: res.status }
|
|
133
163
|
}
|
|
@@ -165,12 +195,13 @@ export async function flushParkedObligations(deps) {
|
|
|
165
195
|
continue
|
|
166
196
|
}
|
|
167
197
|
if ((entry.nextAttemptAt ?? 0) > t) continue
|
|
168
|
-
const res = await post(entry.target, entry.obligations)
|
|
198
|
+
const res = await post(entry.target, entry.obligations, entry.retired ?? [])
|
|
169
199
|
if (res.action === 'recorded' || res.action === 'drop') {
|
|
170
200
|
try { unlinkSync(file) } catch { /* raced with another flush; the write is idempotent */ }
|
|
171
201
|
flushed += 1
|
|
172
202
|
log(res.action === 'recorded'
|
|
173
|
-
? `recorded ${res.body?.proposed ?? '?'} parked obligations for ${id} on attempt ${(entry.attempts ?? 0) + 1}`
|
|
203
|
+
? `recorded ${res.body?.proposed ?? '?'} parked obligations for ${id} on attempt ${(entry.attempts ?? 0) + 1}` +
|
|
204
|
+
(res.body?.undated ? ` — ⚠ ${res.body.undated} left WITHOUT a date` : '')
|
|
174
205
|
: `DROPPED parked obligations for ${id} — the server answered ${res.status} ${res.body?.error ?? ''}`.trim())
|
|
175
206
|
continue
|
|
176
207
|
}
|
|
@@ -234,7 +265,7 @@ async function realDeps(target) {
|
|
|
234
265
|
extract: extractSession,
|
|
235
266
|
lastSkip: lastEdgeSkip,
|
|
236
267
|
review: (obligations, text) => reviewObligations(obligations, text),
|
|
237
|
-
post: (t, obligations) => postCandidates({ base, token, target: t, obligations, fetchImpl: fetchCortex }),
|
|
268
|
+
post: (t, obligations, retired) => postCandidates({ base, token, target: t, obligations, retired, fetchImpl: fetchCortex }),
|
|
238
269
|
park: (entry) => writeParked(parkDir(), entry),
|
|
239
270
|
log: logger(target),
|
|
240
271
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
package/lib/server.mjs
CHANGED
|
@@ -933,7 +933,7 @@ function renderNudge(payload) {
|
|
|
933
933
|
inputSchema: {
|
|
934
934
|
intakeItemId: z.string().describe('intake item uuid'),
|
|
935
935
|
orgId: z.string().describe('destination brain org uuid'),
|
|
936
|
-
documentIds: z.array(z.string()).optional().describe('additional
|
|
936
|
+
documentIds: z.array(z.string()).optional().describe('additional pages in orgId — each a page `ref:` exactly as read_page shows it, or a brain_documents id (deterministic homes are merged automatically)'),
|
|
937
937
|
title: z.string().optional(),
|
|
938
938
|
summary: z.string().optional(),
|
|
939
939
|
source: z.string().optional(),
|
package/package.json
CHANGED