@eventmodelers/cli 1.0.71 → 1.0.72

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/README.md CHANGED
@@ -272,9 +272,12 @@ the turn's own instructions rather than being enforced from outside — the `cla
272
272
  what spawns the agents — so it's a budget the agent is told to keep, not a hard ceiling.
273
273
 
274
274
  Every event that arrives is remembered until a turn carries it — including events that land
275
- while a turn is running. Its own writes come back on that same channel and the platform can't
276
- tell them apart from a human's, so those are *labelled* for the agent rather than discarded,
277
- and the lane is damped on timing instead: it waits for a quiet period (but not forever), waits
275
+ while a turn is running. Its own writes come back on that same channel, and each event says who
276
+ made it (`agent_id` from the writer's `x-agent-id` header, `user_id` for a browser), so the
277
+ agent's own echo is recognized exactly: a burst that is nothing but its own writes is dropped
278
+ without spending a turn, and one that is mixed lists its own lines marked as such. Only a write
279
+ that reached the platform with neither id falls back to the echo window's guess and is labelled
280
+ "possibly your own". The lane is damped on timing on top of that: it waits for a quiet period (but not forever), waits
278
281
  out the echo window of its last turn, never fires twice in quick succession, and widens that
279
282
  floor each time it answers `NOOP`, so a finished board goes quiet by itself. Override the
280
283
  windows if the defaults don't suit your board:
@@ -283,7 +286,7 @@ windows if the defaults don't suit your board:
283
286
  |---|---|---|
284
287
  | `EVENTMODELERS_STANDALONE_DEBOUNCE_MS` | `8000` | quiet period before buffered board changes turn into a turn |
285
288
  | `EVENTMODELERS_STANDALONE_MAX_WAIT_MS` | `90000` | cap on that quiet period, so a board being edited continuously still gets a turn |
286
- | `EVENTMODELERS_STANDALONE_ECHO_WINDOW_MS` | `20000` | after a turn, how long incoming changes are labelled as probably the agent's own echo |
289
+ | `EVENTMODELERS_STANDALONE_ECHO_WINDOW_MS` | `20000` | after a turn, how long an *unattributed* incoming change is labelled as probably the agent's own echo (attributed ones are identified outright) |
287
290
  | `EVENTMODELERS_STANDALONE_MIN_INTERVAL_MS` | `60000` | floor between two self-directed turns |
288
291
  | `EVENTMODELERS_STANDALONE_BACKOFF_CAP_MS` | `900000` | ceiling that floor doubles up to while turns keep answering `NOOP` |
289
292
  | `EVENTMODELERS_STANDALONE_IDLE_MS` | `900000` | with nothing happening at all, how long before the agent reviews the model anyway (`0` disables it) |
package/cli.js CHANGED
@@ -1310,7 +1310,15 @@ function configureHooks({ hooksSrc, targetDir }) {
1310
1310
  // per task, or a long-lived warm process) must call this first — a `.mcp.json`
1311
1311
  // written mid-session by the process itself is too late for that same process.
1312
1312
  // The token itself is never written to disk here — `${EVENTMODELERS_TOKEN}` is
1313
- // resolved by `claude` from its own process env, which the caller must set.
1313
+ // resolved by `claude` from its own process env, which the caller must set. Same for
1314
+ // `${EVENTMODELERS_AGENT_ID}`: without it on the transport, every MCP write this agent makes
1315
+ // reaches the platform unattributed (board_events.agent_id null), and the board shows one
1316
+ // anonymous robot for it instead of this agent's name. It belongs here rather than only in the
1317
+ // skill's Step 3.5 because this entry is rewritten on every run — it would otherwise overwrite
1318
+ // the header the skill just added — and because a `.mcp.json` fixed mid-session comes too late
1319
+ // for the `claude` process already running. An unset var arrives at the server as the literal
1320
+ // `${EVENTMODELERS_AGENT_ID}` text, which it drops (it only accepts a uuid), so the entry is the
1321
+ // same one for a human's session and an agent's.
1314
1322
  function ensureMcpRegistered(projectDir, baseUrl) {
1315
1323
  const mcpConfigPath = join(projectDir, '.mcp.json');
1316
1324
  const mcpConfig = readJsonSafe(mcpConfigPath);
@@ -1318,7 +1326,7 @@ function ensureMcpRegistered(projectDir, baseUrl) {
1318
1326
  mcpConfig.mcpServers.eventmodelers = {
1319
1327
  type: 'http',
1320
1328
  url: `${baseUrl}/mcp`,
1321
- headers: { 'x-token': '${EVENTMODELERS_TOKEN}' },
1329
+ headers: { 'x-token': '${EVENTMODELERS_TOKEN}', 'x-agent-id': '${EVENTMODELERS_AGENT_ID}' },
1322
1330
  };
1323
1331
  writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
1324
1332
  }
@@ -2044,20 +2052,22 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
2044
2052
  // later adds to these payloads lands here without a client change.
2045
2053
  const BOARD_CHANGE_EVENTS = ['node:created', 'node:changed', 'node:deleted', 'edge:added', 'edge:removed', 'board:cleared'];
2046
2054
 
2047
- // Four knobs, because a board event can't tell you who caused it: the platform
2048
- // attributes an API token's writes to the org owner's user_id, so on this channel the
2049
- // agent's own edits are indistinguishable from the human's. They all govern *when* a
2050
- // self-directed turn fires — never whether an event is remembered. Everything that
2051
- // arrives is buffered (see onBoardEvent): an event seen while a turn runs, or inside the
2052
- // echo window, is only *marked* as possibly the agent's own write, so a burst that
2055
+ // Four knobs. They govern *when* a self-directed turn fires never whether an event is
2056
+ // remembered: everything that arrives is buffered (see onBoardEvent), so a burst that
2053
2057
  // straddles a turn boundary still reaches the next turn instead of being thrown away and
2054
2058
  // leaving the agent looking at whichever single event happened to land last.
2059
+ // Who wrote an event is *read off the event*, not inferred from these windows: the payload
2060
+ // carries `agent_id` (stamped from the writer's `x-agent-id` header) and `user_id` (a
2061
+ // browser session), so this agent's own echo is identified exactly and dropped without
2062
+ // costing a turn. The windows below only cover the one case attribution can't: a write that
2063
+ // carries neither id.
2055
2064
  // DEBOUNCE — one gesture (place a node, drag a column) fans out into several
2056
2065
  // events; wait for the board to fall quiet, then send a single turn.
2057
2066
  // MAX_WAIT — cap on that quiet period: a board someone keeps editing never falls
2058
2067
  // quiet, and the debounce alone would slide forever.
2059
- // ECHO_WINDOW — how long after a turn its own writes are expected back; changes in
2060
- // that window are labelled, and the next turn waits it out.
2068
+ // ECHO_WINDOW — how long after a turn its own writes are expected back; an
2069
+ // *unattributed* change in that window is labelled a possible echo, and
2070
+ // the next turn waits it out so a write and its echo don't each get one.
2061
2071
  // MIN_INTERVAL — a floor between self-directed turns, so a mistake upstream can't
2062
2072
  // become a self-feeding loop burning tokens unattended. Doubles per
2063
2073
  // consecutive NOOP up to BACKOFF_CAP, and resets as soon as a turn
@@ -2077,10 +2087,11 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
2077
2087
  const STANDALONE_BACKOFF_CAP_MS = envMs('EVENTMODELERS_STANDALONE_BACKOFF_CAP_MS', 15 * 60_000);
2078
2088
  const STANDALONE_IDLE_MS = envMs('EVENTMODELERS_STANDALONE_IDLE_MS', 15 * 60_000);
2079
2089
 
2080
- // node_id (or '(board)') -> { types: Set<string>, count: number, maybeOwn: boolean }
2081
- // for everything seen since the last self-directed turn. `maybeOwn` stays true only
2082
- // while every event for that node arrived while a turn was running or inside the echo
2083
- // window one event from outside that window and the node is a real change again.
2090
+ // node_id (or '(board)') -> { types: Set<string>, count, own, other, maybe } for
2091
+ // everything seen since the last self-directed turn, each event counted into exactly one
2092
+ // origin bucket: `own` = attributed to this agent's own id, `other` = attributed to a human
2093
+ // or another agent, `maybe` = carries no attribution at all and landed inside the echo
2094
+ // window, so it *might* be this agent's. Only `maybe` is a guess.
2084
2095
  const observed = new Map();
2085
2096
  let observedCount = 0;
2086
2097
  let seqLo = null;
@@ -2097,6 +2108,16 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
2097
2108
  return Math.min(STANDALONE_MIN_INTERVAL_MS * 2 ** noopStreak, STANDALONE_BACKOFF_CAP_MS);
2098
2109
  }
2099
2110
 
2111
+ // Empties the buffer — every field of it, which is why it is one function and not five
2112
+ // lines repeated at each place a burst stops being pending.
2113
+ function resetObserved() {
2114
+ observed.clear();
2115
+ observedCount = 0;
2116
+ seqLo = null;
2117
+ seqHi = null;
2118
+ firstObservedAt = 0;
2119
+ }
2120
+
2100
2121
  function onBoardEvent(type, payload) {
2101
2122
  if (!standalone) {
2102
2123
  if (verbose) log(`board event ${type} dropped — not running with --standalone`);
@@ -2107,12 +2128,26 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
2107
2128
  // The warm-up turn is read-only, so a change that lands while it runs is somebody
2108
2129
  // else's — labelling it "possibly your own write" would only teach the agent to
2109
2130
  // discount the very edits it just came up to work on.
2110
- const maybeOwn = (!!pending && !warmingUp) || draining || inEchoWindow;
2131
+ const guessOwn = (!!pending && !warmingUp) || draining || inEchoWindow;
2132
+ // Attribution beats the clock in both directions. `agent_id === ours` is this agent's own
2133
+ // write, certainly, whenever it comes back. Any *other* id — a person's user_id, another
2134
+ // agent's — is certainly not ours, which is the half the timer used to get wrong: a human
2135
+ // editing while this agent worked had their change written off as an echo of it.
2136
+ const writerAgent = payload?.agent_id || null;
2137
+ const writerUser = payload?.user_id || null;
2138
+ const origin =
2139
+ writerAgent && cfg.agentId && writerAgent === cfg.agentId
2140
+ ? 'own'
2141
+ : writerAgent || writerUser
2142
+ ? 'other'
2143
+ : guessOwn
2144
+ ? 'maybe'
2145
+ : 'other';
2111
2146
  const nodeId = payload?.node_id ?? '(board)';
2112
- const entry = observed.get(nodeId) ?? { types: new Set(), count: 0, maybeOwn };
2147
+ const entry = observed.get(nodeId) ?? { types: new Set(), count: 0, own: 0, other: 0, maybe: 0 };
2113
2148
  entry.types.add(type);
2114
2149
  entry.count += 1;
2115
- if (!maybeOwn) entry.maybeOwn = false;
2150
+ entry[origin] += 1;
2116
2151
  observed.set(nodeId, entry);
2117
2152
  observedCount += 1;
2118
2153
  if (!firstObservedAt) firstObservedAt = Date.now();
@@ -2121,7 +2156,17 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
2121
2156
  if (seqLo === null || seq < seqLo) seqLo = seq;
2122
2157
  if (seqHi === null || seq > seqHi) seqHi = seq;
2123
2158
  }
2124
- log(`board change: ${type} node=${nodeId}${Number.isFinite(seq) ? ` seq=${seq}` : ''}${maybeOwn ? ' (maybe own write)' : ''}`);
2159
+ const writtenBy =
2160
+ origin === 'own'
2161
+ ? ' — own write'
2162
+ : origin === 'maybe'
2163
+ ? ' — unattributed, maybe own write'
2164
+ : writerUser
2165
+ ? ' — by a person'
2166
+ : writerAgent
2167
+ ? ` — by agent ${writerAgent.slice(0, 8)}`
2168
+ : '';
2169
+ log(`board change: ${type} node=${nodeId}${Number.isFinite(seq) ? ` seq=${seq}` : ''}${writtenBy}`);
2125
2170
  armStandaloneTurn(nextDelayMs());
2126
2171
  }
2127
2172
 
@@ -2186,10 +2231,17 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
2186
2231
  'nothing, change nothing and reply <promise>NOOP</promise>.';
2187
2232
 
2188
2233
  function buildStandaloneTurn() {
2189
- const lines = [...observed.entries()].map(
2190
- ([nodeId, entry]) =>
2191
- `- ${nodeId}: ${[...entry.types].join(', ')} (${entry.count}×)${entry.maybeOwn ? ' — possibly your own earlier write' : ''}`,
2192
- );
2234
+ const lines = [...observed.entries()].map(([nodeId, entry]) => {
2235
+ const origin =
2236
+ entry.own === entry.count
2237
+ ? ' — YOUR OWN earlier write, echoed back'
2238
+ : entry.own
2239
+ ? ` — ${entry.own} of ${entry.count} are YOUR OWN earlier writes, the rest are not`
2240
+ : entry.maybe === entry.count
2241
+ ? ' — unattributed, possibly your own earlier write'
2242
+ : '';
2243
+ return `- ${nodeId}: ${[...entry.types].join(', ')} (${entry.count}×)${origin}`;
2244
+ });
2193
2245
  const header = [
2194
2246
  'BOARD_CHANGE',
2195
2247
  `board_id=${cfg.boardId}`,
@@ -2238,6 +2290,15 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
2238
2290
 
2239
2291
  async function dispatchStandaloneTurn({ idle = false } = {}) {
2240
2292
  if (!idle && !observed.size) return;
2293
+ // Every buffered event is provably this agent's own write coming back — attribution says
2294
+ // so, not a timer. There is nothing new on the board, so it costs no turn; if the board
2295
+ // then stays quiet the idle review still comes around.
2296
+ if (!idle && [...observed.values()].every((entry) => entry.own === entry.count)) {
2297
+ log(`standalone turn skipped: ${observedCount} board event(s) on ${observed.size} node(s), all own writes`);
2298
+ resetObserved();
2299
+ armIdleReview();
2300
+ return;
2301
+ }
2241
2302
  // A direct message always outranks the agent's own initiative — re-arm instead of
2242
2303
  // queueing behind the prompt lane, so the buffer just keeps collecting meanwhile.
2243
2304
  if (pending || draining) {
@@ -2255,17 +2316,20 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
2255
2316
  if (idle) {
2256
2317
  log(`standalone review turn: board quiet for ${Math.round(STANDALONE_IDLE_MS / 1000)}s`);
2257
2318
  } else {
2258
- const ownOnly = [...observed.values()].every((entry) => entry.maybeOwn);
2319
+ const totals = [...observed.values()].reduce(
2320
+ (acc, entry) => ({ own: acc.own + entry.own, maybe: acc.maybe + entry.maybe }),
2321
+ { own: 0, maybe: 0 },
2322
+ );
2323
+ const breakdown = [
2324
+ totals.own ? `${totals.own} own` : null,
2325
+ totals.maybe ? `${totals.maybe} unattributed` : null,
2326
+ ].filter(Boolean);
2259
2327
  log(
2260
2328
  `standalone turn: ${observedCount} board event(s) on ${observed.size} node(s)` +
2261
- `${ownOnly ? ' (all possibly own writes)' : ''}`,
2329
+ `${breakdown.length ? ` (${breakdown.join(', ')})` : ''}`,
2262
2330
  );
2263
2331
  }
2264
- observed.clear();
2265
- observedCount = 0;
2266
- seqLo = null;
2267
- seqHi = null;
2268
- firstObservedAt = 0;
2332
+ resetObserved();
2269
2333
  lastStandaloneAt = Date.now();
2270
2334
  try {
2271
2335
  const result = await runClaudeWarm(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.71",
3
+ "version": "1.0.72",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -881,7 +881,7 @@ Set a prompt's status, optionally attaching a progress comment. Auth: `x-token`
881
881
  ```
882
882
 
883
883
  **Response**: `200` — the updated row
884
- **Errors**: `400` invalid/missing `status` · `403` token not for this prompt's org · `404` prompt not found
884
+ **Errors**: `400` invalid/missing `status` · `403` token not for this prompt's org · `404` prompt not found · `409` (`PROMPT_STATUS_ALREADY_SET`) the prompt already carries that status — another agent already made this transition, so don't retry it
885
885
 
886
886
  ---
887
887
 
@@ -52,6 +52,7 @@ Omit `comment` entirely when there isn't one — don't pass an empty string.
52
52
  |----------|---------|--------|
53
53
  | `400` | `status` missing or not a valid value | Fix the value and retry — do not retry with the same bad value. |
54
54
  | `404` | Prompt not found | The prompt may have been deleted by its author while you were working. Report this and move on — do not treat it as a failure of your actual task work. |
55
+ | `409` | The prompt already carries the status you asked for (`PROMPT_STATUS_ALREADY_SET`) | Somebody else — usually another agent working the same queue — already made this transition. Do not retry it; treat the transition as done and carry on. |
55
56
  | `401`/`403` | Token invalid or wrong organization | Re-run `connect` to refresh credentials, then retry once. |
56
57
 
57
58
  ---
@@ -199,6 +199,9 @@ Steps:
199
199
 
200
200
  Keep these turns finished within the turn: wait for the subagents you dispatched, don't leave
201
201
  work trailing. Everything you and they write to the board comes back on this same channel as
202
- another change; the CLI labels changes that arrive in that echo window rather than dropping
203
- them, so you'll see your own writes listed on a later turn recognize them and don't rework
204
- them.
202
+ another change. A burst that is *only* your own writes never becomes a turn at all, so a
203
+ change list you are handed always contains something that isn't yoursbut it may still list
204
+ yours alongside it, marked `YOUR OWN earlier write`. Take that mark literally: those lines are
205
+ there for context, not to be reworked. A line marked `unattributed, possibly your own earlier
206
+ write` is the one uncertain case (a write that reached the platform without an agent id);
207
+ anything unmarked was written by someone else and is real work to look at.