@theronap/agnoclast-mcp 0.9.147 → 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/obligation_review.mjs +23 -0
- package/lib/obligations_worker.mjs +13 -8
- 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
|
|
|
@@ -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
|
|
@@ -84,13 +84,14 @@ export function backoffMs(attempts) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/** POST the candidates. Never throws: a network failure is `retry`, like a 5xx. */
|
|
87
|
-
export async function postCandidates({ base, token, target, obligations, fetchImpl }) {
|
|
87
|
+
export async function postCandidates({ base, token, target, obligations, retired = [], fetchImpl }) {
|
|
88
88
|
if (!token) return { action: 'retry', status: 0, body: { error: 'no CORTEX_TOKEN in env or wired config' } }
|
|
89
89
|
try {
|
|
90
90
|
const res = await fetchImpl(`${base}/api/obligations/candidates`, {
|
|
91
91
|
method: 'POST',
|
|
92
92
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
93
|
-
|
|
93
|
+
// retiredObligations: quotes the reviewer retired (ADR-0059 §5.4). Omitted when empty.
|
|
94
|
+
body: JSON.stringify({ ...target, obligations, ...(retired.length ? { retiredObligations: retired } : {}) }),
|
|
94
95
|
})
|
|
95
96
|
const body = await res.json().catch(() => ({}))
|
|
96
97
|
return { action: classifyPost(res.status), status: res.status, body }
|
|
@@ -138,11 +139,15 @@ export async function processJob(job, deps) {
|
|
|
138
139
|
// drop, merge or reclassify, never add; it fails open. Optional here so a caller without it is unchanged.
|
|
139
140
|
const review = deps.review ?? ((o) => ({ obligations: o, reviewed: false, reason: 'no reviewer' }))
|
|
140
141
|
const before = obligations.length
|
|
142
|
+
const cands = obligations
|
|
141
143
|
const rev = review(obligations, job.text)
|
|
142
144
|
log(describeReview(before, rev))
|
|
143
145
|
obligations = rev.obligations
|
|
144
|
-
|
|
145
|
-
|
|
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)
|
|
146
151
|
if (res.action === 'recorded') {
|
|
147
152
|
log(describeRecorded(res.body, obligations))
|
|
148
153
|
return { outcome: 'recorded', proposed: res.body?.proposed ?? null }
|
|
@@ -152,7 +157,7 @@ export async function processJob(job, deps) {
|
|
|
152
157
|
return { outcome: 'dropped', status: res.status }
|
|
153
158
|
}
|
|
154
159
|
const t = now()
|
|
155
|
-
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) })
|
|
156
161
|
log(`parked ${obligations.length} — ${res.action === 'pending' ? 'the unit is not materialised yet' : `${res.status} ${res.body?.error ?? ''}`.trim()}; retried by later captures`)
|
|
157
162
|
return { outcome: 'parked', status: res.status }
|
|
158
163
|
}
|
|
@@ -190,7 +195,7 @@ export async function flushParkedObligations(deps) {
|
|
|
190
195
|
continue
|
|
191
196
|
}
|
|
192
197
|
if ((entry.nextAttemptAt ?? 0) > t) continue
|
|
193
|
-
const res = await post(entry.target, entry.obligations)
|
|
198
|
+
const res = await post(entry.target, entry.obligations, entry.retired ?? [])
|
|
194
199
|
if (res.action === 'recorded' || res.action === 'drop') {
|
|
195
200
|
try { unlinkSync(file) } catch { /* raced with another flush; the write is idempotent */ }
|
|
196
201
|
flushed += 1
|
|
@@ -260,7 +265,7 @@ async function realDeps(target) {
|
|
|
260
265
|
extract: extractSession,
|
|
261
266
|
lastSkip: lastEdgeSkip,
|
|
262
267
|
review: (obligations, text) => reviewObligations(obligations, text),
|
|
263
|
-
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 }),
|
|
264
269
|
park: (entry) => writeParked(parkDir(), entry),
|
|
265
270
|
log: logger(target),
|
|
266
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