@theronap/agnoclast-mcp 0.9.146 → 0.9.147

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.
@@ -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; a date-time, YYYY-MM-DDTHH:MM, ONLY when a ' +
174
- 'time of day is actually said; otherwise null do NOT invent or infer a date or a time that was not ' +
175
- 'said. A day with no time is kept as a bare date and treated as all-day), ' +
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; null if the evidence ' +
178
- 'names no time. The date itself is worked out from these words, so copy them, do not rephrase), ' +
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
- const normWs = (x) => x.toLowerCase().replace(/[’']/g, '').replace(/\s+/g, ' ').trim()
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 []
@@ -53,6 +53,31 @@ 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)
@@ -119,7 +144,7 @@ export async function processJob(job, deps) {
119
144
  if (obligations.length === 0) return { outcome: 'none', reviewed: true }
120
145
  const res = await post(job.target, obligations)
121
146
  if (res.action === 'recorded') {
122
- log(`recorded ${res.body?.proposed ?? '?'} of ${obligations.length} as proposals on record ${res.body?.recordId ?? '?'}`)
147
+ log(describeRecorded(res.body, obligations))
123
148
  return { outcome: 'recorded', proposed: res.body?.proposed ?? null }
124
149
  }
125
150
  if (res.action === 'drop') {
@@ -170,7 +195,8 @@ export async function flushParkedObligations(deps) {
170
195
  try { unlinkSync(file) } catch { /* raced with another flush; the write is idempotent */ }
171
196
  flushed += 1
172
197
  log(res.action === 'recorded'
173
- ? `recorded ${res.body?.proposed ?? '?'} parked obligations for ${id} on attempt ${(entry.attempts ?? 0) + 1}`
198
+ ? `recorded ${res.body?.proposed ?? '?'} parked obligations for ${id} on attempt ${(entry.attempts ?? 0) + 1}` +
199
+ (res.body?.undated ? ` — ⚠ ${res.body.undated} left WITHOUT a date` : '')
174
200
  : `DROPPED parked obligations for ${id} — the server answered ${res.status} ${res.body?.error ?? ''}`.trim())
175
201
  continue
176
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.146",
3
+ "version": "0.9.147",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {