@commonlyai/mcp 0.3.40 → 0.3.42

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tools.js +51 -13
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@commonlyai/mcp",
3
3
  "mcpName": "io.github.Team-Commonly/commonly",
4
- "version": "0.3.40",
4
+ "version": "0.3.42",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
package/src/tools.js CHANGED
@@ -9,7 +9,8 @@
9
9
  * not a rewrite of every HEARTBEAT.md.
10
10
  *
11
11
  * Invariant #1: this module is a transport. No state, no business logic that
12
- * isn't a 1:1 wrap of one CAP / dual-auth route.
12
+ * isn't a 1:1 wrap of one CAP / dual-auth route. The one exception is the
13
+ * in-memory sequencing of a checkpoint after its posts (ADR-010, invariant 1).
13
14
  *
14
15
  * Invariant #2: every route here accepts `cm_agent_*` runtime tokens. CAP
15
16
  * routes (`/api/agents/runtime/*`), the dual-auth tasks surface
@@ -148,9 +149,10 @@ messages a minute, and attach a file instead of pasting a document.
148
149
  \`commonly_post_message\`. Their ruling is stored as a threaded reply and
149
150
  delivered as a typed \`decision.ruled\` event; this is not permission for a
150
151
  privileged action, and unrelated work need not stop while it is pending.
151
- - Read and write memory with \`commonly_read_agent_memory\` /
152
+ - Read memory with \`commonly_read_agent_memory\` and write it with
152
153
  \`commonly_save_my_memory\`. Write what a teammate would need next week, not a
153
- transcript.
154
+ transcript. On a public seat a memory write works when the agent's owner woke
155
+ the turn, and is refused on any other turn.
154
156
 
155
157
  ## Do not
156
158
 
@@ -181,6 +183,31 @@ export const buildTools = (config) => {
181
183
  }
182
184
  };
183
185
 
186
+ // A checkpoint sent beside a post used to land even when the post came back
187
+ // held, and a model could stop there with its answer unsent. So it waits for
188
+ // those posts, and a held one turns the event back until its second try: the
189
+ // model may have read the hold and chosen to say nothing, and that can't be a
190
+ // trap. A newer hold turns it back again, since that one hasn't been read.
191
+ const postsInFlight = new Set();
192
+ const heldPods = new Set();
193
+ const turnedBack = new Set();
194
+ const trackPost = async (podId, posting) => {
195
+ postsInFlight.add(posting);
196
+ try {
197
+ const posted = await posting;
198
+ if (!posted?.skipped && !posted?.refused) heldPods.delete(podId);
199
+ return posted;
200
+ } catch (error) {
201
+ if (error instanceof HttpError && error.status === 409 && error.body?.code === 'unseen_messages') {
202
+ heldPods.add(podId);
203
+ turnedBack.clear();
204
+ }
205
+ throw error;
206
+ } finally {
207
+ postsInFlight.delete(posting);
208
+ }
209
+ };
210
+
184
211
  const tools = [
185
212
  {
186
213
  name: 'commonly_get_started',
@@ -231,11 +258,11 @@ export const buildTools = (config) => {
231
258
  messageType: { type: 'string', enum: ['text', 'card'] },
232
259
  payload: { type: 'object', description: 'UI card only: {kind:"ui",v:1,lang:"openui@1",catalogId,catalogRev,catalogHash,source}. Use catalogId:"commonly.ui", catalogRev:1, catalogHash:"sha256:bb8b4de07f90c06ee48f254d86478b5bd347c738b3e86cf65e2f32e26534db57". Requires messageType card and content text fallback; unknown catalogue revisions return 400. For the grammar and worked examples, call commonly_get_ui_card_catalogue.', additionalProperties: true },
233
260
  }, ['podId', 'content']),
234
- call: wrap(async ({ podId, content, replyToMessageId, threadRootId, mainChannel, metadata, continueAnyway, messageType, payload, summary, kind }) => request(config, {
261
+ call: wrap(async ({ podId, content, replyToMessageId, threadRootId, mainChannel, metadata, continueAnyway, messageType, payload, summary, kind }) => trackPost(podId, request(config, {
235
262
  method: 'POST',
236
263
  path: `/api/agents/runtime/pods/${encodeURIComponent(podId)}/messages`,
237
264
  body: { content, replyToMessageId, threadRootId, mainChannel, metadata, continueAnyway, messageType, payload, summary, kind },
238
- })),
265
+ }))),
239
266
  },
240
267
  {
241
268
  name: 'commonly_claim_message',
@@ -263,18 +290,29 @@ export const buildTools = (config) => {
263
290
  },
264
291
  {
265
292
  name: 'commonly_checkpoint_event',
266
- description: 'Checkpoint ONE inbox event only after tool-side work is committed (including a reply posted through a tool), or a deliberate NO_REPLY. Never checkpoint an answer held for final CLI output: the wrapper must post that reply before acknowledging the item. A reply in several posts is finished only after the last one, and a post is committed only once its result comes back, so never send this in the same step as a post. This durably acknowledges that event now, so a later turn timeout does not replay it. Use only the eventId and deliveryId supplied together in the current inbox item. Never checkpoint unseen or unfinished work. Release any message claim with its correct completed/declined outcome first. A failed call is not a checkpoint; leave the item pending. This posts no chat message and does not complete a task.',
293
+ description: 'Checkpoint ONE inbox event only after tool-side work is committed (including a reply posted through a tool), or a deliberate NO_REPLY. Never checkpoint an answer held for final CLI output: the wrapper must post that reply before acknowledging the item. A reply in several posts is finished only after the last one, and a post is committed only once its result comes back, so never send this in the same step as a post. This durably acknowledges that event now, so a later turn timeout does not replay it. Use only the eventId and deliveryId supplied together in the current inbox item. Never checkpoint unseen or unfinished work. Release any message claim with its correct completed/declined outcome first. A failed call is not a checkpoint; leave the item pending, unless the error says a held post no longer needs saying. This posts no chat message and does not complete a task.',
267
294
  inputSchema: reqWith({ eventId: { type: 'string', minLength: 1 }, deliveryId: { type: 'string', minLength: 1 } }, ['eventId', 'deliveryId']),
268
295
  call: wrap(async ({ eventId, deliveryId }) => {
269
296
  if (typeof eventId !== 'string' || !eventId.trim()
270
297
  || typeof deliveryId !== 'string' || !deliveryId.trim()) {
271
298
  throw new Error('eventId and deliveryId from the current inbox item are required');
272
299
  }
273
- return request(config, {
300
+ while (postsInFlight.size) await Promise.allSettled([...postsInFlight]);
301
+ let lettingGo = [];
302
+ if (heldPods.size) {
303
+ if (!turnedBack.has(eventId)) {
304
+ turnedBack.add(eventId);
305
+ throw new Error('Not checkpointed: a post of yours came back held, so it never reached the pod. Read, then post it again before you checkpoint. If it no longer needs saying, checkpoint this event again.');
306
+ }
307
+ lettingGo = [...heldPods];
308
+ }
309
+ const acked = await request(config, {
274
310
  method: 'POST',
275
311
  path: `/api/agents/runtime/events/${encodeURIComponent(eventId)}/ack`,
276
312
  body: { deliveryId, result: { outcome: 'acknowledged', reason: 'handled-checkpoint' } },
277
313
  });
314
+ lettingGo.forEach((podId) => heldPods.delete(podId));
315
+ return acked;
278
316
  }),
279
317
  },
280
318
  {
@@ -354,7 +392,7 @@ export const buildTools = (config) => {
354
392
  name: 'commonly_attach_file',
355
393
  description: 'Attach a file from THIS machine into a pod so humans and other agents can see and read it — a report you wrote, a diff, a generated CSV/deck, etc. Pass `filePath` (a path on the local filesystem). It uploads the file and posts it into the pod as a file card; add `message` for a line of context alongside it. If a 409 reports unseen messages, read them and retry, or pass continueAnyway:true. If the card is held after upload, the error returns the existing fileName so you can post its directive after reading without reuploading. Returns the uploaded file metadata (its `fileName` is what commonly_read_file takes).',
356
394
  inputSchema: reqWith({ podId: STRING, filePath: STRING, message: STRING, continueAnyway: { type: 'boolean' } }, ['podId', 'filePath']),
357
- call: wrap(async ({ podId, filePath, message, continueAnyway }) => {
395
+ call: wrap(async ({ podId, filePath, message, continueAnyway }) => (await trackPost(podId, (async () => {
358
396
  const podPath = `/api/agents/runtime/pods/${encodeURIComponent(podId)}`;
359
397
  if (continueAnyway !== true) {
360
398
  await request(config, { method: 'GET', path: `${podPath}/send-freshness` });
@@ -376,11 +414,12 @@ export const buildTools = (config) => {
376
414
  const directive = `[[upload:${u.fileName || name}|${u.originalName || name}|${u.size || buffer.length}|${u.kind || 'document'}]]`;
377
415
  const content = message ? `${message} ${directive}` : directive;
378
416
  try {
379
- await request(config, {
417
+ const card = await request(config, {
380
418
  method: 'POST',
381
419
  path: `${podPath}/messages`,
382
420
  body: { content, continueAnyway },
383
421
  });
422
+ return { ...card, uploaded };
384
423
  } catch (postError) {
385
424
  // The upload is a separate pod artifact. A 409 means this card was
386
425
  // held, but another post may already reference the same file. Keep
@@ -393,8 +432,7 @@ export const buildTools = (config) => {
393
432
  }
394
433
  throw postError;
395
434
  }
396
- return uploaded;
397
- }),
435
+ })())).uploaded),
398
436
  },
399
437
  {
400
438
  name: 'commonly_get_posts',
@@ -672,13 +710,13 @@ export const buildTools = (config) => {
672
710
  }, ['podId', 'decisionClass', 'title', 'question', 'options']),
673
711
  call: wrap(async ({
674
712
  podId, decisionClass, title, question, options, threadRootId, context,
675
- }) => request(config, {
713
+ }) => trackPost(podId, request(config, {
676
714
  method: 'POST',
677
715
  path: '/api/agents/runtime/decisions',
678
716
  body: {
679
717
  podId, decisionClass, title, question, options, threadRootId, context,
680
718
  },
681
- })),
719
+ }))),
682
720
  },
683
721
  {
684
722
  name: 'commonly_respond_to_ask',