agenthub-multiagent-mcp 1.60.0 → 1.64.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.
Files changed (123) hide show
  1. package/README.md +3 -3
  2. package/dist/brain/memvidBrain.d.ts +22 -0
  3. package/dist/brain/memvidBrain.d.ts.map +1 -1
  4. package/dist/brain/memvidBrain.js +41 -2
  5. package/dist/brain/memvidBrain.js.map +1 -1
  6. package/dist/brain/memvidBrain.test.js +90 -0
  7. package/dist/brain/memvidBrain.test.js.map +1 -1
  8. package/dist/brain/migrate.d.ts +43 -0
  9. package/dist/brain/migrate.d.ts.map +1 -1
  10. package/dist/brain/migrate.js +148 -23
  11. package/dist/brain/migrate.js.map +1 -1
  12. package/dist/brain/migrate.test.js +194 -1
  13. package/dist/brain/migrate.test.js.map +1 -1
  14. package/dist/channel.js +48 -6
  15. package/dist/channel.js.map +1 -1
  16. package/dist/channelDiagnostics.d.ts +53 -0
  17. package/dist/channelDiagnostics.d.ts.map +1 -1
  18. package/dist/channelDiagnostics.js +91 -0
  19. package/dist/channelDiagnostics.js.map +1 -1
  20. package/dist/channelDiagnostics.test.js +109 -1
  21. package/dist/channelDiagnostics.test.js.map +1 -1
  22. package/dist/client.d.ts +17 -0
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +32 -0
  25. package/dist/client.js.map +1 -1
  26. package/dist/daemon.js +16 -22
  27. package/dist/daemon.js.map +1 -1
  28. package/dist/index.js +36 -3
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/pending-items.d.ts +63 -0
  31. package/dist/lib/pending-items.d.ts.map +1 -0
  32. package/dist/lib/pending-items.js +96 -0
  33. package/dist/lib/pending-items.js.map +1 -0
  34. package/dist/pinSweep.d.ts +83 -0
  35. package/dist/pinSweep.d.ts.map +1 -0
  36. package/dist/pinSweep.js +105 -0
  37. package/dist/pinSweep.js.map +1 -0
  38. package/dist/pinSweep.test.d.ts +12 -0
  39. package/dist/pinSweep.test.d.ts.map +1 -0
  40. package/dist/pinSweep.test.js +110 -0
  41. package/dist/pinSweep.test.js.map +1 -0
  42. package/dist/pinSweepCli.d.ts +20 -0
  43. package/dist/pinSweepCli.d.ts.map +1 -0
  44. package/dist/pinSweepCli.js +287 -0
  45. package/dist/pinSweepCli.js.map +1 -0
  46. package/dist/run-claude.sh +36 -5
  47. package/dist/sessionCapture.d.ts.map +1 -1
  48. package/dist/sessionCapture.js +7 -1
  49. package/dist/sessionCapture.js.map +1 -1
  50. package/dist/sessionCapture.test.js +20 -0
  51. package/dist/sessionCapture.test.js.map +1 -1
  52. package/dist/setup.js +11 -0
  53. package/dist/setup.js.map +1 -1
  54. package/dist/shiftFiles.d.ts +33 -0
  55. package/dist/shiftFiles.d.ts.map +1 -0
  56. package/dist/shiftFiles.js +57 -0
  57. package/dist/shiftFiles.js.map +1 -0
  58. package/dist/shiftFiles.test.d.ts +2 -0
  59. package/dist/shiftFiles.test.d.ts.map +1 -0
  60. package/dist/shiftFiles.test.js +141 -0
  61. package/dist/shiftFiles.test.js.map +1 -0
  62. package/dist/skillInstaller.d.ts.map +1 -1
  63. package/dist/skillInstaller.gate.test.js +2 -0
  64. package/dist/skillInstaller.gate.test.js.map +1 -1
  65. package/dist/skillInstaller.js +8 -0
  66. package/dist/skillInstaller.js.map +1 -1
  67. package/dist/skillInstaller.test.js +8 -1
  68. package/dist/skillInstaller.test.js.map +1 -1
  69. package/dist/tools/goshtiRead.d.ts +72 -0
  70. package/dist/tools/goshtiRead.d.ts.map +1 -0
  71. package/dist/tools/goshtiRead.js +274 -0
  72. package/dist/tools/goshtiRead.js.map +1 -0
  73. package/dist/tools/goshtiRead.test.d.ts +8 -0
  74. package/dist/tools/goshtiRead.test.d.ts.map +1 -0
  75. package/dist/tools/goshtiRead.test.js +182 -0
  76. package/dist/tools/goshtiRead.test.js.map +1 -0
  77. package/dist/tools/inboxBounds.d.ts +80 -0
  78. package/dist/tools/inboxBounds.d.ts.map +1 -0
  79. package/dist/tools/inboxBounds.js +105 -0
  80. package/dist/tools/inboxBounds.js.map +1 -0
  81. package/dist/tools/inboxBounds.test.d.ts +15 -0
  82. package/dist/tools/inboxBounds.test.d.ts.map +1 -0
  83. package/dist/tools/inboxBounds.test.js +135 -0
  84. package/dist/tools/inboxBounds.test.js.map +1 -0
  85. package/dist/tools/index.d.ts +11 -0
  86. package/dist/tools/index.d.ts.map +1 -1
  87. package/dist/tools/index.js +79 -18
  88. package/dist/tools/index.js.map +1 -1
  89. package/dist/tools/intakeRequests.d.ts +35 -0
  90. package/dist/tools/intakeRequests.d.ts.map +1 -0
  91. package/dist/tools/intakeRequests.js +344 -0
  92. package/dist/tools/intakeRequests.js.map +1 -0
  93. package/dist/tools/intakeRequests.test.d.ts +8 -0
  94. package/dist/tools/intakeRequests.test.d.ts.map +1 -0
  95. package/dist/tools/intakeRequests.test.js +183 -0
  96. package/dist/tools/intakeRequests.test.js.map +1 -0
  97. package/dist/tools/pushedItemsSafety.test.d.ts +12 -0
  98. package/dist/tools/pushedItemsSafety.test.d.ts.map +1 -0
  99. package/dist/tools/pushedItemsSafety.test.js +179 -0
  100. package/dist/tools/pushedItemsSafety.test.js.map +1 -0
  101. package/dist/tools/tools.test.js +17 -5
  102. package/dist/tools/tools.test.js.map +1 -1
  103. package/dist/tools/wrapperCost.test.d.ts +13 -0
  104. package/dist/tools/wrapperCost.test.d.ts.map +1 -0
  105. package/dist/tools/wrapperCost.test.js +149 -0
  106. package/dist/tools/wrapperCost.test.js.map +1 -0
  107. package/dist/websocket.d.ts +30 -1
  108. package/dist/websocket.d.ts.map +1 -1
  109. package/dist/websocket.js +40 -3
  110. package/dist/websocket.js.map +1 -1
  111. package/dist/websocketQueue.test.d.ts +10 -0
  112. package/dist/websocketQueue.test.d.ts.map +1 -0
  113. package/dist/websocketQueue.test.js +78 -0
  114. package/dist/websocketQueue.test.js.map +1 -0
  115. package/package.json +4 -4
  116. package/skills/catalog.json +23 -10
  117. package/skills/commands/close-session.md +144 -144
  118. package/skills/commands/start-session.md +25 -6
  119. package/skills/manifest.json +8 -1
  120. package/skills/skills/code-brain-hook/SKILL.md +61 -61
  121. package/skills/skills/file-bug/SKILL.md +75 -75
  122. package/skills/skills/inbox/SKILL.md +99 -0
  123. package/skills/skills/multiagent-brainstorm/SKILL.md +164 -164
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Bounds for the check_inbox payload (fix-inbox-delivery-and-tool-wrapping §1).
3
+ *
4
+ * check_inbox used to pass no `limit`, so the server applied its own default of
5
+ * 50 messages with full bodies and nothing truncated anywhere in the path. One
6
+ * such call measured 115,769 characters across 787 lines, exceeded the
7
+ * tool-result token budget, and never reached the model — the harness spilled it
8
+ * to a file. From the model's seat an unusable result is indistinguishable from
9
+ * a hang, which is how it was first reported.
10
+ *
11
+ * The cap alone would not be a fix. A caller that silently receives 10 of 40
12
+ * messages has no way to know, and the loss happened precisely because a
13
+ * slash-command bulk-marked read a result nobody had actually seen. So every
14
+ * bound here is paired with a signal: `truncated`, `omitted_count`, and a
15
+ * `_truncated` marker naming the original byte length on any clipped body.
16
+ *
17
+ * The rule that follows — never bulk-ack a truncated result — belongs to the
18
+ * caller, and is why the bundled `inbox` skill is part of this change.
19
+ */
20
+ import type { Message } from "../client.js";
21
+ /** Messages returned when the caller names no limit. */
22
+ export declare const CHECK_INBOX_DEFAULT_LIMIT = 10;
23
+ /**
24
+ * Hard ceiling on messages per call, whatever the caller asks for. The server
25
+ * clamps too; this stops a client from making the request in the first place.
26
+ */
27
+ export declare const CHECK_INBOX_MAX_LIMIT = 50;
28
+ /**
29
+ * Per-message body cap. Generous enough for a normal A2A message to arrive
30
+ * whole — the bodies that blew the budget were multi-kilobyte bug reports — and
31
+ * small enough that a full page of them cannot.
32
+ */
33
+ export declare const CHECK_INBOX_BODY_CAP_BYTES = 4000;
34
+ /** A message body clipped by the cap, carrying its true length. */
35
+ export interface TruncationMarker {
36
+ original_bytes: number;
37
+ }
38
+ export interface BoundedInbox {
39
+ messages: (Message & {
40
+ _truncated?: TruncationMarker;
41
+ })[];
42
+ /** Total matching the query server-side, not the number returned. */
43
+ total: number;
44
+ returned: number;
45
+ omitted_count: number;
46
+ /** True when any message was withheld OR any body was clipped. */
47
+ truncated: boolean;
48
+ }
49
+ /**
50
+ * Resolve the limit to send to the server.
51
+ *
52
+ * A missing, non-numeric, zero or negative limit takes the default rather than
53
+ * meaning "unbounded" — "no limit" is the bug this exists to remove, and it must
54
+ * not be reachable by typo or by a caller passing 0. This mirrors
55
+ * `requestTimeoutMs`, where a malformed override also falls back rather than
56
+ * disabling the deadline.
57
+ */
58
+ export declare function resolveInboxLimit(requested?: unknown): number;
59
+ /**
60
+ * Bound an inbox response and declare what was withheld.
61
+ *
62
+ * `total` is the server's count for the query, which may exceed the rows
63
+ * returned; `omitted_count` is the difference. A clipped body also sets
64
+ * `truncated`, because a caller deciding whether it is safe to acknowledge
65
+ * these messages needs one flag that means "you did not get everything",
66
+ * not two it might check only one of.
67
+ */
68
+ export declare function boundInbox(raw: {
69
+ messages?: Message[];
70
+ total?: number;
71
+ /**
72
+ * How many matched the query server-side, independent of the page size. The
73
+ * server's `total` has always meant "how many are in this response", so it
74
+ * cannot answer "what was withheld" — `matching_total` was added for that.
75
+ * Absent against an older server, in which case the omitted count collapses
76
+ * to 0 and only body clipping can set `truncated`.
77
+ */
78
+ matching_total?: number;
79
+ }): BoundedInbox;
80
+ //# sourceMappingURL=inboxBounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inboxBounds.d.ts","sourceRoot":"","sources":["../../src/tools/inboxBounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,CAAC,OAAO,GAAG;QAAE,UAAU,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC,EAAE,CAAC;IAC1D,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,CAO7D;AAyBD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE;IAC9B,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,YAAY,CAyBf"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Bounds for the check_inbox payload (fix-inbox-delivery-and-tool-wrapping §1).
3
+ *
4
+ * check_inbox used to pass no `limit`, so the server applied its own default of
5
+ * 50 messages with full bodies and nothing truncated anywhere in the path. One
6
+ * such call measured 115,769 characters across 787 lines, exceeded the
7
+ * tool-result token budget, and never reached the model — the harness spilled it
8
+ * to a file. From the model's seat an unusable result is indistinguishable from
9
+ * a hang, which is how it was first reported.
10
+ *
11
+ * The cap alone would not be a fix. A caller that silently receives 10 of 40
12
+ * messages has no way to know, and the loss happened precisely because a
13
+ * slash-command bulk-marked read a result nobody had actually seen. So every
14
+ * bound here is paired with a signal: `truncated`, `omitted_count`, and a
15
+ * `_truncated` marker naming the original byte length on any clipped body.
16
+ *
17
+ * The rule that follows — never bulk-ack a truncated result — belongs to the
18
+ * caller, and is why the bundled `inbox` skill is part of this change.
19
+ */
20
+ /** Messages returned when the caller names no limit. */
21
+ export const CHECK_INBOX_DEFAULT_LIMIT = 10;
22
+ /**
23
+ * Hard ceiling on messages per call, whatever the caller asks for. The server
24
+ * clamps too; this stops a client from making the request in the first place.
25
+ */
26
+ export const CHECK_INBOX_MAX_LIMIT = 50;
27
+ /**
28
+ * Per-message body cap. Generous enough for a normal A2A message to arrive
29
+ * whole — the bodies that blew the budget were multi-kilobyte bug reports — and
30
+ * small enough that a full page of them cannot.
31
+ */
32
+ export const CHECK_INBOX_BODY_CAP_BYTES = 4000;
33
+ /**
34
+ * Resolve the limit to send to the server.
35
+ *
36
+ * A missing, non-numeric, zero or negative limit takes the default rather than
37
+ * meaning "unbounded" — "no limit" is the bug this exists to remove, and it must
38
+ * not be reachable by typo or by a caller passing 0. This mirrors
39
+ * `requestTimeoutMs`, where a malformed override also falls back rather than
40
+ * disabling the deadline.
41
+ */
42
+ export function resolveInboxLimit(requested) {
43
+ const n = typeof requested === "number"
44
+ ? requested
45
+ : Number.parseInt(String(requested ?? ""), 10);
46
+ if (!Number.isFinite(n) || n <= 0)
47
+ return CHECK_INBOX_DEFAULT_LIMIT;
48
+ return Math.min(Math.trunc(n), CHECK_INBOX_MAX_LIMIT);
49
+ }
50
+ /**
51
+ * Apply the body cap to one message, attaching a marker when it bites.
52
+ *
53
+ * Measured in UTF-16 code units (`String.length`), matching how the payload is
54
+ * counted downstream. `original_bytes` keeps the field name the session-event
55
+ * truncation marker already uses, so there is one truncation idiom in the
56
+ * codebase rather than two.
57
+ */
58
+ function capBody(msg) {
59
+ const body = typeof msg.body === "string" ? msg.body : "";
60
+ if (body.length <= CHECK_INBOX_BODY_CAP_BYTES)
61
+ return { msg, clipped: false };
62
+ return {
63
+ msg: {
64
+ ...msg,
65
+ body: body.slice(0, CHECK_INBOX_BODY_CAP_BYTES),
66
+ _truncated: { original_bytes: body.length },
67
+ },
68
+ clipped: true,
69
+ };
70
+ }
71
+ /**
72
+ * Bound an inbox response and declare what was withheld.
73
+ *
74
+ * `total` is the server's count for the query, which may exceed the rows
75
+ * returned; `omitted_count` is the difference. A clipped body also sets
76
+ * `truncated`, because a caller deciding whether it is safe to acknowledge
77
+ * these messages needs one flag that means "you did not get everything",
78
+ * not two it might check only one of.
79
+ */
80
+ export function boundInbox(raw) {
81
+ const messages = raw.messages ?? [];
82
+ let anyClipped = false;
83
+ const capped = messages.map((m) => {
84
+ const { msg, clipped } = capBody(m);
85
+ if (clipped)
86
+ anyClipped = true;
87
+ return msg;
88
+ });
89
+ const returned = capped.length;
90
+ // Prefer matching_total; fall back to total for an older server. Trust the
91
+ // larger of the two either way: a count below the rows actually sent is
92
+ // wrong, and a negative omitted count would read as "nothing withheld" — the
93
+ // one answer that must never be wrong in this direction.
94
+ const reported = raw.matching_total ?? raw.total ?? returned;
95
+ const total = Math.max(reported, returned);
96
+ const omitted = total - returned;
97
+ return {
98
+ messages: capped,
99
+ total,
100
+ returned,
101
+ omitted_count: omitted,
102
+ truncated: omitted > 0 || anyClipped,
103
+ };
104
+ }
105
+ //# sourceMappingURL=inboxBounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inboxBounds.js","sourceRoot":"","sources":["../../src/tools/inboxBounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,wDAAwD;AACxD,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAE5C;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAExC;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAiB/C;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAmB;IACnD,MAAM,CAAC,GACL,OAAO,SAAS,KAAK,QAAQ;QAC3B,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IACnD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,yBAAyB,CAAC;IACpE,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,OAAO,CACd,GAAY;IAEZ,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1D,IAAI,IAAI,CAAC,MAAM,IAAI,0BAA0B;QAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9E,OAAO;QACL,GAAG,EAAE;YACH,GAAG,GAAG;YACN,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,0BAA0B,CAAC;YAC/C,UAAU,EAAE,EAAE,cAAc,EAAE,IAAI,CAAC,MAAM,EAAE;SAC5C;QACD,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,GAW1B;IACC,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC;IACpC,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAChC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,OAAO;YAAE,UAAU,GAAG,IAAI,CAAC;QAC/B,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;IAC/B,2EAA2E;IAC3E,wEAAwE;IACxE,6EAA6E;IAC7E,yDAAyD;IACzD,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,IAAI,GAAG,CAAC,KAAK,IAAI,QAAQ,CAAC;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;IAEjC,OAAO;QACL,QAAQ,EAAE,MAAM;QAChB,KAAK;QACL,QAAQ;QACR,aAAa,EAAE,OAAO;QACtB,SAAS,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU;KACrC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * fix-inbox-delivery-and-tool-wrapping §1 — check_inbox bounds its payload and
3
+ * declares what it withheld.
4
+ *
5
+ * Why this file exists: check_inbox passed no `limit`, so the server returned
6
+ * its default 50 messages with full bodies and nothing truncated anywhere. One
7
+ * such call measured 115,769 characters, exceeded the tool-result token budget
8
+ * and never reached the model. A slash-command then bulk-marked those messages
9
+ * read, and because `unread_only` only returns 'pending'/'delivered' rows they
10
+ * became invisible for good. Bounding the payload is half the fix; SAYING what
11
+ * was withheld is the other half, because a silent cap turns loud breakage into
12
+ * quiet breakage.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=inboxBounds.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inboxBounds.test.d.ts","sourceRoot":"","sources":["../../src/tools/inboxBounds.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * fix-inbox-delivery-and-tool-wrapping §1 — check_inbox bounds its payload and
3
+ * declares what it withheld.
4
+ *
5
+ * Why this file exists: check_inbox passed no `limit`, so the server returned
6
+ * its default 50 messages with full bodies and nothing truncated anywhere. One
7
+ * such call measured 115,769 characters, exceeded the tool-result token budget
8
+ * and never reached the model. A slash-command then bulk-marked those messages
9
+ * read, and because `unread_only` only returns 'pending'/'delivered' rows they
10
+ * became invisible for good. Bounding the payload is half the fix; SAYING what
11
+ * was withheld is the other half, because a silent cap turns loud breakage into
12
+ * quiet breakage.
13
+ */
14
+ import { describe, it, expect, beforeEach, vi } from "vitest";
15
+ import { handleToolCall } from "./index.js";
16
+ import { CHECK_INBOX_DEFAULT_LIMIT, CHECK_INBOX_MAX_LIMIT, CHECK_INBOX_BODY_CAP_BYTES, } from "./inboxBounds.js";
17
+ vi.mock("../state.js", () => ({
18
+ loadState: vi.fn(() => null),
19
+ saveState: vi.fn(),
20
+ deleteState: vi.fn(),
21
+ stateExists: vi.fn(() => false),
22
+ getCurrentOwner: vi.fn(() => "test-user"),
23
+ }));
24
+ vi.mock("../hooks/sessionState.js", async (importOriginal) => {
25
+ const actual = (await importOriginal());
26
+ return { ...actual, isEnabled: vi.fn(() => false) };
27
+ });
28
+ const mockClient = {
29
+ getBaseUrl: vi.fn(() => "http://localhost:8787/api"),
30
+ getInbox: vi.fn(),
31
+ getPendingTasks: vi.fn(),
32
+ };
33
+ /** n messages, each body `bodyBytes` long, newest first like GetInbox returns. */
34
+ function makeMessages(n, bodyBytes = 10) {
35
+ return Array.from({ length: n }, (_, i) => ({
36
+ id: `msg-${i}`,
37
+ from_agent: "sender",
38
+ to_agent: "test-agent",
39
+ broadcast: false,
40
+ type: "question",
41
+ subject: `subject ${i}`,
42
+ body: "x".repeat(bodyBytes),
43
+ status: "delivered",
44
+ priority: "normal",
45
+ created_at: new Date(Date.now() - i * 1000).toISOString(),
46
+ }));
47
+ }
48
+ describe("check_inbox payload bounds", () => {
49
+ let context;
50
+ let agentId;
51
+ beforeEach(() => {
52
+ vi.clearAllMocks();
53
+ agentId = "test-agent";
54
+ context = {
55
+ getCurrentAgentId: () => agentId,
56
+ setCurrentAgentId: (id) => {
57
+ agentId = id;
58
+ },
59
+ stopHeartbeat: () => { },
60
+ getWorkingDir: () => "/tmp",
61
+ setWorkingDir: () => { },
62
+ getPushedItems: () => ({ tasks: [], messages: [] }),
63
+ };
64
+ });
65
+ it("applies a default limit and reports what it withheld", async () => {
66
+ // The regression: 40 unread messages, no limit given by the caller.
67
+ vi.mocked(mockClient.getInbox).mockResolvedValue({
68
+ messages: makeMessages(CHECK_INBOX_DEFAULT_LIMIT),
69
+ total: 40,
70
+ });
71
+ const result = (await handleToolCall("check_inbox", { unread_only: true }, mockClient, context));
72
+ // The limit must reach the client call — omitting it is the original bug.
73
+ expect(mockClient.getInbox).toHaveBeenCalledWith("test-agent", true, CHECK_INBOX_DEFAULT_LIMIT);
74
+ expect(result.total).toBe(40);
75
+ expect(result.returned).toBe(CHECK_INBOX_DEFAULT_LIMIT);
76
+ expect(result.omitted_count).toBe(40 - CHECK_INBOX_DEFAULT_LIMIT);
77
+ expect(result.truncated).toBe(true);
78
+ });
79
+ it("clips an oversized body and names its original length", async () => {
80
+ const original = CHECK_INBOX_BODY_CAP_BYTES + 1000;
81
+ vi.mocked(mockClient.getInbox).mockResolvedValue({
82
+ messages: makeMessages(1, original),
83
+ total: 1,
84
+ });
85
+ const result = (await handleToolCall("check_inbox", { unread_only: true }, mockClient, context));
86
+ const msg = result.messages[0];
87
+ expect(msg.body.length).toBeLessThanOrEqual(CHECK_INBOX_BODY_CAP_BYTES);
88
+ expect(msg._truncated).toEqual({ original_bytes: original });
89
+ // A clipped body is still a withheld thing: the caller must be told.
90
+ expect(result.truncated).toBe(true);
91
+ });
92
+ it("reports nothing withheld when nothing was withheld", async () => {
93
+ vi.mocked(mockClient.getInbox).mockResolvedValue({
94
+ messages: makeMessages(3),
95
+ total: 3,
96
+ });
97
+ const result = (await handleToolCall("check_inbox", { unread_only: true }, mockClient, context));
98
+ expect(result.truncated).toBe(false);
99
+ expect(result.omitted_count).toBe(0);
100
+ expect(result.returned).toBe(3);
101
+ for (const m of result.messages) {
102
+ expect(m._truncated).toBeUndefined();
103
+ expect(m.body.length).toBe(10);
104
+ }
105
+ });
106
+ it("clamps a caller-supplied limit above the maximum", async () => {
107
+ vi.mocked(mockClient.getInbox).mockResolvedValue({
108
+ messages: makeMessages(CHECK_INBOX_MAX_LIMIT),
109
+ total: 500,
110
+ });
111
+ await handleToolCall("check_inbox", { unread_only: true, limit: 10_000 }, mockClient, context);
112
+ expect(mockClient.getInbox).toHaveBeenCalledWith("test-agent", true, CHECK_INBOX_MAX_LIMIT);
113
+ });
114
+ it("honours a caller-supplied limit below the maximum", async () => {
115
+ vi.mocked(mockClient.getInbox).mockResolvedValue({
116
+ messages: makeMessages(2),
117
+ total: 2,
118
+ });
119
+ await handleToolCall("check_inbox", { unread_only: true, limit: 2 }, mockClient, context);
120
+ expect(mockClient.getInbox).toHaveBeenCalledWith("test-agent", true, 2);
121
+ });
122
+ it("keeps the payload well under the budget for an inbox that once blew it", async () => {
123
+ // 787 rows averaging ~147 bytes of body is what produced the 115,769-char
124
+ // response observed on 2026-10-17. Same corpus, bounded output.
125
+ vi.mocked(mockClient.getInbox).mockResolvedValue({
126
+ messages: makeMessages(CHECK_INBOX_DEFAULT_LIMIT, 8000),
127
+ total: 787,
128
+ });
129
+ const result = (await handleToolCall("check_inbox", { unread_only: true }, mockClient, context));
130
+ expect(JSON.stringify(result).length).toBeLessThan(60_000);
131
+ expect(result.truncated).toBe(true);
132
+ expect(result.omitted_count).toBe(787 - CHECK_INBOX_DEFAULT_LIMIT);
133
+ });
134
+ });
135
+ //# sourceMappingURL=inboxBounds.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inboxBounds.test.js","sourceRoot":"","sources":["../../src/tools/inboxBounds.test.ts"],"names":[],"mappings":"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"}
@@ -13,7 +13,18 @@ export interface ToolContext {
13
13
  stopHeartbeat: () => void;
14
14
  getWorkingDir: () => string;
15
15
  setWorkingDir: (dir: string) => void;
16
+ /**
17
+ * @deprecated Destructive read; see peekPushedItems
18
+ * (fix-inbox-delivery-and-tool-wrapping §2). Kept so older callers compile.
19
+ */
16
20
  getPushedItems: () => PushedItems;
21
+ /** Read pushed items without consuming them. */
22
+ peekPushedItems?: () => PushedItems;
23
+ /** Consume exactly the items that made it into a built response. */
24
+ commitPushedItems?: (consumed: {
25
+ taskIds?: string[];
26
+ messageIds?: string[];
27
+ }) => void;
17
28
  }
