@cortexkit/opencode-magic-context 0.21.1 → 0.21.2

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.
@@ -3,6 +3,14 @@ import type { CompartmentRunnerDeps } from "./compartment-runner-types";
3
3
  export interface ActiveCompartmentRun {
4
4
  promise: Promise<void>;
5
5
  published: boolean;
6
+ /**
7
+ * Set to true once the 95%-emergency user-facing notification has been
8
+ * dispatched for this run. Prevents the notification from re-firing on
9
+ * every subsequent transform pass while the same compartment run is
10
+ * still active — which would otherwise persist a fresh ignored user
11
+ * message every pass and drive OpenCode's runLoop break condition false.
12
+ */
13
+ notificationSent?: boolean;
6
14
  }
7
15
  export declare function getActiveCompartmentRun(sessionId: string): ActiveCompartmentRun | undefined;
8
16
  export declare function markActiveCompartmentRunPublished(sessionId: string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"compartment-runner.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/compartment-runner.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,KAAK,kBAAkB,EAC1B,MAAM,qCAAqC,CAAC;AAE7C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAExE,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACtB;AAID,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAE3F;AAED,wBAAgB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGzE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GACvB,oBAAoB,CAetB;AAYD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CA6BvE;AAED,MAAM,WAAW,2BAA2B;IACxC;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC9B;AAED,wBAAsB,oBAAoB,CACtC,IAAI,EAAE,qBAAqB,EAC3B,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,MAAM,CAAC,CAuBjB;AAED,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC"}
