@gajae-code/coding-agent 0.12.16 → 0.12.19

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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.19] - 2026-08-08
6
+
7
+ ### Fixed
8
+
9
+ - Telegram topic-registry archive transitions remain parser-valid across durable-fence load promotion and failed archive-publication rollback. The initial generation-57 fix cleared `disconnectGraceExpiresAt` on direct archive transitions, but loading a `disconnect_grace` record superseded by a newer durable fence could still promote it to `archive_pending` while retaining the grace-only field, and rollback could restore `disconnect_grace` without restoring its required deadline. Both paths now preserve the state/field invariant with parser-roundtrip coverage. This entry also corrects the hotfix attribution omitted from the published 0.12.18 release notes. `DAEMON_GENERATION` bumped to 58.
10
+
11
+ ## [0.12.18] - 2026-08-08
12
+
13
+ ### Fixed
14
+
15
+ - SDK snapshot spill writes now retry once with a fresh temporary file when Bun reports `EBADF` during write or `fsync` under heavily contended descriptor teardown, and no longer fail when `close()` reports `EBADF` after a successful write and sync. The atomic writer removes the abandoned attempt before retrying, preserves primary I/O failures, accepts only the proven already-closed close case, and keeps every non-`EBADF` error or repeated descriptor failure fatal.
16
+ - ACP now imports the synthetic model-profile provider namespace from a dependency-free SDK constant module, keeping the machine entrypoint out of model-registry, session-host, MCP-manager, and extension-runner closures enforced by `check:runtime`.
17
+
18
+ ## [0.12.17] - 2026-08-08
19
+
5
20
  ## [0.12.16] - 2026-08-08
6
21
 
7
22
  ### Added