18
29
  interface PendingTask {
19
30
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,OAAO,EAAqD,MAAM,cAAc,CAAC;AAOrG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAiF/D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,iBAAiB,EAAE,MAAM,MAAM,CAAC;IAChC,iBAAiB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,MAAM,CAAC;IAC5B,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,MAAM,WAAW,CAAC;CACnC;AAED,UAAU,WAAW;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAwJD,wBAAgB,aAAa,IAAI,IAAI,EAAE,CA80FtC;AA2BD,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CA8sFlB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,OAAO,EAAqD,MAAM,cAAc,CAAC;AAOrG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAkG/D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,iBAAiB,EAAE,MAAM,MAAM,CAAC;IAChC,iBAAiB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,MAAM,CAAC;IAC5B,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,cAAc,EAAE,MAAM,WAAW,CAAC;IAClC,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,WAAW,CAAC;IACpC,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE;QAC7B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;KACvB,KAAK,IAAI,CAAC;CACZ;AAED,UAAU,WAAW;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAyLD,wBAAgB,aAAa,IAAI,IAAI,EAAE,CAk2FtC;AA2BD,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CAyuFlB"}
@@ -2,6 +2,7 @@
2
2
  * MCP tool definitions and handlers
3
3
  */
4
4
  import { resolveAgentIdentity, missingStateGuidance, credentialIsDead, } from "../agentIdentity.js";
5
+ import { boundInbox, resolveInboxLimit, CHECK_INBOX_DEFAULT_LIMIT, CHECK_INBOX_MAX_LIMIT, } from "./inboxBounds.js";
5
6
  import * as state from "../state.js";
6
7
  import * as contextModule from "../context.js";
7
8
  import { hookManager } from "../hooks/index.js";
@@ -9,9 +10,12 @@ import * as brainCapture from "../hooks/brainCapture.js";
9
10
  import { getBrain } from "../brain/backend.js";
10
11
  import open from "open";
11
12
  import { registerOpenSpecVizTools, handleOpenSpecVizToolCall } from "./openspec-viz/index.js";
13
+ import { registerIntakeRequestTools, handleIntakeRequestToolCall, isIntakeRequestTool, } from "./intakeRequests.js";
12
14
  import { registerSessionControlTools, handleSessionControlToolCall, isSessionControlTool, } from "./sessionControl.js";
15
+ import { registerGoshtiReadTools, handleGoshtiReadToolCall, isGoshtiReadTool, } from "./goshtiRead.js";
13
16
  import { installPackFromArchive } from "../skillPackInstaller.js";
14
17
  import { getServerTime, useTimeInjection } from "../lib/server-time.js";
18
+ import { getPendingItems } from "../lib/pending-items.js";
15
19
  import { ENG_IC_CHARTER } from "../employee.js";
16
20
  import { promises as fsp } from "node:fs";
17
21
  import * as os from "node:os";
@@ -78,16 +82,23 @@ async function wrapWithPendingItems(client, agentId, result, context) {
78
82
  if (!agentId)
79
83
  return result;
80
84
  try {
81
- // Get items pushed via WebSocket (if connected)
82
- const pushedItems = context?.getPushedItems() || { tasks: [], messages: [] };
83
- // Fetch unread messages and pending tasks in parallel
84
- const [inbox, tasksResponse] = await Promise.all([
85
- client.getInbox(agentId, true, 10),
86
- client.getPendingTasks(agentId),
87
- ]);
88
- // Merge pushed items with fetched items (deduplicating by ID)
89
- const fetchedMessages = inbox.messages || [];
90
- const fetchedTasks = (tasksResponse.tasks || []);
85
+ // Read items pushed via WebSocket WITHOUT consuming them
86
+ // (fix-inbox-delivery-and-tool-wrapping §2.3). The old code drained the
87
+ // queue here, before the two fetches below — so a fetch that threw or timed
88
+ // out sent us to the catch with those messages already gone. They are
89
+ // consumed at the bottom of this function, once a response exists to carry
90
+ // them. peekPushedItems is optional on the context so older callers (and
91
+ // tests constructing a partial context) keep working.
92
+ const pushedItems = context?.peekPushedItems?.() ??
93
+ context?.getPushedItems?.() ?? { tasks: [], messages: [] };
94
+ // Read pending items from cache; never wait on the network
95
+ // (fix-inbox-delivery-and-tool-wrapping §3.2). This used to await two HTTP
96
+ // calls on every one of the 58 wrapped tools, each with a 45s deadline, so
97
+ // an unrelated tool could stall ~90s before doing its own work. A refresh
98
+ // now runs in the background and a cold cache simply carries nothing.
99
+ const cachedItems = getPendingItems(client, agentId);
100
+ const fetchedMessages = cachedItems.messages;
101
+ const fetchedTasks = cachedItems.tasks;
91
102
  // Deduplicate messages
92
103
  const messageIds = new Set(fetchedMessages.map((m) => m.id));
93
104
  const messages = [
@@ -128,7 +139,7 @@ async function wrapWithPendingItems(client, agentId, result, context) {
128
139
  urgentAction = {
129
140
  required: true,
130
141
  instruction: buildUrgentInstruction(urgentMsg),
131
- message: urgentMsg,
142
+ message_id: urgentMsg.id,
132
143
  };
133
144
  }
134
145
  }
@@ -148,10 +159,20 @@ async function wrapWithPendingItems(client, agentId, result, context) {
148
159
  console.warn("server_time injection failed:", err);
149
160
  }
150
161
  }
162
+ // The response now exists and carries these items, so they may be consumed
163
+ // (§2.3). Only the ids we actually peeked are named: anything pushed since
164
+ // then stays queued for the next tool call rather than being cleared away
165
+ // unseen. Reaching the catch above consumes nothing, which is the whole
166
+ // point of the split.
167
+ context?.commitPushedItems?.({
168
+ taskIds: pushedItems.tasks.map((t) => t.id),
169
+ messageIds: pushedItems.messages.map((m) => m.id),
170
+ });
151
171
  return wrapped;
152
172
  }
153
173
  catch (error) {
154
- // Don't break tool call if fetch fails
174
+ // Don't break tool call if fetch fails. Pushed items were only peeked, so
175
+ // they are still queued and will be delivered by the next call.
155
176
  console.warn("Failed to fetch pending items for wrapping:", error);
156
177
  return result;
157
178
  }
@@ -167,13 +188,16 @@ function isUrgentMessage(msg) {
167
188
  * Build instruction text for urgent messages
168
189
  */
169
190
  function buildUrgentInstruction(msg) {
191
+ // No body here (fix-inbox-delivery-and-tool-wrapping §3.4). Interpolating
192
+ // ${msg.body} made this the second of three copies of the same text in one
193
+ // response — the others being pending_messages[] and urgent_action.message.
194
+ // The instruction's job is to say what to do and which message to do it
195
+ // about; the body is one lookup away in pending_messages.
170
196
  return `URGENT: Agent [${msg.from_agent}] needs immediate assistance.
171
197
 
172
198
  Query: ${msg.subject}
173
199
 
174
- Details: ${msg.body}
175
-
176
- Spawn a subagent with Task tool to research and respond to this query. Use send_message to reply when done.`;
200
+ The full message is in this response's pending_messages[], id ${msg.id}. Read it there, then reply with the reply tool (message_id ${msg.id}).`;
177
201
  }
178
202
  /**
179
203
  * Build instruction text for pending tasks
@@ -690,7 +714,12 @@ export function registerTools() {
690
714
  },
691
715
  {
692
716
  name: "check_inbox",
693
- description: "Check for incoming messages",
717
+ description: "Check for incoming messages. The response is bounded: it carries " +
718
+ "`total`, `returned`, `omitted_count` and `truncated`, and a clipped " +
719
+ "body carries `_truncated.original_bytes`. When `truncated` is true you " +
720
+ "did NOT receive everything — do not mark_read anything you did not " +
721
+ "actually read, or it becomes invisible to everyone (unread_only never " +
722
+ "returns a read message again). Page with `limit` instead.",
694
723
  inputSchema: {
695
724
  type: "object",
696
725
  properties: {
@@ -698,6 +727,13 @@ export function registerTools() {
698
727
  type: "boolean",
699
728
  description: "Only return unread messages (default: false)",
700
729
  },
730
+ limit: {
731
+ type: "number",
732
+ description: `Max messages to return (default: ${CHECK_INBOX_DEFAULT_LIMIT}, ` +
733
+ `max: ${CHECK_INBOX_MAX_LIMIT}). Values above the max are clamped; ` +
734
+ `0, negative and non-numeric values take the default — there is no ` +
735
+ `way to request an unbounded inbox.`,
736
+ },
701
737
  },
702
738
  },
703
739
  },
@@ -2116,6 +2152,10 @@ export function registerTools() {
2116
2152
  },
2117
2153
  // Session steering + takeover (add-multiplayer-sessions §4.7, §5.9)
2118
2154
  ...registerSessionControlTools(),
2155
+ // Intake requests: file, triage, scope check (add-intake-requests §6.1)
2156
+ ...registerIntakeRequestTools(),
2157
+ // Reading Goshti as the agent's owner (add-agent-goshti-read §3.1)
2158
+ ...registerGoshtiReadTools(),
2119
2159
  // OpenSpec Visualizer tools
2120
2160
  ...registerOpenSpecVizTools(),
2121
2161
  // Intel layer tools (§2.5 — read-only MCP surface for intel.search + source catalog)
@@ -3006,6 +3046,18 @@ export async function handleToolCall(name, args, client, context) {
3006
3046
  if (isSessionControlTool(name)) {
3007
3047
  return handleSessionControlToolCall(name, args, client);
3008
3048
  }
3049
+ // Intake requests (add-intake-requests §6.1). Same module pattern: the module
3050
+ // owns its tool names and turns refusals into structured results.
3051
+ if (isIntakeRequestTool(name)) {
3052
+ return handleIntakeRequestToolCall(name, args, client, () => state.loadState(context.getWorkingDir())?.project_id);
3053
+ }
3054
+ // Goshti reads (add-agent-goshti-read §3.1). Same module pattern. The owner's
3055
+ // grant, the per-agent switch and the fixed tool list are all the server's
3056
+ // decisions; this only shapes the call and labels the answer as other
3057
+ // people's words.
3058
+ if (isGoshtiReadTool(name)) {
3059
+ return handleGoshtiReadToolCall(name, args, client);
3060
+ }
3009
3061
  switch (name) {
3010
3062
  // Registration
3011
3063
  case "agent_register": {
@@ -3682,10 +3734,19 @@ export async function handleToolCall(name, args, client, context) {
3682
3734
  return wrapWithPendingItems(client, agentId, result, context);
3683
3735
  }
3684
3736
  case "check_inbox": {
3685
- // No wrapping - already fetching messages
3737
+ // No wrapping - already fetching messages.
3738
+ //
3739
+ // The limit is explicit (fix-inbox-delivery-and-tool-wrapping §1.2).
3740
+ // Omitting it let the server apply its own default of 50 full-bodied
3741
+ // messages; one such response measured 115,769 characters, exceeded the
3742
+ // tool-result token budget and never reached the model at all. boundInbox
3743
+ // then declares anything it withheld, so a caller can never acknowledge
3744
+ // messages it did not actually receive.
3686
3745
  if (!agentId)
3687
3746
  throw new Error("Not registered. Call agent_register first.");
3688
- return client.getInbox(agentId, args.unread_only);
3747
+ const inboxLimit = resolveInboxLimit(args.limit);
3748
+ const raw = await client.getInbox(agentId, args.unread_only, inboxLimit);
3749
+ return boundInbox(raw);
3689
3750
  }
3690
3751
  case "mark_read": {
3691
3752
  const result = await client.markRead(args.message_id);