@cortexkit/aft-opencode 0.58.0 → 0.58.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.
Files changed (65) hide show
  1. package/dist/bridge-bootstrap.d.ts +8 -0
  2. package/dist/bridge-bootstrap.d.ts.map +1 -1
  3. package/dist/config-live-reload.d.ts +323 -0
  4. package/dist/config-live-reload.d.ts.map +1 -0
  5. package/dist/config.d.ts +59 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/entry/server.js +2543 -989
  8. package/dist/hashline-tag-sources.d.ts +18 -0
  9. package/dist/hashline-tag-sources.d.ts.map +1 -0
  10. package/dist/hooks/auto-update-checker/checker.d.ts +1 -0
  11. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  12. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1901 -748
  15. package/dist/lsp-auto-install.d.ts +30 -0
  16. package/dist/lsp-auto-install.d.ts.map +1 -1
  17. package/dist/lsp-github-table.d.ts +4 -3
  18. package/dist/lsp-github-table.d.ts.map +1 -1
  19. package/dist/lsp-npm-table.d.ts +11 -1
  20. package/dist/lsp-npm-table.d.ts.map +1 -1
  21. package/dist/lsp-registry-probe.d.ts +8 -4
  22. package/dist/lsp-registry-probe.d.ts.map +1 -1
  23. package/dist/permissions/v2-service.d.ts +110 -5
  24. package/dist/permissions/v2-service.d.ts.map +1 -1
  25. package/dist/rpc/register.d.ts +0 -4
  26. package/dist/rpc/register.d.ts.map +1 -1
  27. package/dist/shared/read-vision.d.ts +10 -0
  28. package/dist/shared/read-vision.d.ts.map +1 -0
  29. package/dist/shared/session-directory.d.ts +7 -0
  30. package/dist/shared/session-directory.d.ts.map +1 -1
  31. package/dist/shared/subagent-detect.d.ts +11 -4
  32. package/dist/shared/subagent-detect.d.ts.map +1 -1
  33. package/dist/tool-perf.d.ts +1 -0
  34. package/dist/tool-perf.d.ts.map +1 -1
  35. package/dist/tool-registration.d.ts +8 -5
  36. package/dist/tool-registration.d.ts.map +1 -1
  37. package/dist/tools/_shared.d.ts +16 -15
  38. package/dist/tools/_shared.d.ts.map +1 -1
  39. package/dist/tools/bash.d.ts +24 -1
  40. package/dist/tools/bash.d.ts.map +1 -1
  41. package/dist/tools/bash_watch.d.ts +5 -2
  42. package/dist/tools/bash_watch.d.ts.map +1 -1
  43. package/dist/tools/bash_write.d.ts.map +1 -1
  44. package/dist/tools/definitions/v2.d.ts +2 -0
  45. package/dist/tools/definitions/v2.d.ts.map +1 -1
  46. package/dist/tools/hoisted/v2.d.ts +8 -1
  47. package/dist/tools/hoisted/v2.d.ts.map +1 -1
  48. package/dist/tools/hoisted.d.ts +9 -7
  49. package/dist/tools/hoisted.d.ts.map +1 -1
  50. package/dist/tools/permissions.d.ts.map +1 -1
  51. package/dist/tools/reading.d.ts +0 -3
  52. package/dist/tools/reading.d.ts.map +1 -1
  53. package/dist/v2-prompt-detach.d.ts +69 -0
  54. package/dist/v2-prompt-detach.d.ts.map +1 -0
  55. package/dist/wakes/runtime-consumer.d.ts +2 -0
  56. package/dist/wakes/runtime-consumer.d.ts.map +1 -1
  57. package/dist/wakes/session-delivery.d.ts +9 -0
  58. package/dist/wakes/session-delivery.d.ts.map +1 -1
  59. package/dist/workflow-hints.d.ts +20 -7
  60. package/dist/workflow-hints.d.ts.map +1 -1
  61. package/package.json +8 -8
  62. package/src/entry/server-runtime.mjs +36 -5
  63. package/src/shared/read-vision.ts +100 -0
  64. package/src/shared/session-directory.ts +92 -0
  65. package/src/shared/subagent-detect.ts +89 -28
@@ -21,10 +21,14 @@
21
21
  * identity is sticky (parentID never changes after session creation), so
22
22
  * the cache never needs invalidation.
23
23
  *
