@gajae-code/coding-agent 0.12.11 → 0.12.13

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 (140) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/types/cli/marketplace-hint.d.ts +28 -0
  3. package/dist/types/cli/notify-cli.d.ts +42 -1
  4. package/dist/types/cli/update-cli.d.ts +9 -3
  5. package/dist/types/cli.d.ts +10 -0
  6. package/dist/types/commands/notify.d.ts +6 -0
  7. package/dist/types/config/file-lock-gc.d.ts +13 -1
  8. package/dist/types/config/file-lock.d.ts +3 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/models-config-schema.d.ts +99 -0
  11. package/dist/types/coordinator/contract.d.ts +1 -1
  12. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  13. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  14. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  15. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  16. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  17. package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
  18. package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
  19. package/dist/types/main.d.ts +6 -2
  20. package/dist/types/modes/acp/acp-agent.d.ts +24 -1
  21. package/dist/types/modes/components/session-selector.d.ts +1 -1
  22. package/dist/types/modes/utils/host-status.d.ts +23 -0
  23. package/dist/types/runtime-mcp/types.d.ts +2 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  25. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  26. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  28. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  29. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  30. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  31. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  32. package/dist/types/sdk/bus/html-format.d.ts +10 -0
  33. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  34. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  35. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  36. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  37. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  38. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +28 -7
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -17
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +20 -1
  42. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  43. package/dist/types/sdk/host/host.d.ts +52 -0
  44. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  45. package/dist/types/sdk/session-activation.d.ts +41 -0
  46. package/dist/types/sdk/session.d.ts +18 -0
  47. package/dist/types/sdk/startup-capability.d.ts +8 -1
  48. package/dist/types/session/agent-session.d.ts +7 -2
  49. package/dist/types/session/artifacts.d.ts +2 -2
  50. package/dist/types/session/session-manager.d.ts +40 -6
  51. package/dist/types/tools/index.d.ts +15 -0
  52. package/dist/types/utils/npm-registry.d.ts +71 -0
  53. package/examples/extensions/README.md +38 -4
  54. package/examples/extensions/ooo-bridge.ts +15 -0
  55. package/package.json +8 -8
  56. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  57. package/src/async/job-manager.ts +9 -2
  58. package/src/cli/fast-help.ts +1 -0
  59. package/src/cli/marketplace-hint.ts +39 -0
  60. package/src/cli/notify-cli.ts +226 -2
  61. package/src/cli/plugin-cli.ts +10 -0
  62. package/src/cli/update-cli.ts +90 -23
  63. package/src/cli.ts +23 -0
  64. package/src/commands/notify.ts +27 -3
  65. package/src/commands/sdk.ts +1 -0
  66. package/src/config/file-lock-gc.ts +65 -26
  67. package/src/config/file-lock.ts +61 -16
  68. package/src/config/model-profiles.ts +46 -30
  69. package/src/config/model-registry.ts +17 -5
  70. package/src/config/models-config-schema.ts +9 -4
  71. package/src/coordinator/contract.ts +1 -0
  72. package/src/coordinator-mcp/question-state.ts +2 -0
  73. package/src/coordinator-mcp/server.ts +358 -7
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  76. package/src/extensibility/extensions/index.ts +1 -0
  77. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  78. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  79. package/src/extensibility/extensions/runner.ts +15 -2
  80. package/src/extensibility/extensions/types.ts +2 -0
  81. package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
  82. package/src/gjc-runtime/deep-interview-state.ts +27 -3
  83. package/src/gjc-runtime/gc-render.ts +8 -1
  84. package/src/gjc-runtime/gc-runtime.ts +15 -1
  85. package/src/gjc-runtime/launch-tmux.ts +53 -1
  86. package/src/internal-urls/docs-index.generated.ts +14 -13
  87. package/src/main.ts +214 -70
  88. package/src/modes/DESIGN.md +8 -5
  89. package/src/modes/acp/acp-agent.ts +123 -7
  90. package/src/modes/components/footer.ts +10 -18
  91. package/src/modes/components/model-selector.ts +52 -5
  92. package/src/modes/components/session-selector.ts +26 -3
  93. package/src/modes/components/status-line/gh.ts +23 -2
  94. package/src/modes/controllers/event-controller.ts +3 -0
  95. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  96. package/src/modes/controllers/input-controller.ts +11 -0
  97. package/src/modes/controllers/selector-controller.ts +8 -2
  98. package/src/modes/interactive-mode.ts +7 -1
  99. package/src/modes/print-mode.ts +10 -0
  100. package/src/modes/utils/host-status.ts +38 -0
  101. package/src/prompts/system/project-prompt.md +19 -0
  102. package/src/runtime-mcp/types.ts +2 -0
  103. package/src/sdk/broker/lifecycle.ts +60 -3
  104. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  105. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  106. package/src/sdk/bus/chat-daemon-control.ts +16 -3
  107. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  108. package/src/sdk/bus/control-plane-frames.ts +51 -0
  109. package/src/sdk/bus/conversation-store.ts +29 -1
  110. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  111. package/src/sdk/bus/html-format.ts +19 -3
  112. package/src/sdk/bus/index.ts +91 -12
  113. package/src/sdk/bus/notification-service.ts +28 -1
  114. package/src/sdk/bus/rich-render.ts +3 -2
  115. package/src/sdk/bus/slack-daemon.ts +99 -4
  116. package/src/sdk/bus/slack-live-provider.ts +32 -2
  117. package/src/sdk/bus/slack-provider.ts +11 -0
  118. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  119. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  120. package/src/sdk/bus/telegram-daemon-contract.ts +28 -7
  121. package/src/sdk/bus/telegram-daemon.ts +2007 -509
  122. package/src/sdk/bus/telegram-reference.ts +114 -15
  123. package/src/sdk/bus/topic-registry.ts +674 -181
  124. package/src/sdk/host/host.ts +180 -1
  125. package/src/sdk/lifecycle-session.ts +8 -2
  126. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  127. package/src/sdk/session-activation.ts +127 -0
  128. package/src/sdk/session.ts +144 -29
  129. package/src/sdk/startup-capability.ts +10 -1
  130. package/src/session/agent-session.ts +220 -13
  131. package/src/session/artifacts.ts +56 -13
  132. package/src/session/internal/managed-session-storage.ts +228 -7
  133. package/src/session/session-manager.ts +225 -60
  134. package/src/setup/provider-presets.json +3 -3
  135. package/src/task/index.ts +92 -44
  136. package/src/thinking-metadata.ts +2 -1
  137. package/src/tools/ask.ts +10 -0
  138. package/src/tools/index.ts +15 -0
  139. package/src/tools/todo-write.ts +4 -4
  140. package/src/utils/npm-registry.ts +660 -0
