@chatpanel/events 0.91.1 → 0.92.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/index.js +1 -1
- package/package.json +1 -1
- package/team-board.js +1 -1
- package/team-record.js +59 -6
- package/team-run.js +22 -0
package/index.js
CHANGED
|
@@ -197,7 +197,7 @@ export { fixedPlan, parsePlan, plannerPrompt, waves, breakCycles, TEAM_PLAN_SCHE
|
|
|
197
197
|
export { createBoard, parseFindings, boardText, findingsInstruction, toBriefClaims, FINDINGS_SCHEMA, FINDING_KINDS, THREAD_KINDS, THREAD_STATUSES, POST_KINDS, POST_STATUSES, ASK_TYPES, emptyBoardState, foldBoard, findingsOf } from './team-board.js';
|
|
198
198
|
export { boardToolProvider, boardToolSpec, createAnswerBox, withBoardTool, BOARD_TOOL_NAME, DEFAULT_ASK_TIMEOUT_MS } from './board-tool.js';
|
|
199
199
|
export { createRunCache, withRunCache } from './team-cache.js';
|
|
200
|
-
export { emptyRun, foldRun, runFromEvents, checkpointFrom, isResumable, LIVE_RUN_STATUSES, RESUMABLE_RUN_STATUSES , spendOf, describeSpend } from './team-record.js';
|
|
200
|
+
export { emptyRun, foldRun, runFromEvents, checkpointFrom, isResumable, LIVE_RUN_STATUSES, RESUMABLE_RUN_STATUSES , spendOf, describeSpend, runState, priorWorkFor, STALLED_AFTER_MS } from './team-record.js';
|
|
201
201
|
export { validateProject, normalizeProject, defineProject, canTransition as canProjectTransition, blankProject, projectFromForm, emptyProjectRecord, foldProject, projectProgress, ProjectError, PROJECT_STATUSES, PROJECT_ID_RE } from './project.js';
|
|
202
202
|
// Job POSTINGS (F8 §12) — `jobs.js` is the scheduler and keeps `defineJob`; a posting is a JobPost here.
|
|
203
203
|
export { validateJob as validateJobPost, normalizeJob as normalizeJobPost, defineJob as defineJobPost, canTransition as canJobPostTransition, applyAll, jobToRole, readyJobs, blankJob as blankJobPost, jobFromForm as jobPostFromForm, JobError as JobPostError, JOB_STATUSES as JOB_POST_STATUSES, JOB_ID_RE as JOB_POST_ID_RE } from './job.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts \u2014 typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/team-board.js
CHANGED
|
@@ -234,7 +234,7 @@ export function createBoard({ now = () => Date.now(), newId = null, state = null
|
|
|
234
234
|
function asFinding(st) {
|
|
235
235
|
return (p) => {
|
|
236
236
|
const t = st.threads.find((x) => x.id === p.threadId);
|
|
237
|
-
return { id: p.id, kind: p.finding?.kind || 'claim', text: p.text, refs: p.refs || [], confidence: p.finding?.confidence ?? null, role: p.by, taskId: t?.taskId || null, at: p.at, status: p.status };
|
|
237
|
+
return { id: p.id, kind: p.finding?.kind || 'claim', text: p.text, refs: p.refs || [], confidence: p.finding?.confidence ?? null, role: p.by, taskId: t?.taskId || null, at: p.at, status: p.status, ...(p.finding?.prior ? { prior: true } : {}) };
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
240
|
|
package/team-record.js
CHANGED
|
@@ -132,9 +132,61 @@ export function spendOf(run, { now = Date.now() } = {}) {
|
|
|
132
132
|
const cap = run?.usage?.cap || run?.budget || null;
|
|
133
133
|
if (!cap || !Object.keys(cap).length) return null;
|
|
134
134
|
const spent = { tokens: 0, calls: 0, usd: 0, ms: 0, ...(run?.usage?.spent || {}) };
|
|
135
|
-
|
|
135
|
+
// The clock runs while the client is writing, and STOPS where it stopped: a run whose
|
|
136
|
+
// record says `running` because its client died (or its end never landed) is not still
|
|
137
|
+
// spending — it read "18m13s / 5m00s" and counting for a member that had finished.
|
|
138
|
+
const st = runState(run, { now });
|
|
139
|
+
if (LIVE_RUN_STATUSES.includes(run?.status) && run?.startedAt) spent.ms = Math.max(spent.ms || 0, (st.key === 'stalled' ? Number(run.lastEventAt) || now : now) - run.startedAt);
|
|
136
140
|
const pct = cap.tokens ? Math.min(100, Math.round(((spent.tokens || 0) / cap.tokens) * 100)) : cap.ms ? Math.min(100, Math.round(((spent.ms || 0) / cap.ms) * 100)) : null;
|
|
137
|
-
|
|
141
|
+
const over = BUDGET_KEYS.filter((k) => cap[k] && (spent[k] || 0) >= cap[k]);
|
|
142
|
+
return { cap, spent, pct, exhausted: run?.usage?.exhausted || over[0] || null, over };
|
|
143
|
+
}
|
|
144
|
+
const BUDGET_KEYS = ['tokens', 'calls', 'usd', 'ms'];
|
|
145
|
+
|
|
146
|
+
export const STALLED_AFTER_MS = 2 * 60_000;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* What a run IS right now, for a person: the record's status read against the clock. A
|
|
150
|
+
* record that says `running` with no event for minutes is STALLED — its client stopped
|
|
151
|
+
* writing (died, or its end never landed) — not running. `{ key, label, tone, detail }`;
|
|
152
|
+
* `tone` is the chip: on · warn · ok · err · muted.
|
|
153
|
+
*/
|
|
154
|
+
export function runState(run, { now = Date.now(), stalledAfterMs = STALLED_AFTER_MS } = {}) {
|
|
155
|
+
const s = String(run?.status || 'planning');
|
|
156
|
+
const quiet = Number.isFinite(run?.quietMs) ? run.quietMs : Math.max(0, now - (Number(run?.lastEventAt) || now));
|
|
157
|
+
const agoText = (ms) => { const sec = Math.round(ms / 1000); return sec < 60 ? `${sec} s` : sec < 3600 ? `${Math.round(sec / 60)} min` : `${Math.round(sec / 3600)} h`; };
|
|
158
|
+
if (s === 'waiting') return { key: 'waiting', label: 'waiting on you', tone: 'warn', detail: '' };
|
|
159
|
+
if (LIVE_RUN_STATUSES.includes(s)) {
|
|
160
|
+
if (run?.stale === true || quiet > stalledAfterMs) return { key: 'stalled', label: 'stalled', tone: 'err', detail: `no events for ${agoText(quiet)} — its client stopped writing; Resume here picks it up from the record` };
|
|
161
|
+
return { key: 'running', label: s === 'merging' ? 'merging' : s === 'planning' ? 'planning' : 'running', tone: 'on', detail: `last event ${agoText(quiet)} ago` };
|
|
162
|
+
}
|
|
163
|
+
if (s === 'completed') return { key: 'done', label: 'done', tone: 'ok', detail: '' };
|
|
164
|
+
if (s === 'partial') return { key: 'partial', label: 'done with failures', tone: 'warn', detail: 'a member failed; the rest merged' };
|
|
165
|
+
if (s === 'answered') return { key: 'answered', label: 'answered — resume to continue', tone: 'warn', detail: '' };
|
|
166
|
+
if (s === 'over-budget') return { key: 'over-budget', label: 'over budget', tone: 'err', detail: 'stopped with what it had' };
|
|
167
|
+
if (s === 'stopped') return { key: 'stopped', label: 'stopped', tone: 'muted', detail: '' };
|
|
168
|
+
if (s === 'failed') return { key: 'failed', label: 'failed', tone: 'err', detail: '' };
|
|
169
|
+
return { key: s, label: s, tone: 'muted', detail: '' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Earlier runs whose work a new run should read before repeating it (§12.2.6, the librarian's
|
|
174
|
+
* first step): the same team (or any, when `team` is empty), a request that says the same
|
|
175
|
+
* thing (word overlap ≥ `minSimilarity`), findings on the record, not the run itself, newest
|
|
176
|
+
* first. `runs` is the store's list (`findings` is a count there). Returns
|
|
177
|
+
* `[{ id, at, similarity, findings }]`; the host fetches the record for the findings.
|
|
178
|
+
*/
|
|
179
|
+
export function priorWorkFor(runs, { team = '', request = '', excludeId = null, minSimilarity = 0.6, maxAgeMs = 7 * 24 * 3600_000, now = Date.now(), limit = 3 } = {}) {
|
|
180
|
+
const words = (t) => new Set(String(t || '').toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, ' ').split(/\s+/).filter((w) => w.length > 2));
|
|
181
|
+
const want = words(request);
|
|
182
|
+
if (!want.size) return [];
|
|
183
|
+
const sim = (t) => { const have = words(t); if (!have.size) return 0; let hit = 0; for (const w of want) if (have.has(w)) hit += 1; return hit / Math.max(want.size, have.size); };
|
|
184
|
+
return (runs || [])
|
|
185
|
+
.filter((r) => r && r.id && r.id !== excludeId && (!team || r.team === team) && !LIVE_RUN_STATUSES.includes(r.status) && (Number(r.findings) || (Array.isArray(r.board) ? r.board.length : 0)) > 0 && now - (Number(r.createdAt) || 0) <= maxAgeMs)
|
|
186
|
+
.map((r) => ({ id: r.id, at: r.createdAt, similarity: Math.round(sim(r.request) * 100) / 100, findings: Number(r.findings) || (Array.isArray(r.board) ? r.board.length : 0), status: r.status }))
|
|
187
|
+
.filter((r) => r.similarity >= minSimilarity)
|
|
188
|
+
.sort((a, b) => b.similarity - a.similarity || b.at - a.at)
|
|
189
|
+
.slice(0, limit);
|
|
138
190
|
}
|
|
139
191
|
|
|
140
192
|
const secs = (ms) => { const s = Math.max(0, Math.round((Number(ms) || 0) / 1000)); return s >= 60 ? `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s` : `${s}s`; };
|
|
@@ -144,10 +196,11 @@ const num = (n) => (Number(n) || 0).toLocaleString('en-US');
|
|
|
144
196
|
export function describeSpend(spend) {
|
|
145
197
|
if (!spend?.cap) return '';
|
|
146
198
|
const { cap, spent } = spend;
|
|
199
|
+
const over = (k) => ((spend.over || []).includes(k) ? ' (over)' : '');
|
|
147
200
|
return [
|
|
148
|
-
cap.tokens ? `${num(spent.tokens)} / ${num(cap.tokens)} tokens` : '',
|
|
149
|
-
cap.calls ? `${num(spent.calls)} / ${num(cap.calls)} calls` : '',
|
|
150
|
-
cap.usd ? `$${(Number(spent.usd) || 0).toFixed(2)} / $${Number(cap.usd).toFixed(2)}` : '',
|
|
151
|
-
cap.ms ? `${secs(spent.ms)} / ${secs(cap.ms)}` : '',
|
|
201
|
+
cap.tokens ? `${num(spent.tokens)} / ${num(cap.tokens)} tokens${over('tokens')}` : '',
|
|
202
|
+
cap.calls ? `${num(spent.calls)} / ${num(cap.calls)} calls${over('calls')}` : '',
|
|
203
|
+
cap.usd ? `$${(Number(spent.usd) || 0).toFixed(2)} / $${Number(cap.usd).toFixed(2)}${over('usd')}` : '',
|
|
204
|
+
cap.ms ? `${secs(spent.ms)} / ${secs(cap.ms)}${over('ms')}` : '',
|
|
152
205
|
].filter(Boolean).join(' · ');
|
|
153
206
|
}
|
package/team-run.js
CHANGED
|
@@ -127,6 +127,10 @@ export async function runTeam({
|
|
|
127
127
|
// model from the board, on either client; the runner continues the task's transcript there.
|
|
128
128
|
control = null,
|
|
129
129
|
recruit = null, projectId = null,
|
|
130
|
+
// Earlier runs' findings for the same request (team-record.js priorWorkFor, fetched by the
|
|
131
|
+
// host): `[{ runId, at, request?, findings: [...] }]`. Posted as a "Prior work" thread every
|
|
132
|
+
// member reads before repeating a lookup — the librarian's first step (§12.2.6).
|
|
133
|
+
prior = null,
|
|
130
134
|
} = {}) {
|
|
131
135
|
if (typeof callModel !== 'function') throw new TeamRunError('BAD_RUN', 'callModel required');
|
|
132
136
|
const t = normalizeTeam(team); // throws on a team without a budget — O1
|
|
@@ -197,6 +201,24 @@ export async function runTeam({
|
|
|
197
201
|
say('plan.ready', { by: planBy, tasks: tasks.map((x) => ({ id: x.id, role: x.role, title: x.title, dependsOn: x.dependsOn, ...(x.parent ? { parent: x.parent, requestedBy: x.requestedBy || null } : {}), ...(x.grants ? { grants: x.grants, why: x.why || '' } : {}) })) });
|
|
198
202
|
// A thread per task, before anything runs: a member's findings and replies have a home.
|
|
199
203
|
for (const task of tasks) board.openThread({ taskId: task.id, kind: 'task', title: task.title || task.id, by: task.requestedBy || RUNNER, ...(task.parent ? { parent: task.parent } : {}), ...(task.role && task.parent ? { holder: task.role } : {}) });
|
|
204
|
+
// PRIOR WORK: what an earlier run already found for this request is on the board before
|
|
205
|
+
// anyone starts — a discussion thread per run, its findings as posts with their refs — so a
|
|
206
|
+
// second run of the same question reads instead of re-searching. Only findings a person did
|
|
207
|
+
// not reject; at most 40 per run.
|
|
208
|
+
if (!resume && Array.isArray(prior)) {
|
|
209
|
+
for (const pr of prior.filter((x) => x && Array.isArray(x.findings) && x.findings.length)) {
|
|
210
|
+
const age = Number.isFinite(pr.at) ? Math.max(0, Math.round((now() - pr.at) / 60000)) : null;
|
|
211
|
+
const th = board.openThread({ kind: 'discussion', title: `Prior work — run ${pr.runId}${age != null ? ` (${age < 60 ? `${age} min` : `${Math.round(age / 60)} h`} ago)` : ''}`, by: RUNNER });
|
|
212
|
+
board.post({ threadId: th.id, by: RUNNER, kind: 'note', text: `An earlier run answered ${pr.request ? `"${String(pr.request).slice(0, 200)}"` : 'the same request'}. Its findings follow — read them, verify what is stale, and do not repeat lookups already made.`, refs: [`run:${pr.runId}`] });
|
|
213
|
+
let n = 0;
|
|
214
|
+
for (const f of pr.findings) {
|
|
215
|
+
if (!f || !f.text || f.status === 'rejected' || n >= 40) continue;
|
|
216
|
+
board.post({ threadId: th.id, by: f.role || RUNNER, kind: 'finding', text: String(f.text).slice(0, 2000), refs: [...(Array.isArray(f.refs) ? f.refs : []), `run:${pr.runId}`].slice(0, 8), finding: { kind: f.kind || 'claim', confidence: f.confidence ?? null, prior: true } });
|
|
217
|
+
n += 1;
|
|
218
|
+
}
|
|
219
|
+
say('run.prior', { from: pr.runId, threadId: th.id, findings: n });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
200
222
|
// The planner's TOOL PROPOSAL (§15.2): which tools each task will need and why, as a
|
|
201
223
|
// proposal thread a person reads — and what the nudge below holds the member to.
|
|
202
224
|
if (!resume && tasks.some((x) => x.grants?.length)) {
|