1
+ {"version":3,"file":"compartment-runner.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/compartment-runner.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,KAAK,kBAAkB,EAC1B,MAAM,qCAAqC,CAAC;AAE7C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAExE,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAID,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAE3F;AAED,wBAAgB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGzE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GACvB,oBAAoB,CAetB;AAYD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CA6BvE;AAED,MAAM,WAAW,2BAA2B;IACxC;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC9B;AAED,wBAAsB,oBAAoB,CACtC,IAAI,EAAE,qBAAqB,EAC3B,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,MAAM,CAAC,CAuBjB;AAED,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"transform-compartment-phase.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/transform-compartment-phase.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,eAAe,EAAqB,MAAM,sCAAsC,CAAC;AAC/F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAYxD,OAAO,EACH,KAAK,4BAA4B,EAEpC,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAc1D,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE/D;AAED,UAAU,uBAAuB;IAC7B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE;QAAE,qBAAqB,EAAE,OAAO,CAAA;KAAE,CAAC;IAChD,YAAY,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,MAAM,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACjC,EAAE,EAAE,eAAe,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,2BAA2B,EAAE,4BAA4B,GAAG,IAAI,CAAC;IACjE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uBAAuB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,OAAO,6BAA6B,EAAE,kBAAkB,CAAC;IACvF,6EAA6E;IAC7E,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,yFAAyF;IACzF,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD,8BAA8B,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5C,yFAAyF;IACzF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,0FAA0F;IAC1F,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,0EAA0E;IAC1E,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,+EAA+E;IAC/E,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kFAAkF;IAClF,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,yFAAyF;IACzF,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mEAAmE;IACnE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uEAAuE;IACvE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+FAA+F;IAC/F,2BAA2B,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7D;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC;IAC9E,2BAA2B,EAAE,4BAA4B,GAAG,IAAI,CAAC;IACjE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,sBAAsB,EAAE,OAAO,CAAC;IAChC,sBAAsB,EAAE,OAAO,CAAC;CACnC,CAAC,CA2QD"}
1
+ {"version":3,"file":"transform-compartment-phase.d.ts","sourceRoot":"","sources":["../../../src/hooks/magic-context/transform-compartment-phase.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,eAAe,EAAqB,MAAM,sCAAsC,CAAC;AAC/F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAYxD,OAAO,EACH,KAAK,4BAA4B,EAEpC,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAc1D,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE/D;AAED,UAAU,uBAAuB;IAC7B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE;QAAE,qBAAqB,EAAE,OAAO,CAAA;KAAE,CAAC;IAChD,YAAY,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,MAAM,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACjC,EAAE,EAAE,eAAe,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,2BAA2B,EAAE,4BAA4B,GAAG,IAAI,CAAC;IACjE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uBAAuB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,OAAO,6BAA6B,EAAE,kBAAkB,CAAC;IACvF,6EAA6E;IAC7E,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,yFAAyF;IACzF,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD,8BAA8B,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5C,yFAAyF;IACzF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,0FAA0F;IAC1F,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,0EAA0E;IAC1E,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,+EAA+E;IAC/E,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kFAAkF;IAClF,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,yFAAyF;IACzF,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mEAAmE;IACnE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uEAAuE;IACvE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+FAA+F;IAC/F,2BAA2B,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7D;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC;IAC9E,2BAA2B,EAAE,4BAA4B,GAAG,IAAI,CAAC;IACjE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,sBAAsB,EAAE,OAAO,CAAC;IAChC,sBAAsB,EAAE,OAAO,CAAC;CACnC,CAAC,CAyRD"}
package/dist/index.js CHANGED
@@ -157007,7 +157007,10 @@ async function getSessionMessages(client, sessionId) {
157007
157007
  try {
157008
157008
  const c = client;
157009
157009
  if (typeof c.session?.messages === "function") {
157010
- const result = await c.session.messages({ path: { id: sessionId } });
157010
+ const result = await c.session.messages({
157011
+ path: { id: sessionId },
157012
+ query: { limit: 50 }
157013
+ });
157011
157014
  return result?.data ?? [];
157012
157015
  }
157013
157016
  } catch (error51) {
@@ -163695,7 +163698,7 @@ async function runHistorianPrompt(args) {
163695
163698
  }
163696
163699
  const messagesResponse = await client.session.messages({
163697
163700
  path: { id: agentSessionId },
163698
- query: { directory: sessionDirectory }
163701
+ query: { directory: sessionDirectory, limit: 50 }
163699
163702
  });
163700
163703
  const messages = normalizeSDKResponse(messagesResponse, [], {
163701
163704
  preferResponseOnMissingData: true
@@ -165653,7 +165656,7 @@ async function runCompressorPass(args) {
165653
165656
  });
165654
165657
  const messagesResponse = await client.session.messages({
165655
165658
  path: { id: agentSessionId },
165656
- query: { directory }
165659
+ query: { directory, limit: 50 }
165657
165660
  });
165658
165661
  const messages = normalizeSDKResponse(messagesResponse, [], {
165659
165662
  preferResponseOnMissingData: true
@@ -167265,7 +167268,7 @@ async function runSidekick(deps) {
167265
167268
  }, { timeoutMs: deps.config.timeout_ms, fallbackModels, callContext: "sidekick" });
167266
167269
  const messagesResponse = await deps.client.session.messages({
167267
167270
  path: { id: agentSessionId },
167268
- query: { directory: deps.sessionDirectory ?? deps.projectPath }
167271
+ query: { directory: deps.sessionDirectory ?? deps.projectPath, limit: 50 }
167269
167272
  });
167270
167273
  const messages = normalizeSDKResponse(messagesResponse, [], {
167271
167274
  preferResponseOnMissingData: true
@@ -168513,7 +168516,7 @@ async function runKeyFilesLlm(args) {
168513
168516
  });
168514
168517
  const messagesResponse = await args.client.session.messages({
168515
168518
  path: { id: agentSessionId },
168516
- query: { directory: args.projectPath }
168519
+ query: { directory: args.projectPath, limit: 50 }
168517
168520
  });
168518
168521
  const messages = normalizeSDKResponse(messagesResponse, [], {
168519
168522
  preferResponseOnMissingData: true
@@ -168709,7 +168712,7 @@ If no promotions are warranted, return empty arrays. Always consume reviewed can
168709
168712
  });
168710
168713
  const messagesResponse = await args.client.session.messages({
168711
168714
  path: { id: agentSessionId },
168712
- query: { directory: args.sessionDirectory }
168715
+ query: { directory: args.sessionDirectory, limit: 50 }
168713
168716
  });
168714
168717
  const messages = normalizeSDKResponse(messagesResponse, [], {
168715
168718
  preferResponseOnMissingData: true
@@ -169014,7 +169017,7 @@ async function runDream(args) {
169014
169017
  }
169015
169018
  const messagesResponse = await args.client.session.messages({
169016
169019
  path: { id: agentSessionId },
169017
- query: { directory: args.sessionDirectory ?? args.projectIdentity }
169020
+ query: { directory: args.sessionDirectory ?? args.projectIdentity, limit: 50 }
169018
169021
  });
169019
169022
  const messages = normalizeSDKResponse(messagesResponse, [], {
169020
169023
  preferResponseOnMissingData: true
@@ -169337,7 +169340,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
169337
169340
  });
169338
169341
  const messagesResponse = await args.client.session.messages({
169339
169342
  path: { id: agentSessionId },
169340
- query: { directory: args.sessionDirectory ?? args.projectIdentity }
169343
+ query: { directory: args.sessionDirectory ?? args.projectIdentity, limit: 50 }
169341
169344
  });
169342
169345
  const messages = normalizeSDKResponse(messagesResponse, [], {
169343
169346
  preferResponseOnMissingData: true
@@ -172477,7 +172480,8 @@ async function runCompartmentPhase(args) {
172477
172480
  sessionLog(args.sessionId, "transform: cannot force-start compartment agent without client");
172478
172481
  }
172479
172482
  if (activeRun) {
172480
- if (args.client) {
172483
+ if (args.client && !activeRun.notificationSent) {
172484
+ activeRun.notificationSent = true;
172481
172485
  const notifParams = args.getNotificationParams?.() ?? {};
172482
172486
  sendIgnoredMessage(args.client, args.sessionId, `⏳ Context at ${args.contextUsage.percentage.toFixed(0)}% — Magic Context is compacting history before continuing. This may take up to 2 minutes.`, notifParams);
172483
172487
  }
@@ -1 +1 @@
1
- {"version":3,"file":"conflict-warning-hook.d.ts","sourceRoot":"","sources":["../../src/plugin/conflict-warning-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AA0KlE;;GAEG;AACH,wBAAsB,mBAAmB,CACrC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,cAAc,GAC/B,OAAO,CAAC,IAAI,CAAC,CA+Cf;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CACzC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAqHf;AAkCD;;;GAGG;AACH,wBAAsB,wBAAwB,CAC1C,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAkEf"}
1
+ {"version":3,"file":"conflict-warning-hook.d.ts","sourceRoot":"","sources":["../../src/plugin/conflict-warning-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAmLlE;;GAEG;AACH,wBAAsB,mBAAmB,CACrC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,cAAc,GAC/B,OAAO,CAAC,IAAI,CAAC,CA+Cf;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CACzC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAqHf;AAkCD;;;GAGG;AACH,wBAAsB,wBAAwB,CAC1C,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAkEf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,112 @@
1
+ /// <reference types="bun-types" />
2
+
3
+ // Regression guard: every `client.session.messages(...)` call in plugin code
4
+ // must include `limit` in its `query`. Without `limit`, OpenCode's legacy
5
+ // messages endpoint hydrates the ENTIRE session into RAM — catastrophic on
6
+ // huge sessions (10k+ messages) which is exactly when Magic Context shines.
7
+ //
8
+ // Background: the plugin only ever needs the latest assistant message of a
9
+ // helper subagent (historian / dreamer / sidekick / key-files / user-memory)
10
+ // or a bounded tail of the active session (conflict-warning cleanup). Both
11
+ // fit comfortably in `limit: 50` with massive headroom.
12
+ //
13
+ // If you add a new `session.messages(...)` call, this test fails until you
14
+ // include a `limit` in the query. The test does a static source-text scan
15
+ // so it catches the issue at lint-time without runtime mocking overhead.
16
+
17
+ import { describe, expect, it } from "bun:test";
18
+ import { readdirSync, readFileSync, statSync } from "node:fs";
19
+ import { join } from "node:path";
20
+
21
+ const PLUGIN_SRC = join(__dirname, "..", "..", "src");
22
+
23
+ /** Recursively walk a directory and return paths of all `.ts` files
24
+ * excluding `.test.ts`, `.gen.ts`, and node_modules. */
25
+ function walkSourceFiles(dir: string, out: string[] = []): string[] {
26
+ for (const entry of readdirSync(dir)) {
27
+ if (entry === "node_modules" || entry === "dist") continue;
28
+ const full = join(dir, entry);
29
+ const s = statSync(full);
30
+ if (s.isDirectory()) {
31
+ walkSourceFiles(full, out);
32
+ } else if (
33
+ s.isFile() &&
34
+ entry.endsWith(".ts") &&
35
+ !entry.endsWith(".test.ts") &&
36
+ !entry.endsWith(".gen.ts")
37
+ ) {
38
+ out.push(full);
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /** Find call expressions matching `session.messages(...)` and return the
45
+ * source text of each call (from "session.messages(" to its matching ")"). */
46
+ function findSessionMessagesCalls(source: string): string[] {
47
+ const calls: string[] = [];
48
+ const needle = "session.messages(";
49
+ let i = 0;
50
+ while (true) {
51
+ const idx = source.indexOf(needle, i);
52
+ if (idx === -1) break;
53
+ // Skip comments — if the line starts with `*` or `//` we ignore.
54
+ const lineStart = source.lastIndexOf("\n", idx) + 1;
55
+ const linePrefix = source.slice(lineStart, idx).trimStart();
56
+ if (linePrefix.startsWith("//") || linePrefix.startsWith("*")) {
57
+ i = idx + needle.length;
58
+ continue;
59
+ }
60
+ // Walk forward to find the matching closing paren, respecting
61
+ // brace/bracket/paren nesting.
62
+ let depth = 1;
63
+ let j = idx + needle.length;
64
+ while (j < source.length && depth > 0) {
65
+ const ch = source[j];
66
+ if (ch === "(" || ch === "{" || ch === "[") depth++;
67
+ else if (ch === ")" || ch === "}" || ch === "]") depth--;
68
+ j++;
69
+ }
70
+ if (depth === 0) {
71
+ calls.push(source.slice(idx, j));
72
+ }
73
+ i = j;
74
+ }
75
+ return calls;
76
+ }
77
+
78
+ describe("session.messages() callsites must include query.limit", () => {
79
+ const files = walkSourceFiles(PLUGIN_SRC);
80
+ const violations: Array<{ file: string; callText: string }> = [];
81
+
82
+ for (const file of files) {
83
+ // Skip the regression test itself
84
+ if (file.endsWith("session-messages-bounded.test.ts")) continue;
85
+ const source = readFileSync(file, "utf-8");
86
+ const calls = findSessionMessagesCalls(source);
87
+ for (const call of calls) {
88
+ // The call text spans from "session.messages(" to the closing
89
+ // ")". A correct call must mention `limit` (the SDK key) somewhere
90
+ // inside that span — typically `query: { ..., limit: N }`.
91
+ if (!/\blimit\b/.test(call)) {
92
+ violations.push({ file: file.replace(PLUGIN_SRC, "<plugin>/src"), callText: call });
93
+ }
94
+ }
95
+ }
96
+
97
+ it("has no unbounded session.messages() calls in plugin code", () => {
98
+ if (violations.length > 0) {
99
+ const report = violations
100
+ .map(
101
+ (v) =>
102
+ `\n in ${v.file}:\n ${v.callText.replace(/\n/g, "\n ").slice(0, 300)}`,
103
+ )
104
+ .join("\n");
105
+ throw new Error(
106
+ `Found ${violations.length} unbounded session.messages() call(s). ` +
107
+ `Add \`limit: 50\` (or appropriate bound) to query.${report}`,
108
+ );
109
+ }
110
+ expect(violations).toEqual([]);
111
+ });
112
+ });