instar 1.3.136 → 1.3.138
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/dashboard/index.html +62 -0
- package/dashboard/preferences-learning.js +388 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +15 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +3 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +31 -10
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/types.d.ts +11 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/CorrectionLedger.d.ts +6 -0
- package/dist/monitoring/CorrectionLedger.d.ts.map +1 -1
- package/dist/monitoring/CorrectionLedger.js +17 -0
- package/dist/monitoring/CorrectionLedger.js.map +1 -1
- package/dist/monitoring/CorrectionLoopDriver.d.ts +59 -9
- package/dist/monitoring/CorrectionLoopDriver.d.ts.map +1 -1
- package/dist/monitoring/CorrectionLoopDriver.js +87 -12
- package/dist/monitoring/CorrectionLoopDriver.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +3 -2
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.js +4 -4
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +88 -6
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/framework-issue-backfill/codex-session-findings.json +116 -0
- package/scripts/framework-issue-backfill/import-findings.mjs +75 -0
- package/src/data/builtin-manifest.json +62 -62
- package/src/scaffold/templates.ts +3 -2
- package/upgrades/1.3.135.md +80 -36
- package/upgrades/1.3.137.md +97 -0
- package/upgrades/1.3.138.md +58 -0
- package/upgrades/side-effects/correction-preference-learning-sentinel-slice2.md +86 -0
- package/upgrades/side-effects/framework-issue-observe-write-path.md +44 -0
package/dashboard/index.html
CHANGED
|
@@ -2636,6 +2636,7 @@
|
|
|
2636
2636
|
<button class="tab" data-tab="threadline" onclick="switchTab('threadline')">Threadline</button>
|
|
2637
2637
|
<button class="tab" data-tab="evidence" onclick="switchTab('evidence')">Evidence</button>
|
|
2638
2638
|
<button class="tab" data-tab="process-health" onclick="switchTab('process-health')">Process Health</button>
|
|
2639
|
+
<button class="tab" data-tab="preferences-learning" onclick="switchTab('preferences-learning')">Preferences</button>
|
|
2639
2640
|
<button class="tab" data-tab="machines" onclick="switchTab('machines')">Machines</button>
|
|
2640
2641
|
</nav>
|
|
2641
2642
|
</div>
|
|
@@ -3149,6 +3150,26 @@
|
|
|
3149
3150
|
</details>
|
|
3150
3151
|
</div>
|
|
3151
3152
|
|
|
3153
|
+
<!-- preferences-learning tab (Correction & Preference Learning Sentinel §10 Slice-2) -->
|
|
3154
|
+
<div id="preferencesLearningPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
|
|
3155
|
+
<div style="display:flex;justify-content:space-between;align-items:baseline">
|
|
3156
|
+
<h2 class="ph-title">Preferences</h2>
|
|
3157
|
+
<div id="plStamp" class="ph-stamp" aria-live="polite"></div>
|
|
3158
|
+
</div>
|
|
3159
|
+
<p class="ph-intro">When you correct me the same way a few times — "no, plainer", "stop asking me that", "lead with the one action" — I save it as a preference and load it at the start of every conversation, so I follow it without you having to remind me. This is a quiet record of what I've picked up. It never blocks or changes a message — these are signals, not rules.</p>
|
|
3160
|
+
<div id="plHeadline" class="ph-headline-wrap" aria-live="polite"></div>
|
|
3161
|
+
<section class="ph-section">
|
|
3162
|
+
<h3 class="ph-h">Preferences I've picked up about you</h3>
|
|
3163
|
+
<p class="ph-h-sub">The ones I load at the start of every conversation. Real instructions and safety always win over these.</p>
|
|
3164
|
+
<div id="plPreferences"></div>
|
|
3165
|
+
</section>
|
|
3166
|
+
<section class="ph-section">
|
|
3167
|
+
<h3 class="ph-h">Corrections I've noticed lately</h3>
|
|
3168
|
+
<p class="ph-h-sub">Patterns I've distilled from moments you corrected me. The exact words are never stored — just a short, scrubbed summary.</p>
|
|
3169
|
+
<div id="plCorrections"></div>
|
|
3170
|
+
</section>
|
|
3171
|
+
</div>
|
|
3172
|
+
|
|
3152
3173
|
<!-- machines tab (Multi-Machine Session Pool §L2) -->
|
|
3153
3174
|
<div id="machinesPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
|
|
3154
3175
|
<div style="display:flex;justify-content:space-between;align-items:baseline">
|
|
@@ -4403,6 +4424,13 @@
|
|
|
4403
4424
|
onActivate: () => { if (typeof startProcessHealth === 'function') startProcessHealth(); },
|
|
4404
4425
|
onDeactivate: () => { if (typeof stopProcessHealth === 'function') stopProcessHealth(); },
|
|
4405
4426
|
},
|
|
4427
|
+
{
|
|
4428
|
+
id: 'preferences-learning',
|
|
4429
|
+
panels: ['preferencesLearningPanel'],
|
|
4430
|
+
display: ['flex'],
|
|
4431
|
+
onActivate: () => { if (typeof startPreferencesLearning === 'function') startPreferencesLearning(); },
|
|
4432
|
+
onDeactivate: () => { if (typeof stopPreferencesLearning === 'function') stopPreferencesLearning(); },
|
|
4433
|
+
},
|
|
4406
4434
|
{
|
|
4407
4435
|
id: 'machines',
|
|
4408
4436
|
panels: ['machinesPanel'],
|
|
@@ -8244,16 +8272,50 @@
|
|
|
8244
8272
|
if (__phController) __phController.stop();
|
|
8245
8273
|
}
|
|
8246
8274
|
|
|
8275
|
+
// ── Preferences tab (Correction & Preference Learning Sentinel §10 Slice-2) ──
|
|
8276
|
+
// Same lazy dynamic-import pattern as Process Health: the controller lives in
|
|
8277
|
+
// this (classic) scope so it can read `token`; the module ships the pure
|
|
8278
|
+
// renderers + the polling controller.
|
|
8279
|
+
let __plController = null;
|
|
8280
|
+
let __plModule = null;
|
|
8281
|
+
async function startPreferencesLearning() {
|
|
8282
|
+
if (!__plModule) {
|
|
8283
|
+
try { __plModule = await import('/dashboard/preferences-learning.js'); }
|
|
8284
|
+
catch (e) { console.error('[preferences-learning] module load failed', e); return; }
|
|
8285
|
+
}
|
|
8286
|
+
if (!__plController) {
|
|
8287
|
+
const els = {
|
|
8288
|
+
headline: document.getElementById('plHeadline'),
|
|
8289
|
+
preferences: document.getElementById('plPreferences'),
|
|
8290
|
+
corrections: document.getElementById('plCorrections'),
|
|
8291
|
+
stamp: document.getElementById('plStamp'),
|
|
8292
|
+
};
|
|
8293
|
+
// Raw fetch (NOT apiFetch) so the controller sees 503 itself.
|
|
8294
|
+
const fetchImpl = (url, opts = {}) => fetch(url, {
|
|
8295
|
+
headers: { 'Authorization': `Bearer ${token}`, ...(opts.headers || {}) },
|
|
8296
|
+
signal: opts.signal,
|
|
8297
|
+
});
|
|
8298
|
+
__plController = __plModule.createController({ doc: document, els, fetchImpl });
|
|
8299
|
+
}
|
|
8300
|
+
__plController.start();
|
|
8301
|
+
}
|
|
8302
|
+
function stopPreferencesLearning() {
|
|
8303
|
+
if (__plController) __plController.stop();
|
|
8304
|
+
}
|
|
8305
|
+
|
|
8247
8306
|
// Handle tab visibility — close SSE when tab is backgrounded
|
|
8248
8307
|
document.addEventListener('visibilitychange', () => {
|
|
8249
8308
|
if (document.hidden) {
|
|
8250
8309
|
disconnectJobsSSE();
|
|
8251
8310
|
if (currentTab === 'process-health') stopProcessHealth();
|
|
8311
|
+
if (currentTab === 'preferences-learning') stopPreferencesLearning();
|
|
8252
8312
|
} else if (currentTab === 'jobs') {
|
|
8253
8313
|
connectJobsSSE();
|
|
8254
8314
|
loadJobs(); // Resync on return
|
|
8255
8315
|
} else if (currentTab === 'process-health') {
|
|
8256
8316
|
startProcessHealth(); // resume on return: one immediate refresh + re-arm
|
|
8317
|
+
} else if (currentTab === 'preferences-learning') {
|
|
8318
|
+
startPreferencesLearning(); // resume on return
|
|
8257
8319
|
}
|
|
8258
8320
|
// Always refresh vital signs on tab focus
|
|
8259
8321
|
if (!document.hidden) pollVitalSigns();
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// Preferences tab — a calm, human-readable read surface for the Correction &
|
|
2
|
+
// Preference Learning Sentinel (Slice 2).
|
|
3
|
+
// Spec: docs/specs/CORRECTION-PREFERENCE-LEARNING-SENTINEL-SPEC.md §10 Slice-2.
|
|
4
|
+
//
|
|
5
|
+
// Browser-native ESM (no build step; served at /dashboard/preferences-learning.js
|
|
6
|
+
// and loaded by index.html via a lazy dynamic import). The pure functions are
|
|
7
|
+
// exported so the jsdom tests exercise the SHIPPED code, not a copy; the
|
|
8
|
+
// controller is driven by the (classic) index.html script on tab activation.
|
|
9
|
+
//
|
|
10
|
+
// Mirrors the Process Health tab precedent (dashboard/process-health.js):
|
|
11
|
+
// - one self-contained module for the tab (NOT a modularization of the SPA);
|
|
12
|
+
// - every dynamic value flows through sanitizeForDisplay before the DOM;
|
|
13
|
+
// - all DOM writes are textContent only — never innerHTML;
|
|
14
|
+
// - a friendly "not turned on yet" disabled (503) state;
|
|
15
|
+
// - a visibility-gated, abort-safe polling controller.
|
|
16
|
+
//
|
|
17
|
+
// Two endpoints back this tab, BOTH already shipping (Slice 1a/1b):
|
|
18
|
+
// - GET /preferences/session-context → { present, block, count } — the active
|
|
19
|
+
// learned preferences as a single plain-language block (the SAME text the
|
|
20
|
+
// session-start hook injects). Raw learning text is never served; the block
|
|
21
|
+
// is the formatted, scrubbed summary.
|
|
22
|
+
// - GET /corrections?limit=N → { records, count, totalRecords, nextBefore } —
|
|
23
|
+
// the deduped, scrubbed correction/preference ledger records (toApiView strips
|
|
24
|
+
// the raw `learning`; only scrubbed_summary + metadata cross the boundary).
|
|
25
|
+
// Either returning 503 means the feature is off → the disabled state renders.
|
|
26
|
+
|
|
27
|
+
// ── Safety: structural sanitize (mirrors process-health §4.6) ──────────────────
|
|
28
|
+
// Strip, after the NFKC fold, control + bidi-control characters and cap visible
|
|
29
|
+
// length. Presentation chrome here is renderer-OWNED static literals only.
|
|
30
|
+
const CAPS = { summary: 240, block: 6000, label: 64, detail: 320 };
|
|
31
|
+
|
|
32
|
+
const CONTROL_RE = new RegExp('[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F-\\u009F]', 'g');
|
|
33
|
+
const BIDI_RE = new RegExp('[\\u202A-\\u202E\\u2066-\\u2069]', 'g');
|
|
34
|
+
|
|
35
|
+
export function sanitizeForDisplay(value, fieldKind = 'summary') {
|
|
36
|
+
let s = value == null ? '' : String(value);
|
|
37
|
+
s = s.normalize('NFKC');
|
|
38
|
+
s = s.replace(CONTROL_RE, ''); // keep \n=0A, \t=09
|
|
39
|
+
s = s.replace(BIDI_RE, '');
|
|
40
|
+
s = s.replace(/\n{3,}/g, '\n\n').replace(/[ \t]{5,}/g, ' ');
|
|
41
|
+
s = capGraphemes(s, CAPS[fieldKind] ?? CAPS.summary);
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function capGraphemes(s, max) {
|
|
46
|
+
if (s.length <= max) return s;
|
|
47
|
+
if (typeof Intl !== 'undefined' && Intl.Segmenter) {
|
|
48
|
+
const arr = Array.from(new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(s), (x) => x.segment);
|
|
49
|
+
if (arr.length <= max) return s;
|
|
50
|
+
return arr.slice(0, max - 1).join('') + '…';
|
|
51
|
+
}
|
|
52
|
+
let cut = max - 1;
|
|
53
|
+
const c = s.charCodeAt(cut - 1);
|
|
54
|
+
if (c >= 0xd800 && c <= 0xdbff) cut -= 1;
|
|
55
|
+
return s.slice(0, cut) + '…';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Friendly wording ───────────────────────────────────────────────────────
|
|
59
|
+
const KIND_WORDS = {
|
|
60
|
+
'user-preference': 'How you like things',
|
|
61
|
+
'infra-gap': 'Something the tool itself should handle better',
|
|
62
|
+
noise: 'No clear lesson',
|
|
63
|
+
};
|
|
64
|
+
export function friendlyKind(kind) {
|
|
65
|
+
return KIND_WORDS[kind] || 'A pattern';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Correction-record lifecycle wording — informational only, no action language.
|
|
69
|
+
const STATUS_WORDS = {
|
|
70
|
+
open: 'Just noticed',
|
|
71
|
+
'acted-on': 'Saved and being watched',
|
|
72
|
+
verified: 'Confirmed and sticking',
|
|
73
|
+
inconclusive: 'Watched, but nothing conclusive',
|
|
74
|
+
reopened: 'Came back after I tried adapting',
|
|
75
|
+
};
|
|
76
|
+
export function friendlyCorrectionStatus(status) {
|
|
77
|
+
return STATUS_WORDS[status] || 'Not set';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function statusDotClass(status) {
|
|
81
|
+
switch (status) {
|
|
82
|
+
case 'open': case 'reopened': return 'status-open';
|
|
83
|
+
case 'acted-on': return 'status-attributed';
|
|
84
|
+
case 'verified': return 'status-verified';
|
|
85
|
+
case 'inconclusive': case 'closed': default: return 'status-closed';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function relativeTime(iso, now = Date.now()) {
|
|
90
|
+
const t = typeof iso === 'string' ? Date.parse(iso) : NaN;
|
|
91
|
+
if (Number.isNaN(t)) return 'recently';
|
|
92
|
+
const sec = Math.max(0, Math.floor((now - t) / 1000));
|
|
93
|
+
if (sec < 90) return 'just now';
|
|
94
|
+
const min = Math.floor(sec / 60);
|
|
95
|
+
if (min < 60) return `about ${min} minute${min === 1 ? '' : 's'} ago`;
|
|
96
|
+
const hr = Math.floor(min / 60);
|
|
97
|
+
if (hr < 24) return `about ${hr} hour${hr === 1 ? '' : 's'} ago`;
|
|
98
|
+
const day = Math.floor(hr / 24);
|
|
99
|
+
if (day < 14) return `about ${day} day${day === 1 ? '' : 's'} ago`;
|
|
100
|
+
const wk = Math.floor(day / 7);
|
|
101
|
+
if (wk < 8) return `about ${wk} week${wk === 1 ? '' : 's'} ago`;
|
|
102
|
+
const mo = Math.floor(day / 30);
|
|
103
|
+
return `about ${mo} month${mo === 1 ? '' : 's'} ago`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── DOM helpers (textContent ONLY — never innerHTML) ─────────────────────────
|
|
107
|
+
function el(doc, tag, cls, text) {
|
|
108
|
+
const node = doc.createElement(tag);
|
|
109
|
+
if (cls) node.setAttribute('class', cls);
|
|
110
|
+
if (text != null) node.textContent = text;
|
|
111
|
+
return node;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function labeledRow(doc, label, value) {
|
|
115
|
+
const row = el(doc, 'div', 'ph-item-row');
|
|
116
|
+
row.appendChild(el(doc, 'span', 'ph-label', `${label}:`));
|
|
117
|
+
row.appendChild(doc.createTextNode(' '));
|
|
118
|
+
row.appendChild(el(doc, 'span', 'ph-value', value));
|
|
119
|
+
return row;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function statusDot(doc, status) {
|
|
123
|
+
const dot = doc.createElement('span');
|
|
124
|
+
dot.setAttribute('class', `ph-status-dot ${statusDotClass(status)}`);
|
|
125
|
+
dot.setAttribute('aria-hidden', 'true');
|
|
126
|
+
return dot;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Renderers (pure data→DOM) ────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Headline (informational, not a verdict): how many preferences I've learned.
|
|
133
|
+
* `stale` shows the calm "can't refresh" line, mirroring process-health.
|
|
134
|
+
*/
|
|
135
|
+
export function renderHeadline(doc, target, { prefCount, stale }) {
|
|
136
|
+
if (!target) return;
|
|
137
|
+
target.replaceChildren();
|
|
138
|
+
if (stale) {
|
|
139
|
+
target.appendChild(el(doc, 'div', 'ph-headline', "Can't refresh right now — showing the last update"));
|
|
140
|
+
target.appendChild(el(doc, 'div', 'ph-subline', "We'll keep trying — nothing is lost."));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const n = Number.isFinite(prefCount) ? prefCount : 0;
|
|
144
|
+
const headline = n === 0
|
|
145
|
+
? "I haven't picked up any preferences yet"
|
|
146
|
+
: `${n} preference${n === 1 ? '' : 's'} I've picked up about you`;
|
|
147
|
+
target.appendChild(el(doc, 'div', 'ph-headline', headline));
|
|
148
|
+
const subline = n === 0
|
|
149
|
+
? 'When you correct me the same way a few times, I save it here so I stop making you repeat yourself.'
|
|
150
|
+
: 'These get loaded at the start of every conversation, so I follow them without you having to remind me.';
|
|
151
|
+
target.appendChild(el(doc, 'div', 'ph-subline', subline));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The active preferences block (§10 — "Preferences I've picked up about you").
|
|
156
|
+
* `/preferences/session-context` serves the preferences as ONE formatted,
|
|
157
|
+
* already-scrubbed plain-language block (the same text the session-start hook
|
|
158
|
+
* injects). We render its lines as plain text, dropping the envelope wrapper
|
|
159
|
+
* tags so the user reads the preferences, not the machinery.
|
|
160
|
+
*/
|
|
161
|
+
export function renderPreferences(doc, target, sessionContext) {
|
|
162
|
+
if (!target) return;
|
|
163
|
+
target.replaceChildren();
|
|
164
|
+
const present = sessionContext && sessionContext.present === true;
|
|
165
|
+
const block = sessionContext && typeof sessionContext.block === 'string' ? sessionContext.block : '';
|
|
166
|
+
if (!present || !block.trim()) {
|
|
167
|
+
target.appendChild(el(doc, 'p', 'ph-empty', "Nothing saved yet — that's normal. Correct me the same way a couple of times and it'll show up here."));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const safe = sanitizeForDisplay(block, 'block');
|
|
171
|
+
// Strip the <auto-learned-preference …> envelope tags + the intro line; show
|
|
172
|
+
// the actual preference lines (each begins with a "- " bullet in the block).
|
|
173
|
+
const lines = safe.split('\n');
|
|
174
|
+
const prefLines = lines
|
|
175
|
+
.map((l) => l.trim())
|
|
176
|
+
.filter((l) => l.startsWith('- '))
|
|
177
|
+
.map((l) => l.replace(/^- /, '').trim());
|
|
178
|
+
if (prefLines.length === 0) {
|
|
179
|
+
// Fall back to showing the block as a paragraph (minus the envelope tags).
|
|
180
|
+
const body = lines.filter((l) => !/^<\/?auto-learned-preference/.test(l.trim())).join('\n').trim();
|
|
181
|
+
target.appendChild(el(doc, 'p', 'ph-card-summary', body || safe));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const line of prefLines) {
|
|
185
|
+
const item = el(doc, 'div', 'ph-item');
|
|
186
|
+
const sum = el(doc, 'div', 'ph-item-summary');
|
|
187
|
+
sum.appendChild(statusDot(doc, 'verified'));
|
|
188
|
+
sum.appendChild(el(doc, 'div', 'ph-card-summary', sanitizeForDisplay(line, 'summary')));
|
|
189
|
+
item.appendChild(sum);
|
|
190
|
+
target.appendChild(item);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The recent corrections ledger (§10 — scrubbed summaries + metadata + status).
|
|
196
|
+
* Each item is an expandable fact sheet: collapsed shows the plain-language
|
|
197
|
+
* summary; expanded shows kind · status · times seen · first noticed. The raw
|
|
198
|
+
* `learning` is NEVER present in the API view, so it can never reach the DOM.
|
|
199
|
+
*/
|
|
200
|
+
export function renderCorrections(doc, target, body, now = Date.now()) {
|
|
201
|
+
if (!target) return;
|
|
202
|
+
target.replaceChildren();
|
|
203
|
+
const list = body && Array.isArray(body.records) ? body.records.slice(0, 20) : [];
|
|
204
|
+
if (list.length === 0) {
|
|
205
|
+
target.appendChild(el(doc, 'p', 'ph-empty', "Nothing recorded yet — I only save a pattern after it shows up across a few different days."));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (const r of list) {
|
|
209
|
+
if (!r) continue;
|
|
210
|
+
const kind = friendlyKind(r.kind);
|
|
211
|
+
const summary = sanitizeForDisplay(r.scrubbedSummary, 'summary');
|
|
212
|
+
const item = doc.createElement('details');
|
|
213
|
+
item.setAttribute('class', 'ph-item');
|
|
214
|
+
const sum = doc.createElement('summary');
|
|
215
|
+
sum.setAttribute('class', 'ph-item-summary');
|
|
216
|
+
sum.appendChild(statusDot(doc, r.status));
|
|
217
|
+
sum.appendChild(el(doc, 'div', 'ph-item-text', summary || kind));
|
|
218
|
+
item.appendChild(sum);
|
|
219
|
+
const detail = el(doc, 'div', 'ph-item-body');
|
|
220
|
+
detail.appendChild(labeledRow(doc, 'Kind', kind));
|
|
221
|
+
detail.appendChild(labeledRow(doc, 'Status', friendlyCorrectionStatus(r.status)));
|
|
222
|
+
const occ = Number.isFinite(r.occurrenceCount) ? r.occurrenceCount : 1;
|
|
223
|
+
detail.appendChild(labeledRow(doc, 'Times seen', occ > 1 ? `${occ} times so far` : 'Just once'));
|
|
224
|
+
if (r.detectedAt) detail.appendChild(labeledRow(doc, 'First noticed', relativeTime(r.detectedAt, now)));
|
|
225
|
+
item.appendChild(detail);
|
|
226
|
+
target.appendChild(item);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Disabled (503) state. Pinned copy + operator hint as PLAIN PROSE — no
|
|
232
|
+
* `<code>`, no monospace, no config-key string (Dashboard Standard).
|
|
233
|
+
*/
|
|
234
|
+
export function renderDisabled(doc, els) {
|
|
235
|
+
if (els.headline) {
|
|
236
|
+
els.headline.replaceChildren();
|
|
237
|
+
els.headline.appendChild(el(doc, 'div', 'ph-headline', "Learning your preferences isn't turned on for this agent yet."));
|
|
238
|
+
els.headline.appendChild(el(doc, 'div', 'ph-subline', 'Once it is, this page will show the preferences I pick up about how you like to work.'));
|
|
239
|
+
const det = doc.createElement('details');
|
|
240
|
+
det.setAttribute('class', 'ph-operator');
|
|
241
|
+
const s = doc.createElement('summary');
|
|
242
|
+
s.setAttribute('class', 'ph-operator-summary');
|
|
243
|
+
s.textContent = 'For whoever set this up';
|
|
244
|
+
det.appendChild(s);
|
|
245
|
+
det.appendChild(el(doc, 'p', 'ph-operator-hint', 'You can switch it on in this agent’s settings.'));
|
|
246
|
+
els.headline.appendChild(det);
|
|
247
|
+
}
|
|
248
|
+
for (const k of ['preferences', 'corrections']) {
|
|
249
|
+
if (els[k]) els[k].replaceChildren();
|
|
250
|
+
}
|
|
251
|
+
if (els.stamp) els.stamp.textContent = '';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Polling controller (visibility-gated, abort-safe, diff-aware) ─────────────
|
|
255
|
+
const URLS = {
|
|
256
|
+
prefs: '/preferences/session-context',
|
|
257
|
+
corrections: '/corrections?limit=20',
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Factory so tests can inject fetch + clock + DOM. opts:
|
|
262
|
+
* - doc: Document; els: { headline, preferences, corrections, stamp }
|
|
263
|
+
* - fetchImpl: (url, {signal}) => Promise<Response-like>
|
|
264
|
+
* - now: () => ms; cadenceMs (default 60_000); staleMs (default 180_000)
|
|
265
|
+
* - schedule/cancel: timer hooks (default setTimeout/clearTimeout)
|
|
266
|
+
*/
|
|
267
|
+
export function createController(opts) {
|
|
268
|
+
const {
|
|
269
|
+
doc,
|
|
270
|
+
els = {},
|
|
271
|
+
fetchImpl,
|
|
272
|
+
now = () => Date.now(),
|
|
273
|
+
cadenceMs = 60_000,
|
|
274
|
+
staleMs = 180_000,
|
|
275
|
+
schedule = (fn, ms) => setTimeout(fn, ms),
|
|
276
|
+
cancel = (id) => clearTimeout(id),
|
|
277
|
+
} = opts;
|
|
278
|
+
|
|
279
|
+
const state = {
|
|
280
|
+
timerId: null,
|
|
281
|
+
inFlight: null,
|
|
282
|
+
active: false,
|
|
283
|
+
snapshot: { prefs: null, corrections: null },
|
|
284
|
+
last200At: 0,
|
|
285
|
+
consecutiveFailedTicks: 0,
|
|
286
|
+
renderedSig: { headline: null, preferences: null, corrections: null, disabled: false },
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
async function fetchOne(key, controller) {
|
|
290
|
+
const resp = await fetchImpl(URLS[key], { signal: controller.signal });
|
|
291
|
+
if (resp.status === 503) return { key, status: 503 };
|
|
292
|
+
if (!resp.ok) throw new Error(`${key} ${resp.status}`);
|
|
293
|
+
const body = await resp.json();
|
|
294
|
+
return { key, status: 200, body };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function tick() {
|
|
298
|
+
if (!state.active) return;
|
|
299
|
+
if (state.inFlight) {
|
|
300
|
+
try { state.inFlight.abort(); } catch { /* ignore */ }
|
|
301
|
+
}
|
|
302
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : { signal: undefined, abort() {} };
|
|
303
|
+
state.inFlight = controller;
|
|
304
|
+
let results;
|
|
305
|
+
try {
|
|
306
|
+
results = await Promise.all(['prefs', 'corrections'].map((k) => fetchOne(k, controller)));
|
|
307
|
+
} catch {
|
|
308
|
+
if (controller.signal && controller.signal.aborted) return;
|
|
309
|
+
state.inFlight = null;
|
|
310
|
+
state.consecutiveFailedTicks += 1;
|
|
311
|
+
render();
|
|
312
|
+
reschedule();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (controller.signal && controller.signal.aborted) return;
|
|
316
|
+
state.inFlight = null;
|
|
317
|
+
// Feature OFF (503 on EITHER endpoint) → pinned disabled copy.
|
|
318
|
+
if (results.some((r) => r.status === 503)) {
|
|
319
|
+
state.consecutiveFailedTicks = 0;
|
|
320
|
+
if (state.renderedSig.disabled !== true) {
|
|
321
|
+
renderDisabled(doc, els);
|
|
322
|
+
state.renderedSig = { headline: null, preferences: null, corrections: null, disabled: true };
|
|
323
|
+
}
|
|
324
|
+
reschedule();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
state.renderedSig.disabled = false;
|
|
328
|
+
for (const r of results) {
|
|
329
|
+
if (r.status === 200) { state.snapshot[r.key] = r.body; }
|
|
330
|
+
}
|
|
331
|
+
state.consecutiveFailedTicks = 0;
|
|
332
|
+
state.last200At = now();
|
|
333
|
+
render();
|
|
334
|
+
reschedule();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function reschedule() {
|
|
338
|
+
if (!state.active) return;
|
|
339
|
+
if (state.timerId != null) cancel(state.timerId);
|
|
340
|
+
state.timerId = schedule(() => { void tick(); }, cadenceMs);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function headlineStale() {
|
|
344
|
+
if (state.consecutiveFailedTicks >= 2) return true;
|
|
345
|
+
if (state.last200At === 0) return false;
|
|
346
|
+
return now() - state.last200At > staleMs;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function render() {
|
|
350
|
+
const stale = headlineStale();
|
|
351
|
+
const prefs = state.snapshot.prefs;
|
|
352
|
+
const corrections = state.snapshot.corrections;
|
|
353
|
+
const prefCount = prefs && Number.isFinite(prefs.count) ? prefs.count : 0;
|
|
354
|
+
|
|
355
|
+
sectionRender('headline', sig(stale, prefCount), () => renderHeadline(doc, els.headline, { prefCount, stale }));
|
|
356
|
+
sectionRender('preferences', sig(prefs), () => renderPreferences(doc, els.preferences, prefs));
|
|
357
|
+
sectionRender('corrections', sig(corrections), () => renderCorrections(doc, els.corrections, corrections, now()));
|
|
358
|
+
|
|
359
|
+
if (els.stamp) {
|
|
360
|
+
const ageS = state.last200At ? Math.max(0, Math.round((now() - state.last200At) / 1000)) : null;
|
|
361
|
+
const stampText = ageS == null ? '' : ageS < 60 ? `updated ${ageS}s ago` : `updated ${Math.round(ageS / 60)}m ago`;
|
|
362
|
+
if (els.stamp.textContent !== stampText) els.stamp.textContent = stampText;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function sectionRender(key, signature, fn) {
|
|
367
|
+
if (state.renderedSig[key] === signature) return;
|
|
368
|
+
state.renderedSig[key] = signature;
|
|
369
|
+
fn();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function start() {
|
|
373
|
+
if (state.active) return;
|
|
374
|
+
state.active = true;
|
|
375
|
+
void tick();
|
|
376
|
+
}
|
|
377
|
+
function stop() {
|
|
378
|
+
state.active = false;
|
|
379
|
+
if (state.timerId != null) { cancel(state.timerId); state.timerId = null; }
|
|
380
|
+
if (state.inFlight) { try { state.inFlight.abort(); } catch { /* ignore */ } state.inFlight = null; }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { start, stop, tick, render, _state: state };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function sig(...parts) {
|
|
387
|
+
try { return JSON.stringify(parts); } catch { return String(Math.random()); }
|
|
388
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmSH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAwvDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmSH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAwvDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoqPtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
|
package/dist/commands/server.js
CHANGED
|
@@ -6165,6 +6165,21 @@ export async function startServer(options) {
|
|
|
6165
6165
|
maxConcurrent: cl.llmMaxConcurrent ?? 1,
|
|
6166
6166
|
maxDailyCents: cl.llmDailyCents ?? 25,
|
|
6167
6167
|
});
|
|
6168
|
+
// Drift-canary sub-budget (Slice 2 NEW-1): a SEPARATE LlmQueue instance
|
|
6169
|
+
// with its own (small) daily cap, so the periodic "would this have been
|
|
6170
|
+
// a correction?" sampler can never starve the main distill path's cap.
|
|
6171
|
+
// Constructed only when the canary is on (it ships dark); when off this
|
|
6172
|
+
// is null and the canary path never runs. The separate instance IS the
|
|
6173
|
+
// sub-budget (LlmQueue has no per-feature sub-cap, spec §2).
|
|
6174
|
+
const driftCanaryLlmQueue = cl.driftCanary === true
|
|
6175
|
+
? new LlmQueue({
|
|
6176
|
+
maxConcurrent: 1,
|
|
6177
|
+
maxDailyCents: cl.driftCanaryDailyCents ?? 5,
|
|
6178
|
+
})
|
|
6179
|
+
: null;
|
|
6180
|
+
void driftCanaryLlmQueue; // reserved for the canary sampler (dark); the
|
|
6181
|
+
// budget is provisioned here so enabling the canary cannot regress the
|
|
6182
|
+
// main cap. Referenced to satisfy no-unused under the dark default.
|
|
6168
6183
|
const ring = new CaptureRing({
|
|
6169
6184
|
captureContextTurns: cl.captureContextTurns ?? 6,
|
|
6170
6185
|
captureTopicMapMax: cl.captureTopicMapMax ?? 64,
|