@tpsdev-ai/flair 0.45.0 → 0.47.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/config.yaml +35 -2
- package/dist/build-info.json +6 -0
- package/dist/cli.js +847 -168
- package/dist/doctor-client.js +358 -11
- package/dist/federation/scheduler.js +114 -9
- package/dist/hook-install.js +150 -1
- package/dist/install/global-bin-path.js +234 -0
- package/dist/lib/entity-vocab-cli.js +113 -0
- package/dist/lib/mcp-enable.js +71 -21
- package/dist/lib/scheduler-platform.js +363 -1
- package/dist/postinstall.cjs +88 -0
- package/dist/rem/runner.js +177 -10
- package/dist/rem/scheduler.js +126 -20
- package/dist/resources/AttentionQuery.js +5 -3
- package/dist/resources/AutoPromoteCandidates.js +18 -12
- package/dist/resources/Federation.js +49 -5
- package/dist/resources/Memory.js +36 -2
- package/dist/resources/MemoryBootstrap.js +118 -7
- package/dist/resources/MemoryMaintenance.js +8 -2
- package/dist/resources/MemoryReflect.js +70 -5
- package/dist/resources/auto-promote-lib.js +46 -0
- package/dist/resources/build-info.js +50 -0
- package/dist/resources/entity-vocab.js +25 -1
- package/dist/resources/health.js +25 -5
- package/dist/resources/mcp-oauth-flag.js +20 -0
- package/dist/resources/mcp-oauth.js +6 -1
- package/dist/resources/mcp-tools.js +53 -3
- package/dist/resources/memory-reflect-lib.js +201 -4
- package/dist/src/lib/scheduler-platform.js +363 -1
- package/dist/src/rem/scheduler.js +126 -20
- package/docs/deepseek-harness.md +110 -0
- package/docs/entity-vocabulary.md +15 -0
- package/docs/integrations.md +1 -0
- package/docs/mcp-clients.md +4 -0
- package/docs/notes/mcp-oauth-model2.md +52 -3
- package/package.json +5 -4
- package/schemas/memory.graphql +12 -0
- package/templates/bin/flair-federation-sync.sh.tmpl +8 -1
- package/templates/bin/flair-rem-nightly.sh.tmpl +8 -1
|
@@ -123,7 +123,12 @@ export async function registerMcpOAuthRoute(deps = {}) {
|
|
|
123
123
|
return decide({
|
|
124
124
|
mounted: false,
|
|
125
125
|
status: "Not enabled",
|
|
126
|
-
|
|
126
|
+
// "true" not "1": flair's flag accepts either, but the component's
|
|
127
|
+
// config-side read of the same var (config.yaml `mcp.enabled:
|
|
128
|
+
// ${FLAIR_MCP_OAUTH}`, flair#1152) accepts ONLY "true"/"false" — with
|
|
129
|
+
// "1" the /mcp route registers and every request 401s against a
|
|
130
|
+
// component that never mounted its AS.
|
|
131
|
+
reason: "Set FLAIR_MCP_OAUTH=true (and an issuer) to serve MCP over HTTP.",
|
|
127
132
|
});
|
|
128
133
|
}
|
|
129
134
|
// Boot guard (flair#1021): fail loudly if the operator enabled the flag but
|
|
@@ -788,6 +788,11 @@ export const TOOLS = {
|
|
|
788
788
|
"sections", "tokenEstimate", "maxTokens", "memoriesIncluded", "memoriesAvailable",
|
|
789
789
|
"memoriesTruncated", "teammateFindingsIncluded", "teammateFindingsTruncated",
|
|
790
790
|
"teammateFindingsMatched", "context", "flairVersion",
|
|
791
|
+
// flair#1270 — the payload token LEDGER: every token-charged content
|
|
792
|
+
// class has a counter, so tokenEstimate ≈ scaffoldTokens + soulTokens +
|
|
793
|
+
// memoryTokens + trustTokens + eventsTokens decomposes from the payload
|
|
794
|
+
// alone (see the identity block in MemoryBootstrap's response tail).
|
|
795
|
+
"soulTokens", "memoryTokens", "trustTokens", "eventsTokens", "scaffoldTokens",
|
|
791
796
|
],
|
|
792
797
|
fieldTypes: {
|
|
793
798
|
agentId: "string", soul: "object", memories: "array", predicted: "array",
|
|
@@ -796,6 +801,8 @@ export const TOOLS = {
|
|
|
796
801
|
memoriesAvailable: "number", memoriesTruncated: "number",
|
|
797
802
|
teammateFindingsIncluded: "number", teammateFindingsTruncated: "number",
|
|
798
803
|
teammateFindingsMatched: "number", context: "string", flairVersion: "string",
|
|
804
|
+
soulTokens: "number", memoryTokens: "number", trustTokens: "number",
|
|
805
|
+
eventsTokens: "number", scaffoldTokens: "number",
|
|
799
806
|
},
|
|
800
807
|
invariants: {
|
|
801
808
|
// count == delivered — the historical count/charge/deliver drift.
|
|
@@ -822,12 +829,55 @@ export const TOOLS = {
|
|
|
822
829
|
// tolerance covers the fixed JSON scaffolding + the #1207 prose-vs-
|
|
823
830
|
// structured charge gap; uncounted content does not fit under it.
|
|
824
831
|
budgetCap: { estimate: "tokenEstimate", budget: "maxTokens", tolerance: 0.25 },
|
|
825
|
-
// #
|
|
826
|
-
//
|
|
832
|
+
// flair#1290 step 4 — the #1270 token-ledger identity, enforced:
|
|
833
|
+
// tokenEstimate ≈ scaffold + soul + memory + trust + events, two-sided
|
|
834
|
+
// (constants sized in bootstrap-token-ledger-1270.test.ts, which reads
|
|
835
|
+
// them from HERE — one identity, one tolerance definition). The upper
|
|
836
|
+
// bound is waived when the request opted into the prose mirror
|
|
837
|
+
// (includeContext), which legitimately widens the gap by design.
|
|
838
|
+
tokenDecomposition: {
|
|
839
|
+
total: "tokenEstimate",
|
|
840
|
+
terms: ["scaffoldTokens", "soulTokens", "memoryTokens", "trustTokens", "eventsTokens"],
|
|
841
|
+
perItemContainers: ["memories", "predicted", "teammateFindings"],
|
|
842
|
+
perItemGap: 60,
|
|
843
|
+
fixedSlack: 150,
|
|
844
|
+
roundingSlack: 48,
|
|
845
|
+
proseMirrorArg: "includeContext",
|
|
846
|
+
},
|
|
847
|
+
// #1207 — count arithmetic: included + truncated <= available. The
|
|
848
|
+
// own-memory triple CAN fail: memoriesAvailable comes from an
|
|
849
|
+
// independent count query, so a double-counted memory breaks it.
|
|
827
850
|
countCoherence: [
|
|
828
851
|
{ included: "memoriesIncluded", truncated: "memoriesTruncated", available: "memoriesAvailable" },
|
|
829
|
-
|
|
852
|
+
// flair#1290 — the teammate triple ({ teammateFindingsIncluded,
|
|
853
|
+
// teammateFindingsTruncated, teammateFindingsMatched }) is
|
|
854
|
+
// deliberately NOT here. teammateFindingsMatched is DEFINED as
|
|
855
|
+
// included + truncated (informational-derived — see the note at its
|
|
856
|
+
// definition in resources/MemoryBootstrap.ts), so the entry asserted
|
|
857
|
+
// X <= X: permanently green by construction. An invariant that
|
|
858
|
+
// cannot fail is decorative, and a decorative entry in this array
|
|
859
|
+
// misrepresents the coverage the suite provides. K&S ruling on
|
|
860
|
+
// #1290: drop it rather than adding an independently-measured pool
|
|
861
|
+
// tally (a new counter that would itself need an invariant); the
|
|
862
|
+
// trust path's real protections are budgetCap plus the own-memory
|
|
863
|
+
// triple above, now exercised under includeTrust:true.
|
|
864
|
+
],
|
|
865
|
+
// flair#1290 — populated-or-hint (#1182's 0.44.11 rule, previously
|
|
866
|
+
// asserted nowhere): every structured container that ships empty
|
|
867
|
+
// carries its "why" hint, and no hint ships beside a populated
|
|
868
|
+
// container. Conditions per the hintWhenEmpty type doc — predictedHint
|
|
869
|
+
// additionally requires subjects to have been requested;
|
|
870
|
+
// currentTaskHint keys off the request, not a container.
|
|
871
|
+
hintWhenEmpty: [
|
|
872
|
+
{ hint: "eventsHint", container: "events" },
|
|
873
|
+
{ hint: "teammateFindingsHint", container: "teammateFindings" },
|
|
874
|
+
{ hint: "predictedHint", container: "predicted", requiresNonEmptyArrayArg: "subjects" },
|
|
875
|
+
{ hint: "currentTaskHint", presentWhenStringArgBlank: "currentTask" },
|
|
830
876
|
],
|
|
877
|
+
// flair#1290 — nothing the wrapper's own classifier calls a zero-row
|
|
878
|
+
// no-op ships as an event (#1200 render filter as a semantic class,
|
|
879
|
+
// not hardcoded fixture strings).
|
|
880
|
+
noOpEventsSuppressed: { container: "events" },
|
|
831
881
|
// #1199 — prose is a pointer at the default, not a second copy.
|
|
832
882
|
proseContextIsPointerAtDefault: { field: "context" },
|
|
833
883
|
// shape of the structured containers a connector reads; #1188 leak bites on memories.
|
|
@@ -39,8 +39,114 @@ export const DEFAULT_MAX_TOKENS = 2000;
|
|
|
39
39
|
* over creative extrapolation.
|
|
40
40
|
*/
|
|
41
41
|
export const GENERATE_TEMPERATURE = 0.2;
|
|
42
|
+
// ─── Continuity-journal distillation (flair#1257 slice 3) ────────────────────
|
|
43
|
+
// The session-continuity journal (slice 2, #1283) writes ephemeral+private
|
|
44
|
+
// rows tagged `adk:continuity:<sessionId>`. REM distills those journals with
|
|
45
|
+
// the SAME scope:"tagged" machinery ADK per-user tags use (#1205b) — slice 3
|
|
46
|
+
// is wiring and guards, not a new engine. The constants/predicates here are
|
|
47
|
+
// the continuity-specific guards:
|
|
48
|
+
//
|
|
49
|
+
// - stale-intent (two layers, Kern-ruled): the distiller prompt carries the
|
|
50
|
+
// rule (primary), AND a text-shape post-filter drops in-flight-intent-
|
|
51
|
+
// shaped candidates when the source session is stale (testable
|
|
52
|
+
// defense-in-depth — see filterStaleSessionIntentCandidates).
|
|
53
|
+
// - visibility (Sherlock-ruled, default-private-unless): promotion out of
|
|
54
|
+
// ephemeral+private is a visibility ESCALATION from the most sensitive
|
|
55
|
+
// tier. The distiller may rule "shared" only AFFIRMATIVELY, with a
|
|
56
|
+
// team-relevance justification recorded on the candidate — never by
|
|
57
|
+
// default, never silently (see resolveCandidateVisibilityRuling).
|
|
58
|
+
/** Tag prefix for continuity-journal rows (`adk:continuity:<sessionId>`).
|
|
59
|
+
* Canonical string duplicated in packages/flair-mcp/src/continuity.ts
|
|
60
|
+
* (CONTINUITY_TAG_PREFIX — the writer) and src/rem/runner.ts — the three
|
|
61
|
+
* live on opposite sides of npm-packaging boundaries (resources/ ships as
|
|
62
|
+
* the Harper component; src/ and packages/ ship separately; imports across
|
|
63
|
+
* them don't survive packaging — see src/cli.ts's header). Kept in sync by
|
|
64
|
+
* the shared canonical string. */
|
|
65
|
+
export const CONTINUITY_SCOPE_TAG_PREFIX = "adk:continuity:";
|
|
66
|
+
/** True iff `tag` is a continuity-journal scope tag (has a non-empty
|
|
67
|
+
* sessionId component — the bare prefix is not a session). */
|
|
68
|
+
export function isContinuityScopeTag(tag) {
|
|
69
|
+
return typeof tag === "string" && tag.length > CONTINUITY_SCOPE_TAG_PREFIX.length && tag.startsWith(CONTINUITY_SCOPE_TAG_PREFIX);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Staleness horizon for the stale-intent guard (spec item 3, default 72h,
|
|
73
|
+
* FLAIR_REM_STALE_INTENT_HOURS). A journal entry like "about to merge X" is
|
|
74
|
+
* useful context shortly after the session died (the intent may still be
|
|
75
|
+
* live); past this horizon the intent has resolved or died, and promoting it
|
|
76
|
+
* manufactures a false present. Distinct from the SETTLE window (2h,
|
|
77
|
+
* src/rem/runner.ts) — settle decides when a session may be distilled at
|
|
78
|
+
* all; this horizon decides whether in-flight-intent content from it may
|
|
79
|
+
* still promote.
|
|
80
|
+
*/
|
|
81
|
+
export const DEFAULT_STALE_INTENT_HORIZON_MS = 72 * 3600_000;
|
|
82
|
+
/**
|
|
83
|
+
* Text shapes that mark a candidate as IN-FLIGHT INTENT — an action described
|
|
84
|
+
* as pending/current rather than decided/done. Deliberately the obvious
|
|
85
|
+
* shapes only (Kern's ruling: the prompt rule is the primary layer; this
|
|
86
|
+
* post-filter is testable defense-in-depth and need not be exhaustive).
|
|
87
|
+
* Case-insensitive; word-bounded so e.g. "roundabout to" doesn't match.
|
|
88
|
+
*/
|
|
89
|
+
export const IN_FLIGHT_INTENT_PATTERNS = [
|
|
90
|
+
/\babout to\b/i,
|
|
91
|
+
/\bwaiting (?:on|for)\b/i,
|
|
92
|
+
/\bgoing to\b/i,
|
|
93
|
+
/\bplanning to\b/i,
|
|
94
|
+
];
|
|
95
|
+
/** True iff `text` matches an in-flight-intent shape. */
|
|
96
|
+
export function isInFlightIntentShaped(text) {
|
|
97
|
+
return IN_FLIGHT_INTENT_PATTERNS.some((p) => p.test(text));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The stale-intent POST-FILTER (spec item 3, the testable layer). When the
|
|
101
|
+
* source session is STALE — its newest entry older than `horizonMs` — drop
|
|
102
|
+
* every candidate whose claim is in-flight-intent-shaped. Runs AFTER
|
|
103
|
+
* parseAndValidateCandidates (a drop here is a policy skip, never a batch
|
|
104
|
+
* failure) and BEFORE dedup/staging.
|
|
105
|
+
*
|
|
106
|
+
* Fresh sessions pass everything through (an "about to merge X" from two
|
|
107
|
+
* hours ago is genuinely useful resume context). Stale sessions still
|
|
108
|
+
* promote DECISION-class content — the filter drops only the in-flight
|
|
109
|
+
* shapes, which is the positive control the acceptance set demands.
|
|
110
|
+
*
|
|
111
|
+
* An UNDATEABLE session (no newest-entry timestamp) is treated as STALE:
|
|
112
|
+
* this guard exists to stop manufactured false-presents, and "can't tell how
|
|
113
|
+
* old" must fail toward filtering, not toward promoting (fail-closed).
|
|
114
|
+
*/
|
|
115
|
+
export function filterStaleSessionIntentCandidates(candidates, params) {
|
|
116
|
+
const horizonMs = params.horizonMs ?? DEFAULT_STALE_INTENT_HORIZON_MS;
|
|
117
|
+
const newestMs = params.sessionNewestCreatedAt ? new Date(params.sessionNewestCreatedAt).getTime() : NaN;
|
|
118
|
+
const sessionStale = !Number.isFinite(newestMs) || params.now.getTime() - newestMs > horizonMs;
|
|
119
|
+
if (!sessionStale)
|
|
120
|
+
return { kept: candidates, droppedStaleIntent: [] };
|
|
121
|
+
const kept = [];
|
|
122
|
+
const droppedStaleIntent = [];
|
|
123
|
+
for (const c of candidates) {
|
|
124
|
+
(isInFlightIntentShaped(c.claim) ? droppedStaleIntent : kept).push(c);
|
|
125
|
+
}
|
|
126
|
+
return { kept, droppedStaleIntent };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolve a distilled candidate's visibility ruling (Sherlock's
|
|
130
|
+
* default-private-unless, flair#1257 slice 3). Returns a ruling ONLY when the
|
|
131
|
+
* distiller AFFIRMATIVELY ruled "shared" AND recorded a non-empty
|
|
132
|
+
* team-relevance justification — anything less (absent, "private", "shared"
|
|
133
|
+
* with no justification, whitespace justification) returns null, which
|
|
134
|
+
* downstream reads as the private default. The uncertainty fallback is
|
|
135
|
+
* private, fail-closed; a shared promoted row must always trace to a
|
|
136
|
+
* recorded justification on its candidate, never to a default.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveCandidateVisibilityRuling(candidate) {
|
|
139
|
+
if (candidate.visibility !== "shared")
|
|
140
|
+
return null;
|
|
141
|
+
const rationale = typeof candidate.teamRelevance === "string" ? candidate.teamRelevance.trim() : "";
|
|
142
|
+
if (rationale.length === 0)
|
|
143
|
+
return null;
|
|
144
|
+
return { ruling: "shared", rationale };
|
|
145
|
+
}
|
|
42
146
|
// ─── Candidate shape (spec §3A) ───────────────────────────────────────────────
|
|
43
147
|
// { candidates: [ { claim: string, sourceMemoryIds: string[], tags?: string[] } ] }
|
|
148
|
+
// Continuity runs (flair#1257 slice 3) may additionally carry per-candidate
|
|
149
|
+
// `visibility` + `teamRelevance` — see resolveCandidateVisibilityRuling.
|
|
44
150
|
//
|
|
45
151
|
// Passed as `responseFormat: { schema: CANDIDATES_SCHEMA }` to models.generate()
|
|
46
152
|
// so backends that honor structured output (Ollama, OpenAI — verified against
|
|
@@ -64,6 +170,11 @@ export const CANDIDATES_SCHEMA = {
|
|
|
64
170
|
claim: { type: "string" },
|
|
65
171
|
sourceMemoryIds: { type: "array", items: { type: "string" } },
|
|
66
172
|
tags: { type: "array", items: { type: "string" } },
|
|
173
|
+
// flair#1257 slice 3 (continuity runs only — see the module note
|
|
174
|
+
// above CONTINUITY_SCOPE_TAG_PREFIX): an AFFIRMATIVE visibility
|
|
175
|
+
// ruling. Optional for every run; validated when present.
|
|
176
|
+
visibility: { type: "string", enum: ["private", "shared"] },
|
|
177
|
+
teamRelevance: { type: "string" },
|
|
67
178
|
},
|
|
68
179
|
required: ["claim", "sourceMemoryIds"],
|
|
69
180
|
},
|
|
@@ -77,7 +188,39 @@ export const FOCUS_PROMPTS = {
|
|
|
77
188
|
patterns: "Identify recurring patterns across these memories. What themes, approaches, or outcomes appear multiple times? Extract each pattern as a persistent memory.",
|
|
78
189
|
decisions: "Catalog the key decisions made and their outcomes. For each: what was decided, why, and what resulted. Promote important decisions to persistent.",
|
|
79
190
|
errors: "Extract errors, bugs, and failures. For each: what failed, root cause, and fix applied. These are high-value persistent memories.",
|
|
191
|
+
// flair#1257 slice 3 — continuity-journal distillation. The source rows are
|
|
192
|
+
// an agent's auto-captured working-state journal (ephemeral, private,
|
|
193
|
+
// intent-class), not curated knowledge — distill what deserves to OUTLIVE
|
|
194
|
+
// the session. The stale-intent prompt rule here is the PRIMARY layer of
|
|
195
|
+
// the two-layer guard (Kern's ruling); filterStaleSessionIntentCandidates
|
|
196
|
+
// is the testable second layer.
|
|
197
|
+
continuity: "These memories are an agent's short-term session journal: auto-captured working-state deltas (what it was doing, deciding, and why). Distill the DURABLE takeaways — decisions made and their reasons, outcomes, lessons — into atomic persistent memories. Do NOT promote in-flight intent (e.g. \"about to merge X\", \"waiting on Y\", \"going to\", \"planning to\") from a session that is no longer live: the action has since resolved or died, and restating it as current manufactures a false present. Do not promote world-recoverable facts (PR status, CI state) — they are re-observable and go stale.",
|
|
80
198
|
};
|
|
199
|
+
/**
|
|
200
|
+
* Extra execute-mode instruction block for continuity runs (flair#1257 slice
|
|
201
|
+
* 3). Two parts:
|
|
202
|
+
* - the VISIBILITY ruling contract (Sherlock, default-private-unless): the
|
|
203
|
+
* source journal is the most sensitive tier (ephemeral+private), so the
|
|
204
|
+
* promoted claim defaults private; the distiller may rule "shared" only
|
|
205
|
+
* affirmatively, and then MUST justify team-relevance (the justification
|
|
206
|
+
* is recorded on the candidate — resolveCandidateVisibilityRuling drops
|
|
207
|
+
* any shared ruling that arrives without one).
|
|
208
|
+
* - when the session is STALE, an explicit restatement of the stale-intent
|
|
209
|
+
* rule with the session's age class named (the prompt-layer half of the
|
|
210
|
+
* two-layer guard; the post-filter backstops it either way).
|
|
211
|
+
*/
|
|
212
|
+
export function buildContinuityExecuteAddendum(params) {
|
|
213
|
+
const lines = [
|
|
214
|
+
`Continuity visibility rules:`,
|
|
215
|
+
`- Every candidate's visibility defaults to "private". Omit the visibility field unless you are AFFIRMATIVELY ruling a candidate team-relevant.`,
|
|
216
|
+
`- To rule a candidate shared, set visibility: "shared" AND teamRelevance: one sentence stating why teammates need this. A shared ruling without a teamRelevance justification is discarded and the candidate stays private.`,
|
|
217
|
+
`- If uncertain, stay private.`,
|
|
218
|
+
];
|
|
219
|
+
if (params.sessionStale) {
|
|
220
|
+
lines.push(`This session is STALE (its newest journal entry is beyond the staleness horizon): do NOT emit candidates describing in-flight actions ("about to", "waiting on", "going to", "planning to") — those intents have resolved or died. Distill only decisions, outcomes, and lessons.`);
|
|
221
|
+
}
|
|
222
|
+
return lines.join("\n");
|
|
223
|
+
}
|
|
81
224
|
/**
|
|
82
225
|
* Shared "Source Memories" block for both prompt mode and execute mode
|
|
83
226
|
* (K&S prompt-injection hardening, spec §3A item 7). Each memory is wrapped
|
|
@@ -127,9 +270,12 @@ For each insight:
|
|
|
127
270
|
* handing a prompt to a human/agent.
|
|
128
271
|
*/
|
|
129
272
|
export function buildExecutePrompt(params) {
|
|
130
|
-
const { agentId, focus, scope, sinceISO, memories } = params;
|
|
273
|
+
const { agentId, focus, scope, sinceISO, memories, continuity } = params;
|
|
131
274
|
const focusText = FOCUS_PROMPTS[focus] ?? FOCUS_PROMPTS.lessons_learned;
|
|
132
275
|
const validIds = memories.map((m) => `"${m.id}"`).join(", ");
|
|
276
|
+
const candidateShape = continuity
|
|
277
|
+
? `{"candidates": [{"claim": string, "sourceMemoryIds": string[], "tags"?: string[], "visibility"?: "shared", "teamRelevance"?: string}]}`
|
|
278
|
+
: `{"candidates": [{"claim": string, "sourceMemoryIds": string[], "tags"?: string[]}]}`;
|
|
133
279
|
return `# Memory Reflection — ${agentId}
|
|
134
280
|
Focus: ${focus}
|
|
135
281
|
Scope: ${scope} (since ${sinceISO})
|
|
@@ -137,13 +283,13 @@ Memories: ${memories.length}
|
|
|
137
283
|
|
|
138
284
|
## Task
|
|
139
285
|
${focusText}
|
|
140
|
-
|
|
286
|
+
${continuity ? `\n${buildContinuityExecuteAddendum(continuity)}\n` : ""}
|
|
141
287
|
## Source Memories
|
|
142
288
|
${buildSourceMemoriesBlock(memories)}
|
|
143
289
|
|
|
144
290
|
## Output
|
|
145
291
|
Respond with ONLY a JSON object of this shape (no prose, no markdown fences):
|
|
146
|
-
{
|
|
292
|
+
${candidateShape}
|
|
147
293
|
Rules:
|
|
148
294
|
- Every sourceMemoryIds entry must be one of: ${validIds || "(none available)"}
|
|
149
295
|
- claim must be a single atomic insight, at most ${MAX_CLAIM_LENGTH} characters
|
|
@@ -219,7 +365,27 @@ export function parseAndValidateCandidates(raw, gatheredMemoryIds) {
|
|
|
219
365
|
}
|
|
220
366
|
tags = candidate.tags;
|
|
221
367
|
}
|
|
222
|
-
|
|
368
|
+
// flair#1257 slice 3: optional visibility ruling fields (continuity runs).
|
|
369
|
+
// Validated for every run — an unknown visibility value must fail closed
|
|
370
|
+
// exactly like any other malformed field, never pass through to a place
|
|
371
|
+
// where "not private" could later read as readable (the free-form-string
|
|
372
|
+
// exact-match lesson). Whether a valid ruling has any EFFECT is decided
|
|
373
|
+
// downstream (resolveCandidateVisibilityRuling, continuity staging only).
|
|
374
|
+
let visibility;
|
|
375
|
+
if (candidate.visibility !== undefined) {
|
|
376
|
+
if (candidate.visibility !== "private" && candidate.visibility !== "shared") {
|
|
377
|
+
return { ok: false, reason: "shape_mismatch" };
|
|
378
|
+
}
|
|
379
|
+
visibility = candidate.visibility;
|
|
380
|
+
}
|
|
381
|
+
let teamRelevance;
|
|
382
|
+
if (candidate.teamRelevance !== undefined) {
|
|
383
|
+
if (typeof candidate.teamRelevance !== "string") {
|
|
384
|
+
return { ok: false, reason: "shape_mismatch" };
|
|
385
|
+
}
|
|
386
|
+
teamRelevance = candidate.teamRelevance;
|
|
387
|
+
}
|
|
388
|
+
candidates.push({ claim: candidate.claim, sourceMemoryIds, tags, visibility, teamRelevance });
|
|
223
389
|
}
|
|
224
390
|
return { ok: true, candidates };
|
|
225
391
|
}
|
|
@@ -310,6 +476,33 @@ export function dedupeCandidates(candidates, existingPendingClaims) {
|
|
|
310
476
|
// rules, not scope selection.
|
|
311
477
|
export function memoryMatchesReflectScope(record, params) {
|
|
312
478
|
const { scope, tag, sinceDate } = params;
|
|
479
|
+
// ── flair#1257 slice 3: continuity-journal containment (both directions) ───
|
|
480
|
+
// The JOURNAL is the ephemeral rows carrying a continuity session tag.
|
|
481
|
+
// Two rules keep it contained:
|
|
482
|
+
//
|
|
483
|
+
// 1. A continuity-tag tagged run gathers THE JOURNAL ONLY — ephemeral
|
|
484
|
+
// rows carrying that session's tag. Promoted rows PRESERVE the
|
|
485
|
+
// session scopeTag (spec item 2), so without the durability bound a
|
|
486
|
+
// re-distill of the same tag would gather its own previous OUTPUTS as
|
|
487
|
+
// input — a distill-of-distilled feedback loop.
|
|
488
|
+
// 2. A journal row is distillable ONLY through its own session's
|
|
489
|
+
// continuity run — the path that carries every slice-3 guard (settle
|
|
490
|
+
// window, continuity focus prompt, stale-intent post-filter, the
|
|
491
|
+
// visibility ruling contract). Without this, the agentId-wide
|
|
492
|
+
// scope:"recent"/"all" gather would sweep a LIVE session's journal
|
|
493
|
+
// into a generic distill, bypassing all of those guards at once (the
|
|
494
|
+
// settle window would be a check that cannot fire).
|
|
495
|
+
//
|
|
496
|
+
// Non-journal rows that carry a continuity tag (the promoted persistent
|
|
497
|
+
// rows) follow the NORMAL scope rules below — they stay re-reflectable
|
|
498
|
+
// like any other durable memory.
|
|
499
|
+
if (scope === "tagged" && isContinuityScopeTag(tag)) {
|
|
500
|
+
return record.durability === "ephemeral" && (record.tags ?? []).includes(tag);
|
|
501
|
+
}
|
|
502
|
+
const rowIsJournal = record.durability === "ephemeral" && (record.tags ?? []).some(isContinuityScopeTag);
|
|
503
|
+
if (rowIsJournal) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
313
506
|
if (scope === "tagged") {
|
|
314
507
|
// No tag ⇒ admit nothing. A tagged reflection with no tag must gather an
|
|
315
508
|
// EMPTY set (fail-closed), never fall through to admitting everything —
|
|
@@ -355,5 +548,9 @@ export function buildStagedCandidateRow(params) {
|
|
|
355
548
|
if (params.scope === "tagged" && typeof params.tag === "string" && params.tag.length > 0) {
|
|
356
549
|
row.scopeTag = params.tag;
|
|
357
550
|
}
|
|
551
|
+
if (params.visibilityRuling) {
|
|
552
|
+
row.visibilityRuling = params.visibilityRuling.ruling;
|
|
553
|
+
row.visibilityRationale = params.visibilityRuling.rationale;
|
|
554
|
+
}
|
|
358
555
|
return row;
|
|
359
556
|
}
|