@@ -39,6 +54,7 @@
39
54
  - `sticky-viewport-showcase.test.ts` runs in ~53s instead of ~192s, cutting the slowest CI test shard from ~369s to ~167s locally. Each of its 17 cases spawned a `bun` subprocess that re-rendered all 20 showcase frames (~9.4s per capture, ~187s of the file's ~192s) purely to obtain a pristine bundle it then corrupted. The bundle is now captured once and handed out as filesystem copies; the one case that must observe mutated `GJC_STICKY_VIEWPORT_ORACLE_COMMIT` state still captures for itself. Isolation is load-bearing and verified: sharing the directory instead of copying it fails 9 cases.
40
55
 
41
56
  ### Fixed
57
+ - Telegram topic archival no longer corrupts the shared topic registry when an active topic first enters disconnect grace. `beginArchive` changed `authorityState` to `archive_pending` but retained `disconnectGraceExpiresAt`, a field the persisted-state parser permits only for `disconnect_grace`; one closed or stale session therefore made the entire otherwise-valid registry unreadable, after which every daemon scan reported `shared topic authority unavailable` and no new session could create a thread or replay a message. Every transition out of disconnect grace now clears the grace-only deadline before persistence. `DAEMON_GENERATION` bumped to 57.
42
58
 
43
59
  - `notifications-topic-registry.test.ts` pins `DAEMON_GENERATION` at 54 after #3965 (was stale at 53; Dev CI run 31133356543).
44
60
  - `smithery-env-trust.test.ts` no longer awaits hung probe pipes past a hard per-attempt deadline: minimal child env, `stdin: "ignore"`, SIGKILL + settled race, and up to 2 timeout-only retries. Fixes the inherited-config case that hit exactly 60001ms on Dev CI run 31133356543 after kill-at-45s left `Promise.all` on stdout/stderr unresolved. Assertions unchanged.
@@ -85,9 +85,14 @@ export declare const NOTIFICATION_PROTOCOL_VERSION = 3;
85
85
  * of escaping to the process-level fatal handler, authority-failure throws
86
86
  * preserve their underlying cause, and the compensation fence retry is bounded.
87
87
  * Generation 56 moves exact unlink and process-incarnation authority behind
88
- * lazy native bindings for the startup-cost cut (#3846).
88
+ * lazy native bindings for the startup-cost cut (#3846). Generation 57 clears
89
+ * disconnect-grace-only fields whenever a topic enters an archive state, so
90
+ * one stale session cannot make the shared registry unreadable and block every
91
+ * later session from creating a topic or replaying notifications. Generation
92
+ * 58 preserves that invariant during durable-fence load promotion and restores
93
+ * the exact grace deadline when a failed archive publication rolls back.
89
94
  */
90
- export declare const DAEMON_GENERATION = 56;
95
+ export declare const DAEMON_GENERATION = 58;
91
96
  /**
92
97
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 5
93
98
  * requires the complete generation-36 topic authority contract, so older
@@ -1,14 +1,5 @@
1
1
  import type { ModelProfileDefinition } from "../config/model-profiles";
2
- /**
3
- * Reserved logical provider namespace for the preset-as-model facade.
4
- *
5
- * `gajae-code/<profile>` is not a real API provider: rows under this namespace
6
- * exist only in the SDK Q10 projection and `model.set` selection routes to a
7
- * session-scoped model-profile activation transaction. The namespace is
8
- * reserved so a user-defined `models.yml` provider of the same name cannot
9
- * silently shadow the facade (see {@link syntheticNamespaceCollision}).
10
- */
11
- export declare const SYNTHETIC_PROVIDER_ID = "gajae-code";
2
+ export { SYNTHETIC_PROVIDER_ID } from "./model-profile-namespace";
12
3
  /**
13
4
  * Display metadata used for synthetic rows whose profile default model cannot
14
5
  * be resolved to a registered model. Mirrors the shared unknown-model
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reserved logical provider namespace for the preset-as-model facade.
3
+ *
4
+ * Kept dependency-free because machine entrypoints use the namespace when
5
+ * filtering projected models and must not pull the model registry/session host
6
+ * graph into their static import closure.
7
+ */
8
+ export declare const SYNTHETIC_PROVIDER_ID = "gajae-code";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.12.16",
4
+ "version": "0.12.19",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -54,13 +54,13 @@
54
54
  "@agentclientprotocol/sdk": "1.3.0",
55
55
  "@babel/parser": "^7.29.3",
56
56
  "@mozilla/readability": "^0.6.0",
57
- "@gajae-code/stats": "0.12.16",
58
- "@gajae-code/agent-core": "0.12.16",
59
- "@gajae-code/ai": "0.12.16",
60
- "@gajae-code/bridge-client": "0.12.16",
61
- "@gajae-code/natives": "0.12.16",
62
- "@gajae-code/tui": "0.12.16",
63
- "@gajae-code/utils": "0.12.16",
57
+ "@gajae-code/stats": "0.12.19",
58
+ "@gajae-code/agent-core": "0.12.19",
59
+ "@gajae-code/ai": "0.12.19",
60
+ "@gajae-code/bridge-client": "0.12.19",
61
+ "@gajae-code/natives": "0.12.19",
62
+ "@gajae-code/tui": "0.12.19",
63
+ "@gajae-code/utils": "0.12.19",
64
64
  "@puppeteer/browsers": "^2.13.0",
65
65
  "@types/turndown": "5.0.6",
66
66
  "@xterm/headless": "^6.0.0",
@@ -48,7 +48,7 @@ import { resolveAcpFinalText } from "../../sdk/acp/final-text";
48
48
  import { ACP_MCP_LIFECYCLE_TIMEOUT_MS, type SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
49
49
  import { ensureBroker } from "../../sdk/broker/ensure";
50
50
  import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
51
- import { SYNTHETIC_PROVIDER_ID } from "../../sdk/model-profile-model";
51
+ import { SYNTHETIC_PROVIDER_ID } from "../../sdk/model-profile-namespace";
52
52
  import type { SdkPromptTerminalOutcome } from "../../sdk/prompt-status";
53
53
  import {
54
54
  buildToolCallStartUpdate,
@@ -87,9 +87,14 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
87
87
  * of escaping to the process-level fatal handler, authority-failure throws
88
88
  * preserve their underlying cause, and the compensation fence retry is bounded.
89
89
  * Generation 56 moves exact unlink and process-incarnation authority behind
90
- * lazy native bindings for the startup-cost cut (#3846).
90
+ * lazy native bindings for the startup-cost cut (#3846). Generation 57 clears
91
+ * disconnect-grace-only fields whenever a topic enters an archive state, so
92
+ * one stale session cannot make the shared registry unreadable and block every
93
+ * later session from creating a topic or replaying notifications. Generation
94
+ * 58 preserves that invariant during durable-fence load promotion and restores
95
+ * the exact grace deadline when a failed archive publication rolls back.
91
96
  */
92
- export const DAEMON_GENERATION = 56;
97
+ export const DAEMON_GENERATION = 58;
93
98
 
94
99
  /**
95
100
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 5
@@ -578,7 +578,10 @@ export class TopicRegistry {
578
578
  : {}),
579
579
  ...(isValidBindingString(raw.archiveHostId) ? { archiveHostId: raw.archiveHostId } : {}),
580
580
  ...(isValidBindingGeneration(raw.archiveLeaseEpoch) ? { archiveLeaseEpoch: raw.archiveLeaseEpoch } : {}),
581
- ...(typeof raw.disconnectGraceExpiresAt === "number" && Number.isFinite(raw.disconnectGraceExpiresAt)
581
+ ...(raw.authorityState === "disconnect_grace" &&
582
+ !fenceSupersedesRecord &&
583
+ typeof raw.disconnectGraceExpiresAt === "number" &&
584
+ Number.isFinite(raw.disconnectGraceExpiresAt)
582
585
  ? { disconnectGraceExpiresAt: raw.disconnectGraceExpiresAt }
583
586
  : {}),
584
587
  ...(bindingMalformed ? { bindingMalformed: true as const } : {}),
@@ -1179,6 +1182,9 @@ export class TopicRegistry {
1179
1182
  } else {
1180
1183
  record.authorityEpoch = snapshot.authorityEpoch;
1181
1184
  record.authorityState = snapshot.authorityState;
1185
+ if (snapshot.authorityState === "disconnect_grace" && snapshot.record?.disconnectGraceExpiresAt !== undefined)
1186
+ record.disconnectGraceExpiresAt = snapshot.record.disconnectGraceExpiresAt;
1187
+ else delete record.disconnectGraceExpiresAt;
1182
1188
  this.rebuildInboundRoutes();
1183
1189
  }
1184
1190
  if (snapshot.fenceEpoch === undefined) this.epochs.delete(snapshot.sessionId);
@@ -1202,11 +1208,14 @@ export class TopicRegistry {
1202
1208
  authorityEpoch: deleteEpoch,
1203
1209
  authorityState: "archive_pending",
1204
1210
  });
1211
+ const restored = this.topics.get(snapshot.sessionId);
1212
+ if (restored) delete restored.disconnectGraceExpiresAt;
1205
1213
  } else if (record.topicId !== snapshot.topicId) {
1206
1214
  return false;
1207
1215
  } else {
1208
1216
  record.authorityEpoch = deleteEpoch;
1209
1217
  record.authorityState = "archive_pending";
1218
+ delete record.disconnectGraceExpiresAt;
1210
1219
  if (this.byTopic.get(record.topicId) === snapshot.sessionId) this.byTopic.delete(record.topicId);
1211
1220
  }
1212
1221
  this.epochs.set(snapshot.sessionId, deleteEpoch);
@@ -1230,6 +1239,7 @@ export class TopicRegistry {
1230
1239
  if (record) {
1231
1240
  record.authorityEpoch = Number.MAX_SAFE_INTEGER;
1232
1241
  record.authorityState = "archive_exhausted";
1242
+ delete record.disconnectGraceExpiresAt;
1233
1243
  if (this.byTopic.get(record.topicId) === sessionId) this.byTopic.delete(record.topicId);
1234
1244
  }
1235
1245
  return undefined;
@@ -1239,6 +1249,7 @@ export class TopicRegistry {
1239
1249
  if (!record) return undefined;
1240
1250
  record.authorityEpoch = epoch;
1241
1251
  record.authorityState = "archive_pending";
1252
+ delete record.disconnectGraceExpiresAt;
1242
1253
  if (hostId) record.archiveHostId = hostId;
1243
1254
  record.archiveLeaseEpoch = epoch;
1244
1255
  if (this.byTopic.get(record.topicId) === sessionId) this.byTopic.delete(record.topicId);
@@ -1368,6 +1379,7 @@ export class TopicRegistry {
1368
1379
  )
1369
1380
  return false;
1370
1381
  record.authorityState = "inactive";
1382
+ delete record.disconnectGraceExpiresAt;
1371
1383
  if (this.byTopic.get(record.topicId) === sessionId) this.byTopic.delete(record.topicId);
1372
1384
  this.archiveJobs.delete(sessionId);
1373
1385
  return true;
@@ -1383,6 +1395,7 @@ export class TopicRegistry {
1383
1395
  record.authorityEpoch = epoch;
1384
1396
  record.archiveLeaseEpoch = epoch;
1385
1397
  record.authorityState = "archive_pending";
1398
+ delete record.disconnectGraceExpiresAt;
1386
1399
  }
1387
1400
  }
1388
1401
  return (record.authorityState === "archive_pending" || record.authorityState === "archive_exhausted") &&
@@ -1420,6 +1433,7 @@ export class TopicRegistry {
1420
1433
  ...(exhausted ? { safeDiagnostic: "archive retry remains discoverable after retry budget" } : {}),
1421
1434
  };
1422
1435
  record.authorityState = "archive_pending";
1436
+ delete record.disconnectGraceExpiresAt;
1423
1437
  this.archiveJobs.set(sessionId, job);
1424
1438
  return job;
1425
1439
  }
@@ -1084,16 +1084,38 @@ export class RevisionStore {
1084
1084
  }
1085
1085
 
1086
1086
  async #writeAtomic(file: string, data: Buffer): Promise<void> {
1087
- const temporary = `${file}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
1087
+ for (let attempt = 0; attempt < 2; attempt++) {
1088
+ const temporary = `${file}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
1089
+ try {
1090
+ await this.#writeAtomicAttempt(temporary, data);
1091
+ await chmod(temporary, 0o600);
1092
+ await rename(temporary, file);
1093
+ return;
1094
+ } catch (error) {
1095
+ await rm(temporary, { force: true }).catch(() => undefined);
1096
+ if ((error as NodeJS.ErrnoException).code !== "EBADF" || attempt === 1) throw error;
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ async #writeAtomicAttempt(temporary: string, data: Buffer): Promise<void> {
1088
1102
  const handle = await open(temporary, "w", 0o600);
1103
+ let failure: { error: unknown } | undefined;
1089
1104
  try {
1090
1105
  await handle.writeFile(data);
1091
1106
  await handle.sync();
1092
- } finally {
1107
+ } catch (error) {
1108
+ failure = { error };
1109
+ }
1110
+ try {
1093
1111
  await handle.close();
1112
+ } catch (error) {
1113
+ // Bun can report EBADF when concurrent descriptor teardown has already
1114
+ // closed a fully written and fsynced handle. Preserve every primary write
1115
+ // failure and keep all other close errors fatal.
1116
+ if ((error as NodeJS.ErrnoException).code !== "EBADF" && failure === undefined) failure = { error };
1094
1117
  }
1095
- await chmod(temporary, 0o600);
1096
- await rename(temporary, file);
1118
+ if (failure !== undefined) throw failure.error;
1097
1119
  }
1098
1120
 
1099
1121
  #retainSpill(revision: Revision): void {
@@ -6,17 +6,9 @@ import {
6
6
  } from "../config/model-profile-contract";
7
7
  import type { ModelProfileDefinition } from "../config/model-profiles";
8
8
  import { isAuthenticated, kNoAuth } from "../config/model-registry";
9
+ import { SYNTHETIC_PROVIDER_ID } from "./model-profile-namespace";
9
10
 
10
- /**
11
- * Reserved logical provider namespace for the preset-as-model facade.
12
- *
13
- * `gajae-code/<profile>` is not a real API provider: rows under this namespace
14
- * exist only in the SDK Q10 projection and `model.set` selection routes to a
15
- * session-scoped model-profile activation transaction. The namespace is
16
- * reserved so a user-defined `models.yml` provider of the same name cannot
17
- * silently shadow the facade (see {@link syntheticNamespaceCollision}).
18
- */
19
- export const SYNTHETIC_PROVIDER_ID = "gajae-code";
11
+ export { SYNTHETIC_PROVIDER_ID } from "./model-profile-namespace";
20
12
 
21
13
  /**
22
14
  * Display metadata used for synthetic rows whose profile default model cannot
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reserved logical provider namespace for the preset-as-model facade.
3
+ *
4
+ * Kept dependency-free because machine entrypoints use the namespace when
5
+ * filtering projected models and must not pull the model registry/session host
6
+ * graph into their static import closure.
7
+ */
8
+ export const SYNTHETIC_PROVIDER_ID = "gajae-code";