@cortexkit/opencode-magic-context 0.32.0 → 0.32.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.
- package/README.md +3 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +3 -0
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +82 -2
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config/transform-mode.d.ts +15 -0
- package/dist/config/transform-mode.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/cron.d.ts +2 -2
- package/dist/features/magic-context/dreamer/cron.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-provider.d.ts +6 -0
- package/dist/features/magic-context/memory/embedding-provider.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-synapse.d.ts +98 -0
- package/dist/features/magic-context/memory/embedding-synapse.d.ts.map +1 -0
- package/dist/features/magic-context/memory/embedding.d.ts +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/message-index-async.d.ts +4 -1
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +7 -4
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +21 -16
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/plugin-messages.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +27 -0
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search-measurement.d.ts +14 -0
- package/dist/features/magic-context/search-measurement.d.ts.map +1 -0
- package/dist/features/magic-context/search.d.ts +11 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/session-project-backfill.d.ts +6 -3
- package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/capabilities.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts +3 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/runner.d.ts +1 -0
- package/dist/features/magic-context/smart-notes/runner.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +8 -1
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/ssrf-guard.d.ts +10 -0
- package/dist/features/magic-context/smart-notes/ssrf-guard.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts +16 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
- package/dist/features/magic-context/storage-clone.d.ts +4 -0
- package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +7 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-embedding-measurements.d.ts +60 -0
- package/dist/features/magic-context/storage-embedding-measurements.d.ts.map +1 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts +10 -5
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-schema-helpers.d.ts +3 -4
- package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -0
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/tool-owner-backfill.d.ts +2 -0
- package/dist/features/magic-context/tool-owner-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts +7 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts +2 -0
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +5 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts +4 -0
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-render-epoch.d.ts +12 -0
- package/dist/hooks/magic-context/compartment-render-epoch.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/decay-render.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts +1 -0
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts +7 -0
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts.map +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +29 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -0
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -0
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +6 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +19 -3
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/index.d.ts +1 -1
- package/dist/hooks/magic-context/index.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +1 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts +51 -0
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts +25 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts +169 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -0
- package/dist/hooks/magic-context/module-wire.d.ts +73 -0
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts +15 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +17 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +30 -2
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +64 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -0
- package/dist/hooks/magic-context/shadow-sender.d.ts +84 -49
- package/dist/hooks/magic-context/shadow-sender.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +56 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +9 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17355 -10510
- package/dist/plugin/boot-quiet.d.ts +12 -0
- package/dist/plugin/boot-quiet.d.ts.map +1 -0
- package/dist/plugin/dream-timer.d.ts +6 -2
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/embedding-bootstrap-helpers.d.ts.map +1 -1
- package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
- package/dist/plugin/embedding-routing.d.ts +32 -0
- package/dist/plugin/embedding-routing.d.ts.map +1 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts +54 -2
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +4 -3
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +19 -2
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/rust-tool-backends.d.ts +10 -0
- package/dist/plugin/rust-tool-backends.d.ts.map +1 -0
- package/dist/plugin/tool-registry.d.ts +2 -0
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts +5 -0
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +3 -1
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +11 -6
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +2 -2
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +3 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/tui-config.d.ts +5 -3
- package/dist/shared/tui-config.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/types.d.ts +2 -0
- package/dist/tools/ctx-memory/types.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts +2 -0
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts +9 -2
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui/data/notification-socket.d.ts +4 -5
- package/dist/tui/data/notification-socket.d.ts.map +1 -1
- package/dist/tui-compiled/data/context-db.d.ts +9 -2
- package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
- package/dist/tui-compiled/data/notification-socket.d.ts +4 -5
- package/dist/tui-compiled/data/notification-socket.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/shared/models-dev-cache.test.ts +5 -0
- package/src/shared/models-dev-cache.ts +44 -6
- package/src/shared/opencode-compaction-detector.test.ts +11 -2
- package/src/shared/rpc-client.test.ts +213 -9
- package/src/shared/rpc-client.ts +48 -39
- package/src/shared/rpc-notifications.test.ts +26 -5
- package/src/shared/rpc-notifications.ts +44 -35
- package/src/shared/rpc-server.ts +69 -33
- package/src/shared/rpc-types.ts +3 -0
- package/src/shared/sqlite.ts +22 -3
- package/src/shared/tui-config.test.ts +4 -12
- package/src/shared/tui-config.ts +6 -6
- package/src/tui/data/context-db.test.ts +113 -0
- package/src/tui/data/context-db.ts +26 -19
- package/src/tui/data/notification-socket.test.ts +277 -11
- package/src/tui/data/notification-socket.ts +257 -86
- package/src/tui/index.tsx +42 -25
- package/src/tui/slots/sidebar-content.tsx +62 -24
- package/src/tui-compiled/data/context-db.ts +26 -19
- package/src/tui-compiled/data/notification-socket.ts +257 -86
- package/src/tui-compiled/index.tsx +83 -30
- package/src/tui-compiled/slots/sidebar-content.tsx +45 -23
|
@@ -15,8 +15,15 @@ export declare function loadSidebarSnapshot(sessionId: string, directory: string
|
|
|
15
15
|
export declare function loadStatusDetail(sessionId: string, directory: string, modelKey?: string): Promise<StatusDetail>;
|
|
16
16
|
/** Fetch embedding coverage status for `/ctx-embed` via RPC. */
|
|
17
17
|
export declare function loadEmbedDetail(sessionId: string, directory: string): Promise<EmbedDetail>;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
export type CompartmentCountResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
count: number;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
error: string;
|
|
24
|
+
};
|
|
25
|
+
/** Get compartment count without making transport failure look like a real zero. */
|
|
26
|
+
export declare function getCompartmentCount(sessionId: string): Promise<CompartmentCountResult>;
|
|
20
27
|
/** Send recomp request to server via RPC. */
|
|
21
28
|
export declare function requestRecomp(sessionId: string): Promise<boolean>;
|
|
22
29
|
/** Run `/ctx-session-upgrade` for the session (full recomp + once-per-project
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-db.d.ts","sourceRoot":"","sources":["../../../src/tui-compiled/data/context-db.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEzF,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC;AAc3D,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAOrD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;sCACsC;AACtC,wBAAgB,YAAY,IAAI,qBAAqB,GAAG,IAAI,CAE3D;AAED,+BAA+B;AAC/B,wBAAgB,QAAQ,IAAI,IAAI,CAM/B;
|
|
1
|
+
{"version":3,"file":"context-db.d.ts","sourceRoot":"","sources":["../../../src/tui-compiled/data/context-db.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEzF,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC;AAc3D,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAOrD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;sCACsC;AACtC,wBAAgB,YAAY,IAAI,qBAAqB,GAAG,IAAI,CAE3D;AAED,+BAA+B;AAC/B,wBAAgB,QAAQ,IAAI,IAAI,CAM/B;AAuFD,sDAAsD;AACtD,wBAAsB,mBAAmB,CACrC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAClB,OAAO,CAAC,eAAe,CAAC,CA8B1B;AAED,wDAAwD;AACxD,wBAAsB,gBAAgB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CA0CvB;AAYD,gEAAgE;AAChE,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAchG;AAED,MAAM,MAAM,sBAAsB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhG,oFAAoF;AACpF,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAe5F;AAED,6CAA6C;AAC7C,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQvE;AAED;mFACmF;AACnF,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQxE;AAED;;iDAEiD;AACjD,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUhF;AAED,gEAAgE;AAChE,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CAQ3D;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAkBrE;AAED,wEAAwE;AACxE,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAQtD"}
|
|
@@ -26,14 +26,13 @@ export interface SocketNotification {
|
|
|
26
26
|
interface NotificationSocketOptions {
|
|
27
27
|
/** Current active session id (re-read cheaply to follow session switches). */
|
|
28
28
|
getSessionId: () => string | null;
|
|
29
|
-
/** Handle one delivered notification. Returns true
|
|
30
|
-
*
|
|
29
|
+
/** Handle one delivered notification. Returns true only after it is fully
|
|
30
|
+
* consumed and can be acknowledged. Async because dialog handlers await. */
|
|
31
31
|
onNotification: (notification: SocketNotification) => boolean | Promise<boolean>;
|
|
32
32
|
}
|
|
33
|
-
/** Open the persistent notification socket.
|
|
34
|
-
* is a no-op. Reconnects on its own after any drop. */
|
|
33
|
+
/** Open the persistent notification socket. Reconnects on its own after a drop. */
|
|
35
34
|
export declare function startNotificationSocket(options: NotificationSocketOptions): void;
|
|
36
|
-
/** Close the socket and
|
|
35
|
+
/** Close the socket and release all state owned by this TUI initialization. */
|
|
37
36
|
export declare function stopNotificationSocket(): void;
|
|
38
37
|
export declare function _resetNotificationSocketStateForTesting(): void;
|
|
39
38
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-socket.d.ts","sourceRoot":"","sources":["../../../src/tui-compiled/data/notification-socket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,MAAM,WAAW,kBAAkB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,yBAAyB;IAC/B,8EAA8E;IAC9E,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAClC
|
|
1
|
+
{"version":3,"file":"notification-socket.d.ts","sourceRoot":"","sources":["../../../src/tui-compiled/data/notification-socket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,MAAM,WAAW,kBAAkB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,yBAAyB;IAC/B,8EAA8E;IAC9E,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAClC;iFAC6E;IAC7E,cAAc,EAAE,CAAC,YAAY,EAAE,kBAAkB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpF;AA6CD,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,yBAAyB,GAAG,IAAI,CAOhF;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,IAAI,IAAI,CA+B7C;AAyUD,wBAAgB,uCAAuC,IAAI,IAAI,CAE9D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/opencode-magic-context",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.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",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"prepublishOnly": "bun run build && bun run build:tui"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"@cortexkit/subc-client": "0.4.1",
|
|
46
47
|
"@huggingface/transformers": "^4.1.0",
|
|
47
48
|
"@jitl/quickjs-singlefile-cjs-release-asyncify": "0.32.0",
|
|
48
49
|
"@opencode-ai/plugin": "^1.17.11",
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
clearModelsDevCache,
|
|
7
7
|
getModelsDevCacheState,
|
|
8
8
|
getSdkContextLimit,
|
|
9
|
+
getSdkInputLimit,
|
|
9
10
|
refreshModelLimitsAfterAuthOnce,
|
|
10
11
|
refreshModelLimitsFromApi,
|
|
11
12
|
resetAuthRewarmLatchForTest,
|
|
@@ -62,7 +63,9 @@ describe("models-dev-cache (SDK-only)", () => {
|
|
|
62
63
|
);
|
|
63
64
|
|
|
64
65
|
expect(getSdkContextLimit("github-copilot", "gpt-5.3-codex")).toBe(272000);
|
|
66
|
+
expect(getSdkInputLimit("github-copilot", "gpt-5.3-codex")).toBe(272000);
|
|
65
67
|
expect(getSdkContextLimit("github-copilot", "legacy-only-context")).toBe(100000);
|
|
68
|
+
expect(getSdkInputLimit("github-copilot", "legacy-only-context")).toBeUndefined();
|
|
66
69
|
expect(getSdkContextLimit("unknown", "unknown")).toBeUndefined();
|
|
67
70
|
});
|
|
68
71
|
|
|
@@ -78,6 +81,7 @@ describe("models-dev-cache (SDK-only)", () => {
|
|
|
78
81
|
]),
|
|
79
82
|
);
|
|
80
83
|
expect(getSdkContextLimit("openai", "gpt-5.5")).toBe(272000);
|
|
84
|
+
expect(getSdkInputLimit("openai", "gpt-5.5")).toBe(272000);
|
|
81
85
|
});
|
|
82
86
|
|
|
83
87
|
test("derived experimental.modes inherit the effective (input) limit", async () => {
|
|
@@ -172,6 +176,7 @@ describe("models-dev-cache (SDK-only)", () => {
|
|
|
172
176
|
makeClient([{ id: "openai", models: { "gpt-5.5": { limit: { input: 272000 } } } }]),
|
|
173
177
|
);
|
|
174
178
|
expect(getSdkContextLimit("openai", "gpt-5.5")).toBe(272000);
|
|
179
|
+
expect(getSdkInputLimit("openai", "gpt-5.5")).toBe(272000);
|
|
175
180
|
|
|
176
181
|
// Simulate a restart: in-memory cache gone, but the persisted file
|
|
177
182
|
// remains under XDG_DATA_HOME. The next lookup seeds from disk.
|
|
@@ -56,6 +56,8 @@ export function isSaneLimit(limit: number | undefined): limit is number {
|
|
|
56
56
|
|
|
57
57
|
interface CachedModelMetadata {
|
|
58
58
|
limit?: number;
|
|
59
|
+
/** Provider-enforced prompt cap. Undefined when only a combined context window is known. */
|
|
60
|
+
inputLimit?: number;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/**
|
|
@@ -90,10 +92,20 @@ function loadPersistedApiCacheOnce(): void {
|
|
|
90
92
|
persistSeedLoaded = true;
|
|
91
93
|
try {
|
|
92
94
|
const raw = readFileSync(persistFilePath(), "utf-8");
|
|
93
|
-
const obj = JSON.parse(raw) as Record<
|
|
95
|
+
const obj = JSON.parse(raw) as Record<
|
|
96
|
+
string,
|
|
97
|
+
number | { limit?: number; inputLimit?: number }
|
|
98
|
+
>;
|
|
94
99
|
const map = new Map<string, CachedModelMetadata>();
|
|
95
|
-
for (const [key,
|
|
96
|
-
|
|
100
|
+
for (const [key, persisted] of Object.entries(obj)) {
|
|
101
|
+
const limit = typeof persisted === "number" ? persisted : persisted.limit;
|
|
102
|
+
const inputLimit = typeof persisted === "number" ? undefined : persisted.inputLimit;
|
|
103
|
+
if (isSaneLimit(limit)) {
|
|
104
|
+
map.set(key, {
|
|
105
|
+
limit,
|
|
106
|
+
inputLimit: isSaneLimit(inputLimit) ? inputLimit : undefined,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
97
109
|
}
|
|
98
110
|
if (map.size > 0) {
|
|
99
111
|
apiCache = map;
|
|
@@ -112,9 +124,14 @@ function loadPersistedApiCacheOnce(): void {
|
|
|
112
124
|
* never sees a torn file (the exact failure mode we're eliminating). */
|
|
113
125
|
function persistApiCache(): void {
|
|
114
126
|
if (!apiCache) return;
|
|
115
|
-
const obj: Record<string,
|
|
127
|
+
const obj: Record<string, CachedModelMetadata> = {};
|
|
116
128
|
for (const [key, value] of apiCache) {
|
|
117
|
-
if (isSaneLimit(value.limit))
|
|
129
|
+
if (isSaneLimit(value.limit)) {
|
|
130
|
+
obj[key] = {
|
|
131
|
+
limit: value.limit,
|
|
132
|
+
inputLimit: isSaneLimit(value.inputLimit) ? value.inputLimit : undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
118
135
|
}
|
|
119
136
|
try {
|
|
120
137
|
const dir = getMagicContextStorageDir();
|
|
@@ -157,6 +174,7 @@ function setCachedModelMetadata(
|
|
|
157
174
|
| undefined,
|
|
158
175
|
): void {
|
|
159
176
|
const limit = resolveLimit(model?.limit);
|
|
177
|
+
const inputLimit = model?.limit?.input;
|
|
160
178
|
|
|
161
179
|
// Only cache plausible limits. A value outside [20k, 3M] is garbage (torn
|
|
162
180
|
// read / unconfigured default) and must never enter the cache or get
|
|
@@ -165,7 +183,10 @@ function setCachedModelMetadata(
|
|
|
165
183
|
return;
|
|
166
184
|
}
|
|
167
185
|
|
|
168
|
-
const value: CachedModelMetadata = {
|
|
186
|
+
const value: CachedModelMetadata = {
|
|
187
|
+
limit,
|
|
188
|
+
inputLimit: isSaneLimit(inputLimit) ? inputLimit : undefined,
|
|
189
|
+
};
|
|
169
190
|
cache.set(key, value);
|
|
170
191
|
|
|
171
192
|
// OpenCode creates derived model IDs from experimental.modes
|
|
@@ -332,6 +353,23 @@ export function getSdkContextLimit(providerID: string, modelID: string): number
|
|
|
332
353
|
return isSaneLimit(fromApi) ? fromApi : undefined;
|
|
333
354
|
}
|
|
334
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Return only a provider-declared input cap. A combined context window is useful
|
|
358
|
+
* for scheduling but is not safe as a fail-closed prompt boundary.
|
|
359
|
+
*/
|
|
360
|
+
export function getSdkInputLimit(providerID: string, modelID: string): number | undefined {
|
|
361
|
+
loadPersistedApiCacheOnce();
|
|
362
|
+
if (!apiCache) return undefined;
|
|
363
|
+
const direct = apiCache.get(`${providerID}/${modelID}`)?.inputLimit;
|
|
364
|
+
if (isSaneLimit(direct)) return direct;
|
|
365
|
+
const colon = modelID.indexOf(":");
|
|
366
|
+
if (colon > 0) {
|
|
367
|
+
const tagless = apiCache.get(`${providerID}/${modelID.slice(0, colon)}`)?.inputLimit;
|
|
368
|
+
if (isSaneLimit(tagless)) return tagless;
|
|
369
|
+
}
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
|
|
335
373
|
/**
|
|
336
374
|
* Look up a model's limit in the cache, with an ollama-style tag-suffix
|
|
337
375
|
* fallback. ollama invokes cloud models with a tag at runtime
|
|
@@ -6,24 +6,33 @@ import * as configDir from "./opencode-config-dir";
|
|
|
6
6
|
|
|
7
7
|
describe("opencode-compaction-detector", () => {
|
|
8
8
|
let tmpDir: string;
|
|
9
|
+
let previousDisableAutocompact: string | undefined;
|
|
10
|
+
let restoreConfigPaths: () => void;
|
|
9
11
|
|
|
10
12
|
beforeEach(() => {
|
|
11
13
|
tmpDir = join("/tmp", `compaction-detector-test-${Date.now()}`);
|
|
12
14
|
mkdirSync(join(tmpDir, ".opencode"), { recursive: true });
|
|
15
|
+
previousDisableAutocompact = process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
13
16
|
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
14
|
-
spyOn(configDir, "getOpenCodeConfigPaths").mockReturnValue({
|
|
17
|
+
const configPathsSpy = spyOn(configDir, "getOpenCodeConfigPaths").mockReturnValue({
|
|
15
18
|
configJson: join(tmpDir, "user-config", "opencode.json"),
|
|
16
19
|
configJsonc: join(tmpDir, "user-config", "opencode.jsonc"),
|
|
17
20
|
} as ReturnType<typeof configDir.getOpenCodeConfigPaths>);
|
|
21
|
+
restoreConfigPaths = () => configPathsSpy.mockRestore();
|
|
18
22
|
});
|
|
19
23
|
|
|
20
24
|
afterEach(() => {
|
|
25
|
+
restoreConfigPaths();
|
|
21
26
|
try {
|
|
22
27
|
rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
23
28
|
} catch {
|
|
24
29
|
/* Ignore EBUSY on Windows */
|
|
25
30
|
}
|
|
26
|
-
|
|
31
|
+
if (previousDisableAutocompact === undefined) {
|
|
32
|
+
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
33
|
+
} else {
|
|
34
|
+
process.env.OPENCODE_DISABLE_AUTOCOMPACT = previousDisableAutocompact;
|
|
35
|
+
}
|
|
27
36
|
});
|
|
28
37
|
|
|
29
38
|
describe("#given no config exists", () => {
|
|
@@ -4,7 +4,12 @@ import { createServer } from "node:http";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { MagicContextRpcClient } from "./rpc-client";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
__resetNotificationStateForTests,
|
|
9
|
+
drainNotifications,
|
|
10
|
+
isTuiConnected,
|
|
11
|
+
pushNotification,
|
|
12
|
+
} from "./rpc-notifications";
|
|
8
13
|
import { MagicContextRpcServer } from "./rpc-server";
|
|
9
14
|
import { parseRpcPortFile, type RpcPortFileRecord, rpcPortDir, rpcPortFilePath } from "./rpc-utils";
|
|
10
15
|
|
|
@@ -20,6 +25,7 @@ afterEach(async () => {
|
|
|
20
25
|
for (const server of servers.splice(0)) {
|
|
21
26
|
await server.close();
|
|
22
27
|
}
|
|
28
|
+
__resetNotificationStateForTests();
|
|
23
29
|
for (const dir of tempDirs.splice(0)) {
|
|
24
30
|
try {
|
|
25
31
|
rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
@@ -51,10 +57,23 @@ function writePortFileForPid(
|
|
|
51
57
|
port: number,
|
|
52
58
|
pid: number,
|
|
53
59
|
startedAt: number,
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
instanceId?: string,
|
|
61
|
+
token?: string,
|
|
62
|
+
): string {
|
|
63
|
+
const portFile = rpcPortFilePath(storageDir, directory, pid, instanceId);
|
|
56
64
|
mkdirSync(dirname(portFile), { recursive: true });
|
|
57
|
-
writeFileSync(
|
|
65
|
+
writeFileSync(
|
|
66
|
+
portFile,
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
port,
|
|
69
|
+
pid,
|
|
70
|
+
started_at: startedAt,
|
|
71
|
+
instance_id: instanceId,
|
|
72
|
+
token,
|
|
73
|
+
}),
|
|
74
|
+
"utf-8",
|
|
75
|
+
);
|
|
76
|
+
return portFile;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
function readNewestPortRecord(storageDir: string, directory: string): RpcPortFileRecord | null {
|
|
@@ -79,8 +98,16 @@ async function waitFor(condition: () => boolean, label: string, timeoutMs = 2_00
|
|
|
79
98
|
throw new Error(`Timed out waiting for ${label}`);
|
|
80
99
|
}
|
|
81
100
|
|
|
82
|
-
async function openSocket(
|
|
83
|
-
|
|
101
|
+
async function openSocket(
|
|
102
|
+
port: number,
|
|
103
|
+
token: string,
|
|
104
|
+
legacyQueryAuth = false,
|
|
105
|
+
): Promise<WebSocket> {
|
|
106
|
+
const ws = legacyQueryAuth
|
|
107
|
+
? new WebSocket(`ws://127.0.0.1:${port}/ws?token=${encodeURIComponent(token)}`)
|
|
108
|
+
: new WebSocket(`ws://127.0.0.1:${port}/ws`, {
|
|
109
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
110
|
+
});
|
|
84
111
|
await new Promise<void>((resolve, reject) => {
|
|
85
112
|
const timeout = setTimeout(() => reject(new Error("socket open timed out")), 2_000);
|
|
86
113
|
ws.addEventListener(
|
|
@@ -129,11 +156,14 @@ function waitForJsonMessage<T extends { type?: string }>(
|
|
|
129
156
|
});
|
|
130
157
|
}
|
|
131
158
|
|
|
132
|
-
async function startRpcServer(
|
|
159
|
+
async function startRpcServer(
|
|
160
|
+
handler: (method: string) => Response | object,
|
|
161
|
+
health: { pid: number; instanceId?: string } = { pid: process.pid },
|
|
162
|
+
): Promise<TestServer> {
|
|
133
163
|
const server = createServer(async (req, res) => {
|
|
134
164
|
if (req.method === "GET" && req.url === "/health") {
|
|
135
165
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
136
|
-
res.end(JSON.stringify({ ok: true }));
|
|
166
|
+
res.end(JSON.stringify({ ok: true, pid: health.pid, instance_id: health.instanceId }));
|
|
137
167
|
return;
|
|
138
168
|
}
|
|
139
169
|
|
|
@@ -238,6 +268,31 @@ describe("MagicContextRpcClient", () => {
|
|
|
238
268
|
}
|
|
239
269
|
});
|
|
240
270
|
|
|
271
|
+
test("accepts a frozen v0.32 websocket upgrade with query-token auth", async () => {
|
|
272
|
+
const storageDir = makeTempDir();
|
|
273
|
+
const directory = "/repo-ws-v032";
|
|
274
|
+
const server = new MagicContextRpcServer(storageDir, directory);
|
|
275
|
+
const port = await server.start();
|
|
276
|
+
const record = readNewestPortRecord(storageDir, directory);
|
|
277
|
+
expect(typeof record?.token).toBe("string");
|
|
278
|
+
|
|
279
|
+
const ws = await openSocket(port, record?.token ?? "", true);
|
|
280
|
+
try {
|
|
281
|
+
const helloAck = waitForJsonMessage(ws, (message) => message.type === "hello-ack");
|
|
282
|
+
ws.send(
|
|
283
|
+
JSON.stringify({
|
|
284
|
+
type: "hello",
|
|
285
|
+
token: record?.token,
|
|
286
|
+
sessionId: "ses_v032",
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
expect((await helloAck).type).toBe("hello-ack");
|
|
290
|
+
} finally {
|
|
291
|
+
ws.close();
|
|
292
|
+
server.stop();
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
241
296
|
test("websocket upgrade rejects missing bearer token before a socket is created", async () => {
|
|
242
297
|
const storageDir = makeTempDir();
|
|
243
298
|
const directory = "/repo-ws-auth";
|
|
@@ -300,6 +355,52 @@ describe("MagicContextRpcClient", () => {
|
|
|
300
355
|
}
|
|
301
356
|
});
|
|
302
357
|
|
|
358
|
+
test("accepts legacy cursor acknowledgements during protocol skew", async () => {
|
|
359
|
+
__resetNotificationStateForTests();
|
|
360
|
+
const storageDir = makeTempDir();
|
|
361
|
+
const directory = "/repo-legacy-ack";
|
|
362
|
+
const server = new MagicContextRpcServer(storageDir, directory);
|
|
363
|
+
const port = await server.start();
|
|
364
|
+
const record = readNewestPortRecord(storageDir, directory);
|
|
365
|
+
expect(typeof record?.token).toBe("string");
|
|
366
|
+
|
|
367
|
+
pushNotification("legacy-one", { ok: true }, "ses_legacy");
|
|
368
|
+
pushNotification("legacy-two", { ok: true }, "ses_legacy");
|
|
369
|
+
const queued = drainNotifications(0, "ses_legacy", { sessionOnly: true });
|
|
370
|
+
expect(queued).toHaveLength(2);
|
|
371
|
+
|
|
372
|
+
const ws = await openSocket(port, record?.token ?? "");
|
|
373
|
+
try {
|
|
374
|
+
const helloAck = waitForJsonMessage<{
|
|
375
|
+
type?: string;
|
|
376
|
+
instanceId?: string;
|
|
377
|
+
}>(ws, (message) => message.type === "hello-ack");
|
|
378
|
+
ws.send(
|
|
379
|
+
JSON.stringify({
|
|
380
|
+
type: "hello",
|
|
381
|
+
token: record?.token,
|
|
382
|
+
sessionId: "ses_legacy",
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
expect((await helloAck).instanceId).toBe(record?.instance_id);
|
|
386
|
+
|
|
387
|
+
ws.send(
|
|
388
|
+
JSON.stringify({
|
|
389
|
+
type: "ack",
|
|
390
|
+
cursor: queued[0].id,
|
|
391
|
+
sessionId: "ses_legacy",
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
await waitFor(() => {
|
|
395
|
+
const pending = drainNotifications(0, "ses_legacy", { sessionOnly: true });
|
|
396
|
+
return pending.length === 1 && pending[0].id === queued[1].id;
|
|
397
|
+
}, "legacy cursor acknowledgement pruning");
|
|
398
|
+
} finally {
|
|
399
|
+
ws.close();
|
|
400
|
+
server.stop();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
303
404
|
test("same-process servers keep distinct port files during overlap", async () => {
|
|
304
405
|
const storageDir = makeTempDir();
|
|
305
406
|
const directory = "/repo-port-collision";
|
|
@@ -319,7 +420,9 @@ describe("MagicContextRpcClient", () => {
|
|
|
319
420
|
expect(remaining?.port).toBe(secondPort);
|
|
320
421
|
|
|
321
422
|
const client = new MagicContextRpcClient(storageDir, directory);
|
|
322
|
-
|
|
423
|
+
const endpoint = await client.resolveEndpoint();
|
|
424
|
+
expect(endpoint?.port).toBe(secondPort);
|
|
425
|
+
expect(endpoint?.instanceId).toBe(remaining?.instance_id);
|
|
323
426
|
} finally {
|
|
324
427
|
first.stop();
|
|
325
428
|
second.stop();
|
|
@@ -368,4 +471,105 @@ describe("MagicContextRpcClient", () => {
|
|
|
368
471
|
const client = new MagicContextRpcClient(storageDir, directory);
|
|
369
472
|
expect(await client.call<{ value: string }>("value")).toEqual({ value: "live" });
|
|
370
473
|
});
|
|
474
|
+
|
|
475
|
+
test("discovers a frozen v0.32 health response without an instance id", async () => {
|
|
476
|
+
const storageDir = makeTempDir();
|
|
477
|
+
const directory = "/repo-v032-health";
|
|
478
|
+
const legacy = await startRpcServer(() => ({ value: "legacy" }), {
|
|
479
|
+
pid: process.pid,
|
|
480
|
+
});
|
|
481
|
+
writePortFileForPid(
|
|
482
|
+
storageDir,
|
|
483
|
+
directory,
|
|
484
|
+
legacy.port,
|
|
485
|
+
process.pid,
|
|
486
|
+
Date.now(),
|
|
487
|
+
"new-client-record",
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const client = new MagicContextRpcClient(storageDir, directory);
|
|
491
|
+
expect(await client.call<{ value: string }>("value")).toEqual({ value: "legacy" });
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("prefers this process and validates every discovery candidate identity", async () => {
|
|
495
|
+
const storageDir = makeTempDir();
|
|
496
|
+
const directory = "/repo-affinity";
|
|
497
|
+
const foreign = await startRpcServer(() => ({ value: "foreign" }), {
|
|
498
|
+
pid: process.ppid,
|
|
499
|
+
instanceId: "foreign",
|
|
500
|
+
});
|
|
501
|
+
const unrelated = await startRpcServer(() => ({ value: "unrelated" }), {
|
|
502
|
+
pid: process.pid,
|
|
503
|
+
instanceId: "different-service",
|
|
504
|
+
});
|
|
505
|
+
const local = await startRpcServer(() => ({ value: "local" }), {
|
|
506
|
+
pid: process.pid,
|
|
507
|
+
instanceId: "local-healthy",
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
writePortFileForPid(
|
|
511
|
+
storageDir,
|
|
512
|
+
directory,
|
|
513
|
+
foreign.port,
|
|
514
|
+
process.ppid,
|
|
515
|
+
Date.now() + 20_000,
|
|
516
|
+
"foreign",
|
|
517
|
+
);
|
|
518
|
+
writePortFileForPid(
|
|
519
|
+
storageDir,
|
|
520
|
+
directory,
|
|
521
|
+
unrelated.port,
|
|
522
|
+
process.pid,
|
|
523
|
+
Date.now() + 10_000,
|
|
524
|
+
"local-stale",
|
|
525
|
+
);
|
|
526
|
+
writePortFileForPid(
|
|
527
|
+
storageDir,
|
|
528
|
+
directory,
|
|
529
|
+
local.port,
|
|
530
|
+
process.pid,
|
|
531
|
+
Date.now(),
|
|
532
|
+
"local-healthy",
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const client = new MagicContextRpcClient(storageDir, directory);
|
|
536
|
+
expect(await client.call<{ value: string }>("value")).toEqual({ value: "local" });
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("resets discovery after a 401 response", async () => {
|
|
540
|
+
const storageDir = makeTempDir();
|
|
541
|
+
const directory = "/repo-reauth";
|
|
542
|
+
let unauthorizedRecord = "";
|
|
543
|
+
const unauthorized = await startRpcServer(
|
|
544
|
+
() => {
|
|
545
|
+
rmSync(unauthorizedRecord, { force: true });
|
|
546
|
+
return new Response("stale token", { status: 401 });
|
|
547
|
+
},
|
|
548
|
+
{ pid: process.pid, instanceId: "unauthorized" },
|
|
549
|
+
);
|
|
550
|
+
const healthy = await startRpcServer(() => ({ value: "healthy" }), {
|
|
551
|
+
pid: process.pid,
|
|
552
|
+
instanceId: "healthy",
|
|
553
|
+
});
|
|
554
|
+
unauthorizedRecord = writePortFileForPid(
|
|
555
|
+
storageDir,
|
|
556
|
+
directory,
|
|
557
|
+
unauthorized.port,
|
|
558
|
+
process.pid,
|
|
559
|
+
Date.now() + 10_000,
|
|
560
|
+
"unauthorized",
|
|
561
|
+
"stale",
|
|
562
|
+
);
|
|
563
|
+
writePortFileForPid(
|
|
564
|
+
storageDir,
|
|
565
|
+
directory,
|
|
566
|
+
healthy.port,
|
|
567
|
+
process.pid,
|
|
568
|
+
Date.now(),
|
|
569
|
+
"healthy",
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
const client = new MagicContextRpcClient(storageDir, directory);
|
|
573
|
+
expect(await client.call<{ value: string }>("value")).toEqual({ value: "healthy" });
|
|
574
|
+
});
|
|
371
575
|
});
|
package/src/shared/rpc-client.ts
CHANGED
|
@@ -18,6 +18,7 @@ type NonRetryableRpcError = Error & { [NON_RETRYABLE_RPC_ERROR]: true };
|
|
|
18
18
|
export class MagicContextRpcClient {
|
|
19
19
|
private port: number | null = null;
|
|
20
20
|
private token: string | null = null;
|
|
21
|
+
private instanceId: string | null = null;
|
|
21
22
|
private portDir: string;
|
|
22
23
|
private legacyPortFilePath: string;
|
|
23
24
|
private healthChecked = false;
|
|
@@ -62,7 +63,7 @@ export class MagicContextRpcClient {
|
|
|
62
63
|
if (!response.ok) {
|
|
63
64
|
const text = await response.text();
|
|
64
65
|
const error = new Error(`RPC ${method} failed (${response.status}): ${text}`);
|
|
65
|
-
if (response.status >= 500) {
|
|
66
|
+
if (response.status === 401 || response.status >= 500) {
|
|
66
67
|
lastError = error;
|
|
67
68
|
this.reset();
|
|
68
69
|
continue;
|
|
@@ -101,52 +102,47 @@ export class MagicContextRpcClient {
|
|
|
101
102
|
* channel). Reuses the same health-checked port-file discovery as `call`,
|
|
102
103
|
* so the WS client and the HTTP client always agree on which server instance
|
|
103
104
|
* (and token) to use. Returns null when no live server is found. */
|
|
104
|
-
async resolveEndpoint(): Promise<{
|
|
105
|
+
async resolveEndpoint(): Promise<{
|
|
106
|
+
port: number;
|
|
107
|
+
token: string | null;
|
|
108
|
+
instanceId: string | null;
|
|
109
|
+
} | null> {
|
|
105
110
|
try {
|
|
106
|
-
|
|
111
|
+
// The socket owns reconnect backoff, so endpoint discovery performs one
|
|
112
|
+
// filesystem/health pass instead of nesting the HTTP client's retries.
|
|
113
|
+
const port = await this.resolvePort(1);
|
|
107
114
|
if (port === null) return null;
|
|
108
|
-
return { port, token: this.token };
|
|
115
|
+
return { port, token: this.token, instanceId: this.instanceId };
|
|
109
116
|
} catch {
|
|
110
117
|
return null;
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
|
|
114
|
-
private async resolvePort(): Promise<number | null> {
|
|
121
|
+
private async resolvePort(maxAttempts = MAX_RETRIES): Promise<number | null> {
|
|
115
122
|
if (this.port && this.healthChecked) {
|
|
116
123
|
return this.port;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
126
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
127
|
+
for (const record of this.readPortFiles()) {
|
|
128
|
+
if (!(await this.healthCheck(record))) continue;
|
|
129
|
+
this.port = record.port;
|
|
130
|
+
this.token = record.token ?? null;
|
|
131
|
+
this.instanceId = record.instance_id ?? null;
|
|
122
132
|
this.healthChecked = true;
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
this.port = null;
|
|
126
|
-
this.healthChecked = false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
130
|
-
const record = this.readPortFile();
|
|
131
|
-
if (record) {
|
|
132
|
-
const alive = await this.healthCheck(record.port);
|
|
133
|
-
if (alive) {
|
|
134
|
-
this.port = record.port;
|
|
135
|
-
this.token = record.token ?? null;
|
|
136
|
-
this.healthChecked = true;
|
|
137
|
-
return record.port;
|
|
138
|
-
}
|
|
133
|
+
return record.port;
|
|
139
134
|
}
|
|
140
135
|
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
this.reset();
|
|
137
|
+
if (attempt < maxAttempts - 1) {
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
143
139
|
}
|
|
144
140
|
}
|
|
145
141
|
|
|
146
142
|
return null;
|
|
147
143
|
}
|
|
148
144
|
|
|
149
|
-
private
|
|
145
|
+
private readPortFiles(): RpcPortFileRecord[] {
|
|
150
146
|
const records: RpcPortFileRecord[] = [];
|
|
151
147
|
|
|
152
148
|
try {
|
|
@@ -160,26 +156,38 @@ export class MagicContextRpcClient {
|
|
|
160
156
|
// Directory may not exist yet. Fall back to the legacy file below.
|
|
161
157
|
}
|
|
162
158
|
|
|
163
|
-
if (records.length > 0) {
|
|
164
|
-
records.sort((a, b) => b.started_at - a.started_at);
|
|
165
|
-
return records[0];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
159
|
try {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
return record;
|
|
160
|
+
const legacy = parseRpcPortFile(readFileSync(this.legacyPortFilePath, "utf-8"));
|
|
161
|
+
if (legacy && (!legacy.pid || isPidAlive(legacy.pid))) records.push(legacy);
|
|
172
162
|
} catch {
|
|
173
|
-
|
|
163
|
+
// Legacy discovery is optional.
|
|
174
164
|
}
|
|
165
|
+
|
|
166
|
+
// A TUI and its server plugin normally share a process. Prefer that exact
|
|
167
|
+
// process before considering another live OpenCode instance for the project.
|
|
168
|
+
records.sort((a, b) => {
|
|
169
|
+
const aLocal = a.pid === process.pid ? 1 : 0;
|
|
170
|
+
const bLocal = b.pid === process.pid ? 1 : 0;
|
|
171
|
+
return bLocal - aLocal || b.started_at - a.started_at;
|
|
172
|
+
});
|
|
173
|
+
return records;
|
|
175
174
|
}
|
|
176
175
|
|
|
177
|
-
private async healthCheck(
|
|
176
|
+
private async healthCheck(record: RpcPortFileRecord): Promise<boolean> {
|
|
178
177
|
try {
|
|
179
|
-
const response = await this.fetchWithTimeout(`http://127.0.0.1:${port}/health`, {
|
|
178
|
+
const response = await this.fetchWithTimeout(`http://127.0.0.1:${record.port}/health`, {
|
|
180
179
|
method: "GET",
|
|
181
180
|
});
|
|
182
|
-
|
|
181
|
+
if (!response.ok) return false;
|
|
182
|
+
const body = (await response.json()) as { pid?: unknown; instance_id?: unknown };
|
|
183
|
+
if (body.pid !== record.pid) return false;
|
|
184
|
+
// v0.32 health responses predate instance ids. A missing id is the
|
|
185
|
+
// one-release discovery bridge; an id that is present must still match.
|
|
186
|
+
return (
|
|
187
|
+
body.instance_id === undefined ||
|
|
188
|
+
record.instance_id === undefined ||
|
|
189
|
+
body.instance_id === record.instance_id
|
|
190
|
+
);
|
|
183
191
|
} catch {
|
|
184
192
|
return false;
|
|
185
193
|
}
|
|
@@ -198,6 +206,7 @@ export class MagicContextRpcClient {
|
|
|
198
206
|
reset(): void {
|
|
199
207
|
this.port = null;
|
|
200
208
|
this.token = null;
|
|
209
|
+
this.instanceId = null;
|
|
201
210
|
this.healthChecked = false;
|
|
202
211
|
}
|
|
203
212
|
}
|