← back to Answer Cockpit
chore: lint, refactor, v0.2.0 (session close)
813fd6af7e4d52e5f66d81f0daa3cadd440f8430 · 2026-09-16 08:38:11 -0700 · Steve Abrams
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Files touched
M lib/queue.jsM lib/transcript.jsM lib/writeback.jsM package.jsonM server.js
Diff
commit 813fd6af7e4d52e5f66d81f0daa3cadd440f8430
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 16 08:38:11 2026 -0700
chore: lint, refactor, v0.2.0 (session close)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---
lib/queue.js | 8 +++++---
lib/transcript.js | 8 +++++---
lib/writeback.js | 4 +++-
package.json | 6 ++++--
server.js | 9 +++++----
5 files changed, 22 insertions(+), 13 deletions(-)
diff --git a/lib/queue.js b/lib/queue.js
index ada641e..c370f20 100644
--- a/lib/queue.js
+++ b/lib/queue.js
@@ -9,11 +9,13 @@
// sort variant=="stopped" first then PRIORITY, attach rgb/emoji from
// COLORS (terminal_status.py:48-61), merge memos by TK, resolve detail.
const path = require('path');
+const crypto = require('crypto');
const { execFile } = require('child_process');
const transcript = require('./transcript');
const memo = require('./memo');
const audit = require('./audit');
const writeback = require('./writeback');
+const ticketlane = require('./ticketlane');
const HOME = process.env.HOME || require('os').homedir();
const TS_PY = path.join(HOME, 'Projects/terminal-status/terminal_status.py');
@@ -126,7 +128,7 @@ function digestOf(detail) {
// correct click in the same second. Labels are identical across both sources.
const basis = q.options.map((o) => String(o.label || '').replace(/\s+/g, ' ').trim().toLowerCase()).join('|');
if (!basis) return '-';
- return require('crypto').createHash('sha1').update(basis).digest('hex').slice(0, 8);
+ return crypto.createHash('sha1').update(basis).digest('hex').slice(0, 8);
}
function keyOf(row, detail) { return `${row.tty}|${row.ticket || '-'}|${row.color}|${digestOf(detail)}`; }
function keyFor(row) { const s = transcript.resolve(row); return keyOf(row, s.detail); }
@@ -216,8 +218,8 @@ async function build(opts = {}) {
// open — invisible to a tty scan by construction. Display/route only: nothing to type into.
let ticketBacklog = [], ticketBacklogCount = 0;
try {
- const all = await require('./ticketlane').loadTickets();
- const lane = all.filter((c) => !linked.has(c.ticket)).map((c) => ({ ...c, urgency: require('./ticketlane').urgency(c), body: String(c.body || '').slice(0, 800) }))
+ const all = await ticketlane.loadTickets();
+ const lane = all.filter((c) => !linked.has(c.ticket)).map((c) => ({ ...c, urgency: ticketlane.urgency(c), body: String(c.body || '').slice(0, 800) }))
.sort((a, b) => b.urgency - a.urgency || new Date(a.created) - new Date(b.created));
ticketBacklogCount = lane.length;
ticketBacklog = opts.orphans ? lane.slice(0, 80) : [];
diff --git a/lib/transcript.js b/lib/transcript.js
index ed5c072..c16a87e 100644
--- a/lib/transcript.js
+++ b/lib/transcript.js
@@ -13,6 +13,7 @@
// Cache 30s per pid; re-resolve when updated_at changes.
const fs = require('fs');
const path = require('path');
+const crypto = require('crypto');
const { execFileSync } = require('child_process');
const HOME = process.env.HOME || require('os').homedir();
@@ -21,6 +22,7 @@ const SESSIONS = path.join(HOME, '.claude/sessions');
const MAP_DIR = path.join(HOME, '.claude/answer-cockpit/map');
const TAIL_BYTES = 256 * 1024;
const CACHE_MS = 30 * 1000;
+const LAST_TEXT_MAX = 2000; // lastText char cap (parseTail + parsePane)
const cache = new Map(); // pid → {key, ts, result}
// ---- tail parser (copied from claude-control-center/server.js:337-356 tailJsonl) ----
@@ -101,7 +103,7 @@ function parseTail(filepath) {
}
return {
question, answeredQuestion,
- lastText: lastText ? lastText.slice(-2000) : null,
+ lastText: lastText ? lastText.slice(-LAST_TEXT_MAX) : null,
pasteCmd, lastTs, sessionId, cwd, turns: turns.length,
};
}
@@ -243,7 +245,7 @@ function allPaneContents() {
if (Date.now() - paneBatch.ts < PANE_BATCH_MS) return paneBatch.map;
// Per-call NONCE delimiters: a pane that happens to print the literal delimiter (e.g. a session
// reviewing this file) can no longer truncate its own capture (Cody FIX-FIRST #4).
- const nonce = require('crypto').randomBytes(6).toString('hex');
+ const nonce = crypto.randomBytes(6).toString('hex');
const BEGIN = '@@TTY-' + nonce + ' ', END = '@@END-' + nonce;
const script = `tell application "iTerm2"
set out to ""
@@ -369,7 +371,7 @@ function parsePane(text) {
const tail = content.slice(-45);
let pasteCmd = null;
for (let i = tail.length - 1; i >= 0 && !pasteCmd; i--) { const m = tail[i].match(/^\s*!\s+(.+)$/); if (m) pasteCmd = '! ' + m[1].trim(); }
- const lastText = (degenerate ? nonEmpty.map((l) => l.trim()).join('') : tail.join('\n')).slice(-2000);
+ const lastText = (degenerate ? nonEmpty.map((l) => l.trim()).join('') : tail.join('\n')).slice(-LAST_TEXT_MAX);
// answeredQuestion is UNKNOWN in pane mode (no tool_use ids) — null, never a false "not stale".
return { question, answeredQuestion: null, lastText: lastText || null, pasteCmd, lastTs: null, sessionId: null, cwd: null, turns: 0, source: 'pane', queued, degenerate, menuOnScreen: footer >= 0 };
}
diff --git a/lib/writeback.js b/lib/writeback.js
index 35b4a13..2f2303a 100644
--- a/lib/writeback.js
+++ b/lib/writeback.js
@@ -17,6 +17,8 @@
// - every accessor inside `try`; returns {typed:true} only when a tty matched.
// - refuse the cockpit's own tty (climb ppid like colordots.sh:61-72).
const fs = require('fs');
+const path = require('path');
+const os = require('os');
const { execFile, execFileSync } = require('child_process');
const LOCKDIR = '/tmp/answer-cockpit.lock';
@@ -183,7 +185,7 @@ async function focus(tty, opts = {}) {
function repaint(tty, label) {
return new Promise((resolve) => {
if (!TTY_RE.test(String(tty))) return resolve({ ok: false, err: 'bad tty' });
- const py = require('path').join(process.env.HOME || require('os').homedir(), 'Projects/terminal-status/terminal_status.py');
+ const py = path.join(process.env.HOME || os.homedir(), 'Projects/terminal-status/terminal_status.py');
execFile('python3', [py, 'set', 'green', String(label || '').slice(0, 120), '--tty', tty, '--quiet'], { timeout: 30000 }, (err, stdout, stderr) => {
if (err) return resolve({ ok: false, err: (stderr || err.message).trim().slice(0, 300) });
resolve({ ok: true });
diff --git a/package.json b/package.json
index c331282..98bd593 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "answer-cockpit",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "Answer Cockpit — one-card needs-Steve stream (:9805). Zero-dependency Node http.",
"main": "server.js",
@@ -8,5 +8,7 @@
"start": "node server.js",
"smoke": "node scripts/smoke.js"
},
- "engines": { "node": ">=18" }
+ "engines": {
+ "node": ">=18"
+ }
}
diff --git a/server.js b/server.js
index 19054b2..8e8ed96 100644
--- a/server.js
+++ b/server.js
@@ -28,6 +28,8 @@ const queue = require('./lib/queue');
const memo = require('./lib/memo');
const writeback = require('./lib/writeback');
const audit = require('./lib/audit');
+const transcript = require('./lib/transcript');
+const menu = require('./lib/menu');
const HOST = '127.0.0.1'; // hard-coded on purpose — never expose (it types into live agents)
const TEST = process.argv.includes('--test');
@@ -90,9 +92,8 @@ async function guardTarget(body, { needsSteveOnly = true, requireKey = true } =
// menu is live on screen (a pane-detected question is answerable even if the dot is none/green).
// GENUINELY fresh: drop the 4s pane batch + this pid's 30s resolve cache before resolving, so a
// menu that closed/advanced since the last poll cannot digest-match a stale card (Cody #3).
- const tr = require('./lib/transcript');
- tr.invalidatePanes(); tr._cache.delete(String(row.pid));
- const sess = tr.resolve(row);
+ transcript.invalidatePanes(); transcript._cache.delete(String(row.pid));
+ const sess = transcript.resolve(row);
const liveMenu = !!(sess.detail && sess.detail.question && !sess.detail.queued);
if (requireKey) {
const key = queue.keyOf(row, sess.detail); // includes the content digest — a changed question 409s
@@ -171,7 +172,7 @@ const server = http.createServer(async (req, res) => {
// against a live menu (a stale client or a curl script must not be able to mis-answer).
let typed = text, optionLabel = typeof body.optionLabel === 'string' ? body.optionLabel.slice(0, 200) : null, translated = false;
if (g.liveMenu && !(body.force === true && body.rawText === true)) {
- const m = require('./lib/menu').resolveMenuAnswer(text, g.sess.detail.question);
+ const m = menu.resolveMenuAnswer(text, g.sess.detail.question);
if (m.error) { audit.audit({ action: 'answer', tty: g.row.tty, ticket: g.row.ticket, color: g.row.color, text, ok: false, err: m.error, refused: true }); return json(res, 400, { error: m.error, liveMenu: true }); }
typed = m.text; translated = m.translated; if (m.optionLabel) optionLabel = m.optionLabel;
}
← c938da9 TK-11793: pane batch TTL 12s + warmed once per build (/api/q
·
back to Answer Cockpit
·
TK-11793: scale fix at 51 sessions — background async pane w 0ace03e →