@@ -13,13 +13,15 @@ import type {
13
13
  GcPruneOutcome,
14
14
  GcRecord,
15
15
  GcStoreAdapter,
16
+ GcWarning,
16
17
  } from "../gjc-runtime/gc-runtime";
17
18
  import { gcPidStatusLabel } from "../gjc-runtime/gc-runtime";
18
19
  import { resolveReceiptSpoolDir } from "../harness-control-plane/receipt-spool";
19
20
  import { readFileLockInfoForGc, removeFileLockDirForGc } from "./file-lock";
20
21
 
21
22
  const MAX_WALK_DEPTH = 6;
22
- const MAX_WALK_ENTRIES = 20_000;
23
+ /** Default per-root walk budget. Truncation is a warning, not a hard error. */
24
+ export const FILE_LOCK_GC_MAX_WALK_ENTRIES = 20_000;
23
25
 
24
26
  // High-cardinality, lock-free subtrees we never descend into. `.lock` dirs are
25
27
  // created next to config files, never inside these.
@@ -30,6 +32,11 @@ interface WalkState {
30
32
  truncated: boolean;
31
33
  }
32
34
 
35
+ export interface FileLocksGcCollectOptions {
36
+ /** Override per-root entry budget (tests). Defaults to {@link FILE_LOCK_GC_MAX_WALK_ENTRIES}. */
37
+ maxWalkEntries?: number;
38
+ }
39
+
33
40
  // Global, env-aware GJC lock roots. Per the approved scope this covers the
34
41
  // user config root, the agent dir (honors GJC_CODING_AGENT_DIR), and the
35
42
  // configured receipt-spool dir — NOT the invocation cwd's project `.gjc`.
