@chatpanel/events 0.97.0 → 0.98.1

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/model-picker.js CHANGED
@@ -102,9 +102,9 @@ export function groupModels(models, { selectedId = '' } = {}) {
102
102
  if (agents.length) {
103
103
  sections.push({
104
104
  key: 'agents',
105
- // Naming phase 1 (naming-revamp.md): the CLI coding agents are HARNESSES — a runtime one
105
+ // Naming (naming-revamp.md): the CLI coding agents are AGENT TOOLS — a runtime one
106
106
  // of the user's own agents can be given as its engine. The key and kind are code.
107
- label: 'Harnesses',
107
+ label: 'Agent Tools',
108
108
  kind: 'agent',
109
109
  items: agents.slice().sort(availableFirst).map(decorate),
110
110
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/events",
3
- "version": "0.97.0",
3
+ "version": "0.98.1",
4
4
  "description": "The canonical ChatPanel event-log and capability contracts — 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/recruit.js CHANGED
@@ -143,7 +143,7 @@ export function needForJob(job, { reach = 'any' } = {}) {
143
143
  const capabilities = [];
144
144
  const why = [];
145
145
  if (tools.length || grants.length) { capabilities.push('tools'); why.push('the job uses tools'); }
146
- if (harness) why.push(`a work grant (${grants.filter((g) => WORK_GRANTS.includes(g)).join(', ')}) needs a harness`);
146
+ if (harness) why.push(`a work grant (${grants.filter((g) => WORK_GRANTS.includes(g)).join(', ')}) needs an agent tool`);
147
147
  const r = REACH_RANK[reach] != null ? reach : 'any';
148
148
  if (r !== 'any') why.push(`reach ≤ ${r} (the project's privacy setting)`);
149
149
  return { capabilities, harness, reach: r, why };
@@ -155,7 +155,7 @@ const meets = (row, need, policy) => {
155
155
  const why = [];
156
156
  if (row.available === false) why.push('unavailable right now');
157
157
  if (REACH_RANK[row.reach] > REACH_RANK[need.reach]) why.push(`reach ${row.reach} exceeds ${need.reach}`);
158
- if (need.harness && row.engine.kind !== 'harness') why.push('not a harness');
158
+ if (need.harness && row.engine.kind !== 'harness') why.push('not an agent tool');
159
159
  const missing = need.capabilities.filter((c) => !row.capabilities.includes(c));
160
160
  // A harness brings its own tools; the capability list of a bridge agent is the host's guess.
161
161
  if (missing.length && !(row.engine.kind === 'harness' && missing.every((c) => c === 'tools'))) why.push(`lacks ${missing.join(', ')}`);
@@ -222,7 +222,7 @@ export function routeFor(agent, job, { rows = [], summary = null, need = null, r
222
222
  const policy = spec.policy || {};
223
223
  const rejected = [];
224
224
  const cleared = list.filter((r) => { const why = meets(r, n, policy); if (why.length) rejected.push(`${r.key}: ${why.join('; ')}`); return !why.length; });
225
- if (!cleared.length) return none([`no engine clears ${n.harness ? 'a harness with ' : ''}${n.capabilities.join(', ') || 'the requirements'}${n.reach !== 'any' ? ` within reach ${n.reach}` : ''}${policy.prefer ? ` under ${policy.prefer}` : ''}`, ...rejected.slice(0, 4)]);
225
+ if (!cleared.length) return none([`no engine clears ${n.harness ? 'an agent tool with ' : ''}${n.capabilities.join(', ') || 'the requirements'}${n.reach !== 'any' ? ` within reach ${n.reach}` : ''}${policy.prefer ? ` under ${policy.prefer}` : ''}`, ...rejected.slice(0, 4)]);
226
226
  const ordered = orderByPolicy(cleared, policy.prefer || 'balanced', summary);
227
227
  let pick = ordered[0];
228
228
  let exploration = false;
package/team-board.js CHANGED
@@ -104,6 +104,7 @@ export function foldBoard(state, ev) {
104
104
  if (type === 'board.thread' && p.thread?.id) {
105
105
  if (!s.threads.some((t) => t.id === p.thread.id)) s.threads.push({ ...p.thread });
106
106
  } else if (type === 'board.post' && p.post?.id) {
107
+ if (Array.isArray(s.removed) && s.removed.includes(p.post.threadId)) return s;
107
108
  if (!s.posts.some((x) => x.id === p.post.id)) s.posts.push({ ...p.post });
108
109
  const t = s.threads.find((x) => x.id === p.post.threadId);
109
110
  if (t) { t.lastAt = p.post.at; t.lastBy = p.post.by; t.posts = (t.posts || 0) + 1; }
@@ -113,6 +114,13 @@ export function foldBoard(state, ev) {
113
114
  } else if (type === 'board.thread-status' && p.threadId) {
114
115
  const t = s.threads.find((x) => x.id === p.threadId);
115
116
  if (t) { t.status = p.status; if (p.status !== 'waiting') t.waitingOn = null; if (p.holder) t.holder = p.holder; }
117
+ } else if (type === 'board.thread-removed' && p.threadId) {
118
+ // A person took the thread off the board: it and its posts go, and its id is kept so a
119
+ // post a member still makes in it (its runner did not see the removal) lands nowhere.
120
+ s.threads = s.threads.filter((x) => x.id !== p.threadId);
121
+ s.posts = s.posts.filter((x) => x.threadId !== p.threadId);
122
+ if (!Array.isArray(s.removed)) s.removed = [];
123
+ if (!s.removed.includes(p.threadId)) s.removed.push(p.threadId);
116
124
  }
117
125
  return s;
118
126
  }
@@ -123,7 +131,7 @@ export function foldBoard(state, ev) {
123
131
  * stays: a finding is a post of kind `finding` in its task's thread.
124
132
  */
125
133
  export function createBoard({ now = () => Date.now(), newId = null, state = null, onEvent = null } = {}) {
126
- const st = state && Array.isArray(state.threads) ? { threads: state.threads.map((t) => ({ ...t })), posts: (state.posts || []).map((x) => ({ ...x })) } : emptyBoardState();
134
+ const st = state && Array.isArray(state.threads) ? { threads: state.threads.map((t) => ({ ...t })), posts: (state.posts || []).map((x) => ({ ...x })), ...(Array.isArray(state.removed) && state.removed.length ? { removed: [...state.removed] } : {}) } : emptyBoardState();
127
135
  const listeners = new Set();
128
136
  let seq = st.posts.length + st.threads.length;
129
137
  const mk = (prefix) => (newId ? newId(prefix) : `${prefix}_${(++seq).toString(36)}${Math.random().toString(36).slice(2, 6)}`);
@@ -181,6 +189,20 @@ export function createBoard({ now = () => Date.now(), newId = null, state = null
181
189
  say('board.thread-status', { threadId, status, ...extra });
182
190
  return t;
183
191
  },
192
+ /**
193
+ * A person takes a thread off the board (from either client — the store appends the
194
+ * event, the running client's tail applies it here). Its posts go with it; a member's
195
+ * later post in it lands nowhere. A waiting ask is not removed: answer it or stop the run.
196
+ */
197
+ removeThread(threadId, { by = PERSON } = {}) {
198
+ const t = threadOf(threadId);
199
+ if (!t || (t.kind === 'ask' && t.status === 'waiting')) return null;
200
+ st.threads = st.threads.filter((x) => x.id !== threadId);
201
+ st.posts = st.posts.filter((x) => x.threadId !== threadId);
202
+ (st.removed ||= []).push(threadId);
203
+ say('board.thread-removed', { threadId, by });
204
+ return t;
205
+ },
184
206
  /**
185
207
  * A member is stuck: open an ask thread (status `waiting`) with the question as its
186
208
  * first post. The runner waits on it; a person answers from either client.
@@ -208,7 +230,7 @@ export function createBoard({ now = () => Date.now(), newId = null, state = null
208
230
  /** Every ask still waiting — what a client pins at the top. */
209
231
  waiting: () => st.threads.filter((t) => t.kind === 'ask' && t.status === 'waiting').map((t) => ({ ...t })),
210
232
  /** The whole board, for the run record and for a resume. */
211
- state: () => ({ threads: st.threads.map((t) => ({ ...t })), posts: st.posts.map((x) => ({ ...x })) }),
233
+ state: () => ({ threads: st.threads.map((t) => ({ ...t })), posts: st.posts.map((x) => ({ ...x })), ...(st.removed?.length ? { removed: [...st.removed] } : {}) }),
212
234
 
213
235
  // ── findings, as before: a finding is a post in its task's thread ──
214
236
  add(list, { by = null } = {}) {
package/team-record.js CHANGED
@@ -99,7 +99,7 @@ export function foldRun(run, ev) {
99
99
  if (p.checkpoint) run.checkpoint = p.checkpoint;
100
100
  break;
101
101
  case 'run.stop-requested': run.stopRequested = at; break;
102
- case 'board.thread': case 'board.post': case 'board.decision': case 'board.thread-status':
102
+ case 'board.thread': case 'board.post': case 'board.decision': case 'board.thread-status': case 'board.thread-removed':
103
103
  run.threads = foldBoard(run.threads || emptyBoardState(), ev);
104
104
  if (type === 'board.thread-status' && p.status !== 'waiting' && run.status === 'waiting' && !(run.threads.threads || []).some((t) => t.kind === 'ask' && t.status === 'waiting')) run.status = 'answered';
105
105
  break;
package/team-trail.js CHANGED
@@ -16,6 +16,7 @@ export function teamLine(ev) {
16
16
  case 'run.resumed': return { type: 'status', text: `team ${ev.team} resumed${(ev.carried || []).length ? ` (${ev.carried.length} task${ev.carried.length === 1 ? '' : 's'} carried over)` : ''}` };
17
17
  case 'board.post': return ev.post && ev.post.kind !== 'finding' ? { type: 'status', text: `${ev.post.by} ${ev.post.replyTo ? 'replied' : 'posted'} (${ev.post.kind}): ${String(ev.post.text || '').slice(0, 120)}` } : null;
18
18
  case 'board.decision': return { type: 'status', text: `${ev.by || 'someone'} ${ev.status} a post` };
19
+ case 'board.thread-removed': return { type: 'status', text: `${ev.by || 'someone'} removed a thread from the board` };
19
20
  case 'task.note': return { type: 'status', text: `${role}: ${ev.text}` };
20
21
  case 'task.handoff': return { type: 'status', text: `${role} handed off ${ev.from ? `from ${ev.from} ` : ''}to ${ev.to} by ${ev.by || 'person'}${ev.reason ? ` — ${ev.reason}` : ''}` };
21
22
  case 'task.step': return null;
@@ -63,6 +64,7 @@ export function teamLanes(prev, ev) {
63
64
  case 'task.waiting': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], status: 'waiting', waitingOn: ev.threadId }; lanes.waiting = [...(lanes.waiting || []), ev.threadId]; break;
64
65
  case 'task.done': case 'task.failed': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], status: ev.status || 'ok', ms: ev.ms }; break;
65
66
  case 'board.thread-status': if (ev.status !== 'waiting' && lanes.waiting) lanes.waiting = lanes.waiting.filter((x) => x !== ev.threadId); break;
67
+ case 'board.thread-removed': if (lanes.waiting) lanes.waiting = lanes.waiting.filter((x) => x !== ev.threadId); break;
66
68
  case 'run.waiting': lanes.waiting = [...(lanes.waiting || []), ev.threadId]; break;
67
69
  case 'run.done': lanes.status = ev.status; lanes.usage = ev.usage; break;
68
70
  default: break;