24
- * Errors are not cached. If the SDK is briefly unavailable on the first
25
- * call, we retry on the next bash invocation rather than permanently
26
- * misclassifying. This is cheap because the cache is hit on every
27
- * subsequent call within the same session.
24
+ * Every bridge call asks for the role (AFT words its replies by it), so the
25
+ * lookup must stay cheap and bounded:
26
+ * - concurrent first calls for one session share a single lookup;
27
+ * - a lookup that has not answered within LOOKUP_TIMEOUT_MS is abandoned and
28
+ * that call proceeds as primary, so a slow host can't delay a read;
29
+ * - a failed or abandoned lookup is cached as "primary" for
30
+ * FAILED_LOOKUP_RETRY_MS, then retried, so a struggling host is not asked on
31
+ * every call but the real answer is still picked up later.
28
32
  *
29
33
  * Mirrors the pattern in `session-directory.ts`.
30
34
  */
@@ -48,20 +52,33 @@ interface OpenCodeClientShape {
48
52
 
49
53
  interface CacheEntry {
50
54
  isSubagent: boolean;
51
- recordedAt: number;
55
+ /**
56
+ * When a provisional "primary" answer (recorded after a failed or timed-out
57
+ * lookup) stops counting and the host is asked again. Absent for a real
58
+ * answer, which never changes: a session's parent is fixed at creation.
59
+ */
60
+ retryAfter?: number;
52
61
  }
53
62
 
54
63
  const CACHE_MAX_ENTRIES = 200;
64
+ /** Longest a caller waits for the host's session lookup. */
65
+ export const LOOKUP_TIMEOUT_MS = 1_000;
66
+ /** How long a failed or timed-out lookup is answered as "primary" before retrying. */
67
+ export const FAILED_LOOKUP_RETRY_MS = 30_000;
55
68
  const cache = new Map<string, CacheEntry>();
69
+ /** One lookup per session at a time; concurrent first calls await it. */
70
+ const inflight = new Map<string, Promise<boolean>>();
71
+ let now: () => number = () => Date.now();
56
72
 
57
73
  /**
58
74
  * Returns `true` when the given session has a non-empty `parentID`
59
75
  * (subagent). Returns `false` for primary sessions, when the SDK is
60
- * unavailable, or when any error occurs — the false default keeps
61
- * primary sessions working exactly as before.
76
+ * unavailable, or when the lookup fails or takes longer than
77
+ * LOOKUP_TIMEOUT_MS; the false default keeps primary sessions working
78
+ * exactly as before.
62
79
  *
63
- * First call per session: one SDK round-trip. All subsequent calls:
64
- * O(1) cache lookup.
80
+ * A known answer is an O(1) cache hit. Otherwise at most one host lookup
81
+ * per session is in flight, and it is bounded by LOOKUP_TIMEOUT_MS.
65
82
  */
66
83
  export async function resolveIsSubagent(
67
84
  client: unknown,
@@ -74,7 +91,7 @@ export async function resolveIsSubagent(
74
91
  }
75
92
 
76
93
  const cached = cache.get(sessionId);
77
- if (cached) {
94
+ if (cached && (cached.retryAfter === undefined || now() < cached.retryAfter)) {
78
95
  // Refresh LRU position. Don't log the hit — it's pure noise; the
79
96
  // downstream call sites already log their effective gate decisions.
80
97
  cache.delete(sessionId);
@@ -82,6 +99,48 @@ export async function resolveIsSubagent(
82
99
  return cached.isSubagent;
83
100
  }
84
101
 
102
+ const pending = inflight.get(sessionId);
103
+ if (pending) return pending;
104
+ const lookup = boundedLookup(client, sessionId).finally(() => {
105
+ inflight.delete(sessionId);
106
+ });
107
+ inflight.set(sessionId, lookup);
108
+ return lookup;
109
+ }
110
+
111
+ /**
112
+ * Runs the host lookup, giving up after LOOKUP_TIMEOUT_MS. A real answer is
113
+ * cached for good; a failure or timeout is cached as "primary" until
114
+ * FAILED_LOOKUP_RETRY_MS has passed. The abandoned host call is left to
115
+ * settle on its own; its late answer is ignored.
116
+ */
117
+ async function boundedLookup(client: unknown, sessionId: string): Promise<boolean> {
118
+ let timer: ReturnType<typeof setTimeout> | undefined;
119
+ const timedOut = new Promise<"timeout">((resolve) => {
120
+ timer = setTimeout(() => resolve("timeout"), LOOKUP_TIMEOUT_MS);
121
+ });
122
+ let answer: boolean | "failed" | "timeout";
123
+ try {
124
+ answer = await Promise.race([lookupSession(client, sessionId), timedOut]);
125
+ } finally {
126
+ clearTimeout(timer);
127
+ }
128
+ if (typeof answer === "boolean") {
129
+ setCache(sessionId, answer);
130
+ return answer;
131
+ }
132
+ if (answer === "timeout") {
133
+ sessionWarn(
134
+ sessionId,
135
+ `[subagent-detect] session lookup did not answer within ${LOOKUP_TIMEOUT_MS}ms → primary for now`,
136
+ );
137
+ }
138
+ setCache(sessionId, false, now() + FAILED_LOOKUP_RETRY_MS);
139
+ return false;
140
+ }
141
+
142
+ /** The host's answer, or "failed" when the lookup threw. Never rejects. */
143
+ async function lookupSession(client: unknown, sessionId: string): Promise<boolean | "failed"> {
85
144
  if (isV2PluginContext(client)) return resolveV2(client, sessionId);
86
145
 
87
146
  const c = client as OpenCodeClientShape;
@@ -93,7 +152,6 @@ export async function resolveIsSubagent(
93
152
  sessionId,
94
153
  `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`,
95
154
  );
96
- setCache(sessionId, false);
97
155
  return false;
98
156
  }
99
157
 
@@ -102,8 +160,6 @@ export async function resolveIsSubagent(
102
160
  `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`,
103
161
  );
104
162
 
105
- let isSubagent = false;
106
- let parentIdRaw: unknown;
107
163
  try {
108
164
  // Call as a method so the SDK's `this._client` reference resolves
109
165
  // correctly. Extracting `sessionApi.get` into a local would lose
@@ -119,25 +175,21 @@ export async function resolveIsSubagent(
119
175
  // `Session` depending on `ThrowOnError`. Handle both shapes.
120
176
  const session: SessionInfo | undefined =
121
177
  (result as { data?: SessionInfo } | undefined)?.data ?? (result as SessionInfo | undefined);
122
- parentIdRaw = session?.parentID;
123
- isSubagent =
178
+ const parentIdRaw = session?.parentID;
179
+ const isSubagent =
124
180
  session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
125
181
  sessionLog(
126
182
  sessionId,
127
183
  `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`,
128
184
  );
185
+ return isSubagent;
129
186
  } catch (err) {
130
- // Don't poison the cache on transient errors — but do log once.
131
- // Return false so primary-session behavior is preserved.
132
187
  sessionWarn(
133
188
  sessionId,
134
189
  `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`,
135
190
  );
136
- return false;
191
+ return "failed";
137
192
  }
138
-
139
- setCache(sessionId, isSubagent);
140
- return isSubagent;
141
193
  }
142
194
 
143
195
  /**
@@ -166,33 +218,35 @@ function isV2PluginContext(client: unknown): client is V2PluginContextShape {
166
218
  );
167
219
  }
168
220
 
169
- async function resolveV2(context: V2PluginContextShape, sessionId: string): Promise<boolean> {
221
+ async function resolveV2(
222
+ context: V2PluginContextShape,
223
+ sessionId: string,
224
+ ): Promise<boolean | "failed"> {
170
225
  let session: SessionInfo | undefined;
171
226
  try {
172
227
  session = await Effect.runPromise(
173
228
  context.session.get({ sessionID: sessionId }) as Effect.Effect<SessionInfo | undefined>,
174
229
  );
175
230
  } catch (err) {
176
- // Same policy as the V1 path: a failed lookup is not cached, and the call
177
- // is treated as primary for now so the next tool call can retry.
231
+ // Same policy as the V1 path: a failed lookup counts as primary only
232
+ // until FAILED_LOOKUP_RETRY_MS has passed.
178
233
  sessionWarn(
179
234
  sessionId,
180
235
  `[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`,
181
236
  );
182
- return false;
237
+ return "failed";
183
238
  }
184
239
  const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
185
240
  sessionLog(
186
241
  sessionId,
187
242
  `[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`,
188
243
  );
189
- setCache(sessionId, isSubagent);
190
244
  return isSubagent;
191
245
  }
192
246
 
193
- function setCache(sessionId: string, isSubagent: boolean): void {
247
+ function setCache(sessionId: string, isSubagent: boolean, retryAfter?: number): void {
194
248
  if (cache.has(sessionId)) cache.delete(sessionId);
195
- cache.set(sessionId, { isSubagent, recordedAt: Date.now() });
249
+ cache.set(sessionId, retryAfter === undefined ? { isSubagent } : { isSubagent, retryAfter });
196
250
  if (cache.size > CACHE_MAX_ENTRIES) {
197
251
  const oldest = cache.keys().next().value;
198
252
  if (oldest !== undefined) cache.delete(oldest);
@@ -202,4 +256,11 @@ function setCache(sessionId: string, isSubagent: boolean): void {
202
256
  /** Test-only cache reset. Not exported from the public surface. */
203
257
  export function _resetSubagentCacheForTest(): void {
204
258
  cache.clear();
259
+ inflight.clear();
260
+ now = () => Date.now();
261
+ }
262
+
263
+ /** Test-only clock override for the failed-lookup retry window. */
264
+ export function _setSubagentClockForTest(clock: () => number): void {
265
+ now = clock;
205
266
  }