@@ -61,6 +68,21 @@ function keptMalformedRecord(lockDir: string): GcRecord {
61
68
  async function collectLockRecord(lockDir: string, ctx: GcContext): Promise<GcRecord> {
62
69
  const info = await readFileLockInfoForGc(lockDir);
63
70
  if (!info) return keptMalformedRecord(lockDir);
71
+ if (info.owner_host_id !== undefined) {
72
+ return {
73
+ store: "file_locks",
74
+ id: lockDir,
75
+ path: lockDir,
76
+ pid: info.pid,
77
+ pid_status: "unknown",
78
+ status: "host_qualified",
79
+ stale: false,
80
+ removable: false,
81
+ action: "none",
82
+ reason: "host_qualified_lock_requires_owner_reclamation",
83
+ detail: `timestamp=${info.timestamp}`,
84
+ };
85
+ }
64
86
 
65
87
  const probeResult = ctx.probe(info.pid);
66
88
  const pidStatus = gcPidStatusLabel(probeResult);
@@ -87,8 +109,9 @@ async function walkForLockDirs(
87
109
  state: WalkState,
88
110
  lockDirs: Set<string>,
89
111
  errors: GcError[],
112
+ maxWalkEntries: number,
90
113
  ): Promise<void> {
91
- if (state.entries >= MAX_WALK_ENTRIES) {
114
+ if (state.entries >= maxWalkEntries) {
92
115
  state.truncated = true;
93
116
  return;
94
117
  }
@@ -122,50 +145,66 @@ async function walkForLockDirs(
122
145
  }
123
146
 
124
147
  for (const entry of entries) {
125
- if (state.entries >= MAX_WALK_ENTRIES) {
148
+ if (state.entries >= maxWalkEntries) {
126
149
  state.truncated = true;
127
150
  return;
128
151
  }
129
152
  if (PRUNED_DIR_NAMES.has(entry)) continue;
130
- await walkForLockDirs(path.join(dir, entry), depth + 1, state, lockDirs, errors);
153
+ await walkForLockDirs(path.join(dir, entry), depth + 1, state, lockDirs, errors, maxWalkEntries);
131
154
  }
132
155
  }
133
156
 
134
- export const fileLocksGcAdapter: GcStoreAdapter = {
135
- store: "file_locks",
136
- async collect(ctx: GcContext): Promise<GcCollectResult> {
137
- const records: GcRecord[] = [];
138
- const errors: GcError[] = [];
139
- const lockDirs = new Set<string>();
157
+ /**
158
+ * Discover + classify file-lock records. Each known lock root gets its own walk
159
+ * budget so truncating one root never skips the others. Caps surface as
160
+ * warnings (partial results), not hard discovery errors.
161
+ */
162
+ export async function collectFileLocksForGc(
163
+ ctx: GcContext,
164
+ options: FileLocksGcCollectOptions = {},
165
+ ): Promise<GcCollectResult> {
166
+ const maxWalkEntries = options.maxWalkEntries ?? FILE_LOCK_GC_MAX_WALK_ENTRIES;
167
+ const records: GcRecord[] = [];
168
+ const errors: GcError[] = [];
169
+ const warnings: GcWarning[] = [];
170
+ const lockDirs = new Set<string>();
171
+
172
+ for (const root of knownFileLockRoots(ctx)) {
173
+ // Fresh budget per root so a huge agent dir cannot starve config/spool.
140
174
  const state: WalkState = { entries: 0, truncated: false };
141
-
142
- for (const root of knownFileLockRoots(ctx)) {
143
- await walkForLockDirs(root, 0, state, lockDirs, errors);
144
- if (state.truncated) break;
145
- }
146
-
175
+ await walkForLockDirs(root, 0, state, lockDirs, errors, maxWalkEntries);
147
176
  if (state.truncated) {
148
- errors.push({
177
+ warnings.push({
149
178
  store: "file_locks",
150
- scope: "discovery",
151
- message: `file lock discovery capped at ${MAX_WALK_ENTRIES} entries`,
179
+ scope: root,
180
+ message: `file lock discovery capped at ${maxWalkEntries} entries for root ${root} (scanned ${state.entries})`,
152
181
  });
153
182
  }
183
+ }
154
184
 
155
- for (const lockDir of lockDirs) {
156
- try {
157
- records.push(await collectLockRecord(lockDir, ctx));
158
- } catch (error) {
159
- errors.push({ store: "file_locks", scope: lockDir, message: errorMessage(error) });
160
- }
185
+ for (const lockDir of lockDirs) {
186
+ try {
187
+ records.push(await collectLockRecord(lockDir, ctx));
188
+ } catch (error) {
189
+ errors.push({ store: "file_locks", scope: lockDir, message: errorMessage(error) });
161
190
  }
191
+ }
162
192
 
163
- return { records, errors };
193
+ return { records, errors, warnings };
194
+ }
195
+
196
+ export const fileLocksGcAdapter: GcStoreAdapter = {
197
+ store: "file_locks",
198
+ async collect(ctx: GcContext): Promise<GcCollectResult> {
199
+ return collectFileLocksForGc(ctx);
164
200
  },
165
201
  async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
166
202
  const lockDir = record.path ?? record.id;
167
203
  const info = await readFileLockInfoForGc(lockDir);
168
204
  if (!info) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
205
+ if (info.owner_host_id !== undefined) {
206
+ return { removed: false, skipped: "host_qualified_lock_requires_owner_reclamation" };
207
+ }
169
208
 
170
209
  const probeResult = ctx.probe(info.pid);
171
210
  if (probeResult.status !== "dead") {
@@ -1,3 +1,4 @@
1
+ import * as crypto from "node:crypto";
1
2
  import type { Stats } from "node:fs";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as path from "node:path";
@@ -7,9 +8,11 @@ export interface FileLockOptions {
7
8
  staleMs?: number;
8
9
  retries?: number;
9
10
  retryDelayMs?: number;
11
+ /** Stable host identity required to safely reclaim locks on a shared volume. */
12
+ ownerHostId?: string;
10
13
  }
11
14
 
12
- const DEFAULT_OPTIONS: Required<FileLockOptions> = {
15
+ const DEFAULT_OPTIONS: Required<Omit<FileLockOptions, "ownerHostId">> = {
13
16
  staleMs: 10_000,
14
17
  retries: 50,
15
18
  retryDelayMs: 100,
@@ -61,8 +64,16 @@ function ownerIsAlive(owner: FileLockOwnerToken, startTimeCache?: Map<string, st
61
64
  return currentStartTime === null || currentStartTime === owner.start_time;
62
65
  }
63
66
 
64
- function writeLockInfo(lockPath: string): Promise<LockInfo> {
65
- const info: LockInfo = { pid: process.pid, start_time: currentProcessStartTime(), timestamp: Date.now() };
67
+ function lockInfo(ownerHostId?: string): LockInfo {
68
+ return {
69
+ pid: process.pid,
70
+ start_time: currentProcessStartTime(),
71
+ timestamp: Date.now(),
72
+ ...(ownerHostId === undefined ? {} : { owner_host_id: ownerHostId }),
73
+ };
74
+ }
75
+
76
+ function writeLockInfo(lockPath: string, info: LockInfo): Promise<LockInfo> {
66
77
  return Bun.write(`${lockPath}/info`, JSON.stringify(info)).then(() => info);
67
78
  }
68
79
 
@@ -76,17 +87,18 @@ async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
76
87
  }
77
88
 
78
89
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
79
- const { pid, start_time, timestamp } = parsed as Partial<LockInfo>;
90
+ const { pid, start_time, timestamp, owner_host_id } = parsed as Partial<LockInfo>;
80
91
  if (
81
92
  typeof pid !== "number" ||
82
93
  !Number.isInteger(pid) ||
83
94
  pid <= 0 ||
84
95
  typeof timestamp !== "number" ||
85
96
  !Number.isFinite(timestamp) ||
86
- (start_time !== undefined && (typeof start_time !== "string" || !start_time))
97
+ (start_time !== undefined && (typeof start_time !== "string" || !start_time)) ||
98
+ (owner_host_id !== undefined && (typeof owner_host_id !== "string" || !owner_host_id))
87
99
  )
88
100
  return null;
89
- return { pid, start_time, timestamp };
101
+ return { pid, start_time, timestamp, owner_host_id };
90
102
  }
91
103
 
92
104
  /** @internal */
@@ -98,7 +110,7 @@ export async function readFileLockInfoForGc(lockDir: string): Promise<FileLockOw
98
110
  export interface FileLockOwnerToken {
99
111
  pid: number;
100
112
  start_time?: string;
101
-
113
+ owner_host_id?: string;
102
114
  timestamp: number;
103
115
  }
104
116
 
@@ -147,6 +159,7 @@ export async function removeFileLockDirForGc(
147
159
  if (
148
160
  current.pid !== expected.pid ||
149
161
  (expected.start_time !== undefined && current.start_time !== expected.start_time) ||
162
+ current.owner_host_id !== expected.owner_host_id ||
150
163
  current.timestamp !== expected.timestamp
151
164
  ) {
152
165
  return "owner_changed";
@@ -187,6 +200,7 @@ function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
187
200
  async function staleLockSnapshot(
188
201
  lockPath: string,
189
202
  staleMs: number,
203
+ ownerHostId?: string,
190
204
  startTimeCache?: Map<string, string | null>,
191
205
  ): Promise<LockStaleSnapshot> {
192
206
  let info: LockInfo | null;
@@ -199,6 +213,7 @@ async function staleLockSnapshot(
199
213
  if (hasFsCode(error, "EPERM")) return { stale: false };
200
214
  throw error;
201
215
  }
216
+ if (!info && ownerHostId !== undefined) return { stale: false };
202
217
  if (!info) {
203
218
  try {
204
219
  const stats = await fs.stat(lockPath);
@@ -210,6 +225,10 @@ async function staleLockSnapshot(
210
225
  }
211
226
  }
212
227
 
228
+ // A host-qualified lock may only be reclaimed after proving that its owner is
229
+ // local. Foreign and malformed host-qualified records fail closed: PID values
230
+ // and clocks are not meaningful across hosts.
231
+ if (ownerHostId !== undefined && info.owner_host_id !== ownerHostId) return { stale: false };
213
232
  // Never reap a live owner by elapsed time: a long legitimate critical section must
214
233
  // not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
215
234
  // liveness is indeterminate do we fall back to the staleMs elapsed-time heuristic.
@@ -239,18 +258,43 @@ async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSn
239
258
  }
240
259
  }
241
260
 
242
- async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
261
+ async function tryAcquireLock(lockPath: string, ownerHostId?: string): Promise<LockInfo | null> {
243
262
  await fs.mkdir(path.dirname(lockPath), { recursive: true });
244
263
  const afterParentMkdir = FileLockTestHooks.afterParentMkdir;
245
264
  if (afterParentMkdir) await afterParentMkdir(lockPath);
265
+ if (ownerHostId === undefined) {
266
+ try {
267
+ await fs.mkdir(lockPath);
268
+ return await writeLockInfo(lockPath, lockInfo());
269
+ } catch (error) {
270
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") return null;
271
+ throw error;
272
+ }
273
+ }
274
+
275
+ const pendingPath = `${lockPath}.pending.${process.pid}.${crypto.randomUUID()}`;
276
+ const owner = lockInfo(ownerHostId);
246
277
  try {
247
- await fs.mkdir(lockPath);
248
- return await writeLockInfo(lockPath);
249
- } catch (error) {
250
- if ((error as NodeJS.ErrnoException).code === "EEXIST") {
251
- return null;
278
+ await fs.mkdir(pendingPath);
279
+ await writeLockInfo(pendingPath, owner);
280
+ try {
281
+ await fs.rename(pendingPath, lockPath);
282
+ return owner;
283
+ } catch (error) {
284
+ const code = (error as NodeJS.ErrnoException).code;
285
+ if (code === "EEXIST" || code === "ENOTEMPTY") return null;
286
+ if (code === "EPERM") {
287
+ try {
288
+ await fs.stat(lockPath);
289
+ return null;
290
+ } catch (statError) {
291
+ if (!isEnoent(statError)) throw statError;
292
+ }
293
+ }
294
+ throw error;
252
295
  }
253
- throw error;
296
+ } finally {
297
+ await fs.rm(pendingPath, { recursive: true, force: true }).catch(() => undefined);
254
298
  }
255
299
  }
256
300
 
@@ -259,14 +303,15 @@ async function releaseLock(lockPath: string, owner: FileLockOwnerToken): Promise
259
303
  if (outcome !== "removed") throw new Error(`Failed to release file lock: ${outcome}.`);
260
304
  }
261
305
  async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
306
+ if (options.ownerHostId !== undefined && !options.ownerHostId) throw new Error("ownerHostId must be non-empty");
262
307
  const opts = { ...DEFAULT_OPTIONS, ...options };
263
308
  const lockPath = getLockPath(filePath);
264
309
  const contentionStartTimes = new Map<string, string | null>();
265
310
  for (let attempt = 0; attempt < opts.retries; attempt++) {
266
- const owner = await tryAcquireLock(lockPath);
311
+ const owner = await tryAcquireLock(lockPath, opts.ownerHostId);
267
312
  if (owner) return () => releaseLock(lockPath, owner);
268
313
 
269
- const stale = await staleLockSnapshot(lockPath, opts.staleMs, contentionStartTimes);
314
+ const stale = await staleLockSnapshot(lockPath, opts.staleMs, opts.ownerHostId, contentionStartTimes);
270
315
  if (await removeStaleLockForAcquire(lockPath, stale)) continue;
271
316
  await Bun.sleep(opts.retryDelayMs);
272
317
  }
@@ -243,46 +243,46 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
243
243
  architect: "grok-build/grok-build",
244
244
  }),
245
245
  profile("cursor-eco", ["cursor"], {
246
- default: "cursor/composer-1.5:low",
247
- executor: "cursor/composer-1.5:minimal",
248
- planner: "cursor/composer-1.5:low",
249
- critic: "cursor/composer-1.5:medium",
250
- architect: "cursor/composer-1.5:high",
246
+ default: "cursor/composer-2.5",
247
+ executor: "cursor/composer-2.5",
248
+ planner: "cursor/composer-2.5",
249
+ critic: "cursor/composer-2.5",
250
+ architect: "cursor/composer-2.5",
251
251
  }),
252
252
  profile("cursor-medium", ["cursor"], {
253
- default: "cursor/composer-1.5:medium",
254
- executor: "cursor/composer-1.5:low",
255
- planner: "cursor/composer-1.5:medium",
256
- critic: "cursor/composer-1.5:high",
257
- architect: "cursor/composer-1.5:xhigh",
253
+ default: "cursor/composer-2.5",
254
+ executor: "cursor/composer-2.5-fast",
255
+ planner: "cursor/composer-2.5",
256
+ critic: "cursor/composer-2.5-fast",
257
+ architect: "cursor/composer-2.5-fast",
258
258
  }),
259
259
  profile("cursor-pro", ["cursor"], {
260
- default: "cursor/composer-1.5:xhigh",
261
- executor: "cursor/composer-1.5:medium",
262
- planner: "cursor/composer-1.5:high",
263
- critic: "cursor/composer-1.5:xhigh",
264
- architect: "cursor/composer-1.5:xhigh",
260
+ default: "cursor/composer-2.5-fast",
261
+ executor: "cursor/composer-2.5-fast",
262
+ planner: "cursor/composer-2.5-fast",
263
+ critic: "cursor/composer-2.5-fast",
264
+ architect: "cursor/composer-2.5-fast",
265
265
  }),
266
266
  profile("minimax-eco", ["minimax-code"], {
267
- default: "minimax-code/minimax-m3:low",
268
- executor: "minimax-code/minimax-m3:minimal",
269
- planner: "minimax-code/minimax-m3:low",
270
- critic: "minimax-code/minimax-m3:medium",
271
- architect: "minimax-code/minimax-m3:high",
267
+ default: "minimax-code/MiniMax-M3:low",
268
+ executor: "minimax-code/MiniMax-M3:minimal",
269
+ planner: "minimax-code/MiniMax-M3:low",
270
+ critic: "minimax-code/MiniMax-M3:medium",
271
+ architect: "minimax-code/MiniMax-M3:high",
272
272
  }),
273
273
  profile("minimax-medium", ["minimax-code"], {
274
- default: "minimax-code/minimax-m3:medium",
275
- executor: "minimax-code/minimax-m3:low",
276
- planner: "minimax-code/minimax-m3:medium",
277
- critic: "minimax-code/minimax-m3:high",
278
- architect: "minimax-code/minimax-m3:xhigh",
274
+ default: "minimax-code/MiniMax-M3:medium",
275
+ executor: "minimax-code/MiniMax-M3:low",
276
+ planner: "minimax-code/MiniMax-M3:medium",
277
+ critic: "minimax-code/MiniMax-M3:high",
278
+ architect: "minimax-code/MiniMax-M3:xhigh",
279
279
  }),
280
280
  profile("minimax-pro", ["minimax-code"], {
281
- default: "minimax-code/minimax-m3:xhigh",
282
- executor: "minimax-code/minimax-m3:medium",
283
- planner: "minimax-code/minimax-m3:high",
284
- critic: "minimax-code/minimax-m3:xhigh",
285
- architect: "minimax-code/minimax-m3:xhigh",
281
+ default: "minimax-code/MiniMax-M3:xhigh",
282
+ executor: "minimax-code/MiniMax-M3:medium",
283
+ planner: "minimax-code/MiniMax-M3:high",
284
+ critic: "minimax-code/MiniMax-M3:xhigh",
285
+ architect: "minimax-code/MiniMax-M3:xhigh",
286
286
  }),
287
287
  profile("alibaba-token-plan-balanced", ["alibaba-token-plan"], {
288
288
  default: "alibaba-token-plan/qwen3.8-max-preview:medium",
@@ -305,6 +305,20 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
305
305
  architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
306
306
  critic: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
307
307
  }),
308
+ profile("alibaba-token-plan-qwen-deepseek", ["alibaba-token-plan"], {
309
+ default: "alibaba-token-plan/qwen3.8-max:high",
310
+ executor: "alibaba-token-plan/deepseek-v4-flash-0731:high",
311
+ planner: "alibaba-token-plan/deepseek-v4-flash-0731:max",
312
+ architect: "alibaba-token-plan/qwen3.8-max:xhigh",
313
+ critic: "alibaba-token-plan/qwen3.8-max:xhigh",
314
+ }),
315
+ profile("alibaba-token-plan-glm-deepseek", ["alibaba-token-plan"], {
316
+ default: "alibaba-token-plan/glm-5.2:high",
317
+ executor: "alibaba-token-plan/deepseek-v4-flash-0731:high",
318
+ planner: "alibaba-token-plan/deepseek-v4-flash-0731:max",
319
+ architect: "alibaba-token-plan/glm-5.2:xhigh",
320
+ critic: "alibaba-token-plan/glm-5.2:xhigh",
321
+ }),
308
322
  profile("opus-codex", ["anthropic", "openai-codex"], {
309
323
  default: "anthropic/claude-opus-5:xhigh",
310
324
  executor: "openai-codex/gpt-5.6-terra:low",
@@ -370,6 +384,8 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
370
384
  "alibaba-token-plan-balanced": { displayName: "Balanced", providerGroup: "ALIBABA TOKEN PLAN" },
371
385
  "alibaba-token-plan-pro": { displayName: "Pro", providerGroup: "ALIBABA TOKEN PLAN" },
372
386
  "alibaba-token-plan-qwenmaxxing": { displayName: "QwenMaxxing", providerGroup: "ALIBABA TOKEN PLAN" },
387
+ "alibaba-token-plan-qwen-deepseek": { displayName: "Qwen + DeepSeek", providerGroup: "ALIBABA TOKEN PLAN" },
388
+ "alibaba-token-plan-glm-deepseek": { displayName: "GLM + DeepSeek", providerGroup: "ALIBABA TOKEN PLAN" },
373
389
  "opus-codex": { displayName: "Opus + Codex", providerGroup: "COMBOS" },
374
390
  "codex-opencodego": { displayName: "Codex + OpenCodeGo", providerGroup: "COMBOS" },
375
391
  "fable-opus-codex": { displayName: "Fable + Opus + Codex", providerGroup: "COMBOS" },
@@ -926,12 +926,24 @@ const customReferenceMap = buildCustomReferenceMap();
926
926
 
927
927
  function getCustomReferenceCandidateIds(modelId: string): string[] {
928
928
  const candidates = new Set<string>();
929
- const minimaxM = /^minimax-m(\d+(?:\.\d+)*)$/i.exec(modelId.trim());
930
- const queue = minimaxM ? [`MiniMax-M${minimaxM[1]}`, modelId] : [modelId];
929
+ const trimmedId = modelId.trim();
930
+ const minimaxM = /^minimax-m(\d+(?:\.\d+)*)$/i.exec(trimmedId);
931
+ const queue = minimaxM ? [`MiniMax-M${minimaxM[1]}`, trimmedId] : [trimmedId];
931
932
  if (minimaxM) {
932
- // MiniMax catalogs include lowercase wire ids plus display-cased aliases.
933
- // Custom providers should keep the lowercase wire id while inheriting the
934
- // canonical display casing when the alias exists.
933
+ // First-class MiniMax catalogs expose canonical `MiniMax-M*` ids only,
934
+ // but custom providers may still use lowercase wire ids. Normalize to
935
+ // the canonical display casing so metadata inheritance keeps working.
936
+ }
937
+ // Namespaced wire IDs (e.g. `cline-pass/deepseek-v4-flash`) keep the full id for
938
+ // the API request, but should still try the leaf segment against bundled
939
+ // references so capability metadata is not silently replaced by 128K/16K defaults.
940
+ // Only an exact leaf match in the reference map inherits; unknown leaves stay defaulted.
941
+ const slashIndex = trimmedId.lastIndexOf("/");
942
+ if (slashIndex >= 0 && slashIndex < trimmedId.length - 1) {
943
+ const leafId = trimmedId.slice(slashIndex + 1).trim();
944
+ if (leafId && leafId !== trimmedId) {
945
+ queue.push(leafId);
946
+ }
935
947
  }
936
948
  for (let index = 0; index < queue.length; index += 1) {
937
949
  const candidate = queue[index]?.trim();
@@ -20,7 +20,7 @@ const ReasoningEffortMapSchema = z.object({
20
20
  max: z.string().optional(),
21
21
  });
22
22
 
23
- export const OpenAICompatSchema = z.object({
23
+ export const ModelCompatSchema = z.object({
24
24
  supportsStore: z.boolean().optional(),
25
25
  supportsDeveloperRole: z.boolean().optional(),
26
26
  sendSessionHeaders: z.boolean().optional(),
@@ -48,8 +48,13 @@ export const OpenAICompatSchema = z.object({
48
48
  extraBody: z.record(z.string(), z.unknown()).optional(),
49
49
  supportsStrictMode: z.boolean().optional(),
50
50
  toolStrictMode: z.enum(["all_strict", "none"]).optional(),
51
+ supportsLongCacheRetention: z.boolean().optional(),
52
+ promptCacheMode: z.enum(["none", "explicit", "automatic"]).optional(),
51
53
  });
52
54
 
55
+ // Backward-compatible export for callers that imported the original schema name.
56
+ export const OpenAICompatSchema = ModelCompatSchema;
57
+
53
58
  export const GJC_MODEL_EFFORT_IDS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
54
59
  export const GJC_MODEL_ASSIGNMENT_TARGET_IDS = ["default", "executor", "architect", "planner", "critic"] as const;
55
60
  export const EffortSchema = z.enum(GJC_MODEL_EFFORT_IDS);
@@ -146,7 +151,7 @@ const ModelDefinitionSchema = z
146
151
  contextWindow: z.number().optional(),
147
152
  maxTokens: z.number().optional(),
148
153
  headers: z.record(z.string(), z.string()).optional(),
149
- compat: OpenAICompatSchema.optional(),
154
+ compat: ModelCompatSchema.optional(),
150
155
  contextPromotionTarget: z.string().min(1).optional(),
151
156
  wireModelId: z.string().min(1).optional(),
152
157
  requestTransform: RequestTransformSchema.optional(),
@@ -173,7 +178,7 @@ export const ModelOverrideSchema = z
173
178
  contextWindow: z.number().optional(),
174
179
  maxTokens: z.number().optional(),
175
180
  headers: z.record(z.string(), z.string()).optional(),
176
- compat: OpenAICompatSchema.optional(),
181
+ compat: ModelCompatSchema.optional(),
177
182
  contextPromotionTarget: z.string().min(1).optional(),
178
183
  wireModelId: z.string().min(1).optional(),
179
184
  requestTransform: RequestTransformSchema.optional(),
@@ -221,7 +226,7 @@ const ProviderConfigSchema = z
221
226
  ])
222
227
  .optional(),
223
228
  headers: z.record(z.string(), z.string()).optional(),
224
- compat: OpenAICompatSchema.optional(),
229
+ compat: ModelCompatSchema.optional(),
225
230
  webSearch: z.enum(["on", "off", "auto"]).optional(),
226
231
  authHeader: z.boolean().optional(),
227
232
  auth: ProviderAuthSchema.optional(),
@@ -12,6 +12,7 @@ export const COORDINATOR_MCP_TOOL_NAMES = [
12
12
  "gjc_coordinator_watch_events",
13
13
  "gjc_coordinator_register_session",
14
14
  "gjc_coordinator_start_session",
15
+ "gjc_coordinator_activate_session",
15
16
  "gjc_coordinator_stop_session",
16
17
  "gjc_coordinator_send_prompt",
17
18
  "gjc_coordinator_submit_question_answer",
@@ -6,6 +6,8 @@ import type { PrivateAskGateCodecV1, PublicReason } from "./question-gate-codec"
6
6
 
7
7
  export type CoordinatorSessionState =
8
8
  | "booting"
9
+ /** Live and endpoint-addressable, but withholding readiness until activation. */
10
+ | "prepared"
9
11
  | "ready_for_input"
10
12
  | "running"
11
13
  | "needs_user_input"