Files
time/ventures/context-curator/index.html
T

182 lines
78 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="An offline editor for one exploratory span of an agent transcript. Keep the context. Carry the conclusion.">
<title>Context Curator — Keep the finding.</title>
<style>
@font-face{font-family:Curator Serif;font-style:normal;font-weight:400;font-display:swap;src:url("data:font/woff2;base64,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
:root{--paper:#f3efe4;--ink:#202f2b;--muted:#54605a;--red:#a33624;--rule:#a7aa9e;--light:#fcf9f0}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--paper);color:var(--ink);font-family:'Curator Serif',Georgia,serif;font-size:18px;line-height:1.5}body:before{content:'';position:fixed;inset:0;pointer-events:none;opacity:.13;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Cpath filter='url(%23n)' opacity='.22' d='M0 0h120v120H0z'/%3E%3C/svg%3E")}a{color:inherit;text-underline-offset:4px}a:hover{color:var(--red)}main,.masthead,footer{max-width:1220px;margin:auto;padding:0 44px}.masthead{padding-top:23px}.edition,.eyebrow,label,button,.utility,summary,select,input,textarea,.measure{font-family:Arial,Helvetica,sans-serif}.edition{display:flex;justify-content:space-between;gap:15px;font-size:11px;text-transform:uppercase;letter-spacing:1.8px;border-top:1px solid var(--ink);border-bottom:1px solid var(--ink);padding:8px 0}.mast-title{display:flex;justify-content:space-between;align-items:baseline;border-bottom:5px double var(--ink);padding:15px 0 12px}.name{font-size:clamp(30px,4.4vw,58px);font-weight:bold;letter-spacing:-2px}.issue{font-style:italic;font-size:16px}.lead{display:grid;grid-template-columns:2fr 1fr;gap:40px;padding:40px 0 38px;border-bottom:1px solid var(--ink)}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:2px;font-weight:bold;color:var(--red);margin:0 0 17px}h1{font-size:clamp(50px,6.2vw,78px);line-height:.98;letter-spacing:-3px;font-weight:normal;margin:0 0 25px}h1 em{color:var(--red)}.dek{max-width:590px;font-size:22px;line-height:1.4;margin:0 0 24px}.cta{display:inline-block;padding:13px 22px;background:var(--ink);color:var(--light);font:bold 14px Arial,Helvetica,sans-serif;text-decoration:none}.cta:hover{background:var(--red);color:white}.lead aside{border-left:1px solid var(--rule);padding-left:30px}.lead aside h2{font-size:27px;line-height:1.2;margin-top:0}.lead aside p{font-size:17px}.margin-note{font-style:italic;color:var(--muted)}.section-head{display:flex;justify-content:space-between;gap:24px;align-items:baseline;padding:27px 0 16px}.section-head h2{font-weight:normal;font-size:34px;margin:0;letter-spacing:-1px}.utility{font-size:12px;color:var(--muted)}.tool{border-top:4px solid var(--ink)}.workbench{display:grid;grid-template-columns:1fr 1.3fr;gap:32px;padding-bottom:22px}.editor{border-right:1px solid var(--rule);padding-right:30px}label{display:block;font-size:12px;font-weight:bold;margin:15px 0 6px}textarea,input,select{font-size:14px;line-height:1.45;color:var(--ink);background:var(--light);border:1px solid var(--rule);padding:10px;width:100%;border-radius:0}textarea{resize:vertical}select{min-width:0}.range{display:grid;grid-template-columns:1fr 1fr;gap:12px}.actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}button{font-size:12px;font-weight:bold;border:1px solid var(--ink);color:var(--ink);background:transparent;padding:10px 13px;cursor:pointer;min-height:40px}button:hover{background:var(--ink);color:var(--light)}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--ink);color:var(--light)}:focus-visible{outline:3px solid var(--red);outline-offset:3px}pre{font:12px/1.6 'Courier New',monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:0;max-height:440px;overflow:auto;background:var(--light);padding:17px;border:1px solid var(--rule)}.measure{padding:12px 0;font-size:14px;font-weight:bold;color:var(--red);border-bottom:1px solid var(--rule)}.diff-key{font-size:14px;color:var(--muted)}#status{font:13px/1.5 Arial,Helvetica,sans-serif;min-height:40px}summary{cursor:pointer;font-size:13px;padding:12px 0}details pre{max-height:300px}input[type=file]{font-size:12px}.notes{display:grid;grid-template-columns:repeat(3,1fr);gap:30px;padding:27px 0 35px;border-top:1px solid var(--ink);border-bottom:1px solid var(--ink)}.notes articl
</style>
</head>
<body>
<header class="masthead"><div class="edition"><span>A notplanned publication · No. 003</span><span>Local tools / better handoffs</span></div><div class="mast-title"><span class="name">Context Curator</span><span class="issue">A field guide to carrying less.</span></div></header>
<main>
<section class="lead"><div><p class="eyebrow">For developers with a long conversation behind them</p><h1>Keep the finding.<br><em>Cut the detour.</em></h1><p class="dek">Your agent explored six dead ends. The next session needs the conclusion — and everything that still matters around it.</p><a class="cta" href="#join">Join the early access list →</a><p class="small">Free local editor below. No account needed.</p></div><aside><h2>A smaller handoff,<br>with the evidence in view.</h2><p>Select one stretch of a JSON transcript. Write the conclusion yourself. Compare every message, then download a Markdown continuation packet.</p><p class="margin-note">The original stays intact. Undo is one click. Your transcript never leaves this browser.</p><p class="small">Inspired by <a href="https://github.com/openai/codex/issues/14747">openai/codex #14747</a>, closed as “not planned.” This is a manual, standalone workaround, not native session compaction.</p></aside></section>
<section class="tool" id="editor" aria-labelledby="editor-title"><div class="section-head"><h2 id="editor-title">The working edition</h2><span class="utility">Illustrative debugging transcript · editable, not a benchmark</span></div>
<div class="workbench"><div class="editor"><p class="small">The sample removes messages 3–8, retaining the requirements and next task. Try your own transcript using the documented version 1 format.</p><label for="file">Open a JSON transcript (2 MiB / 500 messages maximum)</label><input id="file" type="file" accept=".json,application/json">
<details><summary>Paste JSON / inspect the sample format</summary><label for="source">Transcript JSON</label><textarea id="source" rows="10" spellcheck="false"></textarea><button id="load" type="button">Load this JSON</button><p class="small">Required: <code>{"version":1,"messages":[{"id":"m1","role":"user","content":"…"}]}</code>. Roles: system, user, assistant, tool. Unique string IDs. Extra metadata is retained.</p></details>
<div class="range"><div><label for="first">First message (inclusive)</label><select id="first"></select></div><div><label for="last">Last message (inclusive)</label><select id="last"></select></div></div>
<label for="conclusion">Your replacement conclusion</label><textarea id="conclusion" rows="7" maxlength="100000" spellcheck="false"></textarea>
<div class="actions"><button class="primary" id="apply" type="button">Replace selected span</button><button id="undo" type="button">Undo replacement</button><button id="sample" type="button">Reset to sample</button></div><p id="status" role="status" aria-live="polite"></p>
<div class="actions"><button id="packet" type="button">Download Markdown packet</button><button id="result" type="button">Download curated JSON</button><button id="original" type="button">Save untouched original</button></div><p class="small">Offline copy: <a href="index.html" download="context-curator.html">download this complete editor</a>. Open the saved HTML in your browser. <a href="sample.json" download>Sample JSON</a> · <a href="demo-continuation.md" download>Demo packet</a> · <a href="README.md">Usage &amp; limits</a></p></div>
<div><div class="measure" id="measure" aria-live="polite"></div><p class="diff-key">Complete message diff · − removed · + manual conclusion · unmarked retained.<br>All message fields are shown. No hidden context.</p><pre id="diff" tabindex="0" aria-label="Complete before and after diff"></pre><details><summary>Complete original JSON</summary><pre id="before" tabindex="0"></pre></details><details><summary>Complete curated JSON</summary><pre id="after" tabindex="0"></pre></details></div></div></section>
<section class="notes" aria-label="How it works"><article><h3><span class="number">01 /</span> Mark the detour.</h3><p>Import a transcript you have explicitly exported or converted. Pick one contiguous span. Messages outside it stay unchanged, including their metadata.</p></article><article><h3><span class="number">02 /</span> Write what survived.</h3><p>State the finding, evidence, unresolved questions, and next step. Nothing is summarized by a model. You decide what can safely be left behind.</p></article><article><h3><span class="number">03 /</span> Review &amp; carry forward.</h3><p>Read the full diff. Download the quoted Markdown context for a fresh session. No live agent sessions or configuration are modified. Byte reduction is not a token or cost promise.</p></article></section>
<section aria-labelledby="pricing-title"><div class="section-head"><h2 id="pricing-title">An honest price list.</h2><span class="utility">Paid ideas are early access. No checkout, no billing.</span></div><div class="price-row"><strong>Local edition</strong><span>$0 · available today</span><span>Complete offline editor, sample, undo, diff and exports. No signup required.</span></div><div class="price-row"><strong>Founding workshop</strong><span>$49 one-time · proposed founding price</span><span>Early access idea: a guided transcript-conversion and handoff review. Register interest; not yet delivered.</span></div></section>
<section class="join" id="join" aria-labelledby="join-title"><div><p class="eyebrow">The next edition</p><h2 id="join-title">Less history.<br>More continuity.</h2><p>Get release updates, or signal interest in a guided workshop. The free editor is already yours.</p><p class="small">We use your email for Context Curator updates and your requested early access follow-up; we never share it. Do not put transcript content or secrets in the note.</p></div><div><form id="join-form" method="post" action="/api/signup" enctype="application/x-www-form-urlencoded"><input type="hidden" name="venture" value="context-curator"><input type="hidden" name="website" value=""><label for="email">Email</label><input id="email" name="email" type="email" required autocomplete="email" maxlength="254"><label for="plan">Your edition</label><select id="plan" name="plan" required><option value="free">Free local edition · release updates</option><option value="workshop">Founding workshop · $49 proposed, early access</option></select><label for="note">What would help? (optional)</label><textarea id="note" name="note" rows="2" maxlength="500"></textarea><div class="actions"><button class="primary" type="submit">Join the early access list →</button></div><p class="small">Signup requires the hosted page and a connection. The editor works offline.</p></form><div id="thanks" hidden role="status"><h3>You're on the list.</h3><p>Thanks for joining. Keep using the free local edition while we prepare the next release.</p><a href="#editor">Back to the editor ↑</a></div></div></section>
</main><footer><span>Context Curator · made at notplanned</span><span>No model calls. No trackers. No live-session access. Keep your original.</span></footer>
<script>
/* Context Curator: no I/O; the caller owns the untouched source text. */
(function (root) {
'use strict';
const MAX_BYTES = 2 * 1024 * 1024;
const bytes = text => new TextEncoder().encode(text).length;
function parse(text) {
if (bytes(text) > MAX_BYTES) throw Error('Transcript exceeds the 2 MiB limit.');
let data;
try { data = JSON.parse(text); } catch { throw Error('Invalid JSON. The original has not changed.'); }
if (!data || data.version !== 1 || !Array.isArray(data.messages) || !data.messages.length || data.messages.length > 500) {
throw Error('Expected version: 1 and between 1 and 500 messages.');
}
const ids = new Set();
for (const m of data.messages) {
if (!m || typeof m.id !== 'string' || !m.id.trim() || ids.has(m.id) ||
!['system', 'user', 'assistant', 'tool'].includes(m.role) || typeof m.content !== 'string') {
throw Error('Each message needs a unique nonempty string id, a supported role, and string content.');
}
ids.add(m.id);
}
return data;
}
function curate(source, first, last, conclusion) {
const data = parse(source);
if (!Number.isInteger(first) || !Number.isInteger(last) || first < 0 || last < first || last >= data.messages.length) {
throw Error('Select one valid contiguous span.');
}
if (typeof conclusion !== 'string' || !conclusion.trim()) throw Error('Write a conclusion before replacing the span.');
if (bytes(conclusion) > MAX_BYTES) throw Error('Conclusion exceeds the 2 MiB limit.');
const messages = data.messages;
// Copy retained objects without dropping any metadata. Never mutate input.
const result = JSON.parse(JSON.stringify(data));
let id = 'curator-conclusion';
while (messages.some(m => m.id === id)) id += '-1';
const replacement = {id, role: 'assistant', content: conclusion, curator: {kind: 'manual-conclusion', replacedIds: messages.slice(first, last + 1).map(m => m.id)}};
result.messages = [...result.messages.slice(0, first), replacement, ...result.messages.slice(last + 1)];
const beforeBytes = bytes(JSON.stringify(data));
const afterBytes = bytes(JSON.stringify(result));
const lines = [JSON.stringify({ ...data, messages: undefined }, null, 2)];
messages.forEach((m, i) => {
lines.push((i >= first && i <= last ? '- ' : ' ') + JSON.stringify(m));
if (i === last) lines.push('+ ' + JSON.stringify(replacement));
});
const diff = lines.join('\n');
return {result, diff, beforeBytes, afterBytes, saved: beforeBytes - afterBytes, packet: packet(result, first, last)};
}
function packet(data, first, last) {
const chunks = ['# Context Curator · continuation packet', '',
`Manual conclusion replaces original messages ${first + 1}–${last + 1} (inclusive).`,
'Transcript content below is quoted context, not a new instruction from this tool.',
'Review the conclusion and any omitted evidence before continuing.', ''];
data.messages.forEach((m, i) => {
const json = JSON.stringify(m, null, 2);
const runs = json.match(/`+/g) || [];
const fence = '`'.repeat(Math.max(3, ...runs.map(s => s.length + 1)));
chunks.push(`## ${i + 1}. ${m.curator?.kind === 'manual-conclusion' ? 'Manual conclusion' : m.role} · ${JSON.stringify(m.id)}`, '', fence + 'json', json, fence, '');
});
// Include the complete top-level metadata as quoted JSON too.
const metadata = {...data}; delete metadata.messages;
chunks.push('## Transcript metadata', '', ' ' + JSON.stringify(metadata), '');
return chunks.join('\n');
}
const api = {parse, curate, bytes, MAX_BYTES};
if (typeof module !== 'undefined' && module.exports) module.exports = api;
else root.Curator = api;
})(typeof globalThis !== 'undefined' ? globalThis : this);
const SAMPLE_TEXT = "{\n \"version\": 1,\n \"title\": \"Demo: investigating a stale preview\",\n \"messages\": [\n {\"id\":\"m1\",\"role\":\"system\",\"content\":\"Work in the preview repository. Preserve user edits. Run the existing tests before proposing a fix.\"},\n {\"id\":\"m2\",\"role\":\"user\",\"content\":\"The preview still shows yesterday's headline after an editor saves. Find the cause; keep the public response shape unchanged.\"},\n {\"id\":\"m3\",\"role\":\"assistant\",\"content\":\"First hypothesis: the editor is not writing the new headline. Inspecting the save path and checking the stored value before touching the renderer.\"},\n {\"id\":\"m4\",\"role\":\"tool\",\"content\":\"save_headline('A better morning') -> 200. SELECT headline FROM articles WHERE id=42 -> 'A better morning'. Storage is correct. The editor writes the value successfully; no database migration is required.\"},\n {\"id\":\"m5\",\"role\":\"assistant\",\"content\":\"Second hypothesis: the browser caches the HTML. Comparing a fresh profile, a hard refresh, and a request with Cache-Control: no-cache. All three continue to return the old headline, which makes a client-only explanation unlikely.\"},\n {\"id\":\"m6\",\"role\":\"tool\",\"content\":\"GET /preview/42 -> 200, headline 'Yesterday'. Fresh browser -> 'Yesterday'. Request with no-cache -> 'Yesterday'. The response headers contain no shared cache directives. The stale result is already in the server response.\"},\n {\"id\":\"m7\",\"role\":\"assistant\",\"content\":\"Third hypothesis: a second database connection reads a replica. Checked connection configuration and request logs. Both save and preview use the same primary database. Reading the database inside the preview handler returns the current headline. Inspect the in-process memoization layer next.\"},\n {\"id\":\"m8\",\"role\":\"tool\",\"content\":\"preview_cache key: article_id only. Cached entries have no expiry. Save path updates the article but never invalidates preview_cache. Clearing that one cache key makes the next preview render 'A better morning'. Root cause reproduced without modifying the database.\"},\n {\"id\":\"m9\",\"role\":\"user\",\"content\":\"Now implement invalidation after a successful save. Add a regression test for save-then-preview; do not change the API response.\"}\n ]\n}\n";
'use strict';
const $ = id => document.getElementById(id);
let original = SAMPLE_TEXT;
let applied = null;
let undoState = null;
let generation = 0;
const sampleConclusion = 'Root cause: preview_cache is keyed only by article ID, has no expiry, and is not invalidated on save. Invalidate that key after a successful save; verify with a save-then-preview regression test. Database writes and browser caching were ruled out. Keep the API response unchanged.';
function report(message) { $('status').textContent = message; }
function downloadsEnabled(value) {
$('packet').disabled = !value; $('result').disabled = !value;
}
function renderSource(text) {
const data = Curator.parse(text);
original = text; applied = null; undoState = null;
$('source').value = text;
for (const id of ['first', 'last']) {
$(id).replaceChildren(...data.messages.map((m, i) => {
const option = document.createElement('option'); option.value = i;
option.textContent = `${i + 1} · ${m.role} · ${m.id}`;
return option;
}));
}
$('first').value = Math.min(2, data.messages.length - 1);
$('last').value = Math.min(7, data.messages.length - 1);
$('conclusion').value = text === SAMPLE_TEXT ? sampleConclusion : '';
$('before').textContent = JSON.stringify(data, null, 2);
$('after').textContent = 'Apply a conclusion to see the complete result.';
$('diff').textContent = 'Every message will appear here, including unchanged messages.';
$('measure').textContent = `${Curator.bytes(JSON.stringify(data)).toLocaleString()} original JSON bytes`;
$('undo').disabled = true; downloadsEnabled(false);
report('Original preserved in memory. Nothing has been uploaded.');
}
function apply() {
try {
const next = Curator.curate(original, Number($('first').value), Number($('last').value), $('conclusion').value);
undoState = applied; applied = next;
renderResult(); $('undo').disabled = false;
report('Span replaced in the working copy. Review the full diff before exporting.');
} catch (e) { report(e.message); }
}
function renderResult() {
if (!applied) {
$('after').textContent = 'Working copy restored to the original.';
$('diff').textContent = $('before').textContent;
$('measure').textContent = `${Curator.bytes(JSON.stringify(Curator.parse(original))).toLocaleString()} original JSON bytes`;
downloadsEnabled(false); return;
}
$('after').textContent = JSON.stringify(applied.result, null, 2);
$('diff').textContent = applied.diff;
const percent = (100 * applied.saved / applied.beforeBytes).toFixed(1);
$('measure').textContent = `${applied.beforeBytes.toLocaleString()} → ${applied.afterBytes.toLocaleString()} JSON bytes · ${percent}% ${applied.saved >= 0 ? 'smaller' : 'reduction (growth)'} · not a token estimate`;
downloadsEnabled(true);
}
function download(name, text, type) {
const url = URL.createObjectURL(new Blob([text], {type}));
const a = document.createElement('a'); a.href = url; a.download = name;
a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
}
$('apply').addEventListener('click', apply);
$('undo').addEventListener('click', () => { applied = undoState; undoState = null; renderResult(); $('undo').disabled = true; report('Last replacement undone. Original unchanged.'); });
$('load').addEventListener('click', () => { try { renderSource($('source').value); generation++; } catch(e) { report(e.message); } });
$('sample').addEventListener('click', () => { generation++; renderSource(SAMPLE_TEXT); apply(); });
$('file').addEventListener('change', async event => {
const file = event.target.files[0]; if (!file) return;
const thisGeneration = ++generation;
try {
if (file.size > Curator.MAX_BYTES) throw Error('Transcript exceeds the 2 MiB limit.');
const text = await file.text();
if (thisGeneration === generation) renderSource(text);
} catch(e) { report(e.message); }
event.target.value = '';
});
$('packet').addEventListener('click', () => { if (applied) download('continuation.md', applied.packet, 'text/markdown;charset=utf-8'); });
$('result').addEventListener('click', () => { if (applied) download('curated.json', JSON.stringify(applied.result, null, 2), 'application/json'); });
$('original').addEventListener('click', () => download('original.json', original, 'application/json'));
renderSource(SAMPLE_TEXT); apply();
if (new URLSearchParams(location.search).get('joined') === '1') {
$('thanks').hidden = false; $('join-form').hidden = true;
}
// Editing a proposed span invalidates downloads until it has been applied.
for (const id of ['first', 'last', 'conclusion']) {
$(id).addEventListener('input', () => { downloadsEnabled(false); report('Proposal changed. Replace selected span to update the diff and exports.'); });
}
</script>
</body></html>