@sayknow-cli/coding-agent 0.5.2 → 0.5.8

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 (94) hide show
  1. package/CHANGELOG.md +66 -16
  2. package/dist/types/cli/update-cli.d.ts +8 -1
  3. package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
  4. package/dist/types/config/keybindings.d.ts +10 -0
  5. package/dist/types/config/model-registry.d.ts +1 -1
  6. package/dist/types/config/models-config-schema.d.ts +4 -0
  7. package/dist/types/config/settings-schema.d.ts +73 -0
  8. package/dist/types/dap/client.d.ts +1 -0
  9. package/dist/types/lsp/client.d.ts +5 -0
  10. package/dist/types/modes/components/welcome.d.ts +1 -0
  11. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  12. package/dist/types/modes/interactive-mode.d.ts +13 -1
  13. package/dist/types/modes/types.d.ts +7 -1
  14. package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
  15. package/dist/types/session/agent-session.d.ts +8 -0
  16. package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
  17. package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
  18. package/dist/types/session/session-manager.d.ts +9 -0
  19. package/dist/types/session/session-storage.d.ts +17 -0
  20. package/dist/types/session-import/claude.d.ts +26 -0
  21. package/dist/types/session-import/codex.d.ts +22 -0
  22. package/dist/types/session-import/command.d.ts +20 -0
  23. package/dist/types/session-import/detect.d.ts +22 -0
  24. package/dist/types/session-import/index.d.ts +7 -0
  25. package/dist/types/session-import/redact.d.ts +23 -0
  26. package/dist/types/session-import/service.d.ts +28 -0
  27. package/dist/types/session-import/types.d.ts +125 -0
  28. package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
  29. package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
  30. package/dist/types/slash-commands/types.d.ts +5 -0
  31. package/dist/types/system-prompt.d.ts +5 -0
  32. package/dist/types/tools/ask.d.ts +5 -0
  33. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  34. package/package.json +13 -7
  35. package/scripts/compile-args.ts +8 -4
  36. package/scripts/generate-sdk-operation-inventory.ts +5 -6
  37. package/src/cli/config-cli.ts +23 -8
  38. package/src/cli/update-cli.ts +265 -21
  39. package/src/config/atomic-yaml-patch.ts +167 -30
  40. package/src/config/keybindings.ts +10 -0
  41. package/src/config/model-profiles.ts +84 -16
  42. package/src/config/model-registry.ts +60 -38
  43. package/src/config/models-config-schema.ts +1 -1
  44. package/src/config/settings-schema.ts +79 -0
  45. package/src/config/settings.ts +13 -2
  46. package/src/dap/client.ts +78 -30
  47. package/src/internal-urls/docs-index.generated.ts +5 -4
  48. package/src/lsp/client.ts +17 -29
  49. package/src/modes/action-registry.ts +2 -0
  50. package/src/modes/components/welcome.ts +5 -0
  51. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  52. package/src/modes/controllers/input-controller.ts +71 -6
  53. package/src/modes/controllers/plan-mode-controller.ts +8 -1
  54. package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
  55. package/src/modes/controllers/selector-controller.ts +47 -19
  56. package/src/modes/interactive-mode.ts +54 -4
  57. package/src/modes/types.ts +5 -1
  58. package/src/prompts/system/system-prompt.md +8 -0
  59. package/src/runtime-mcp/transports/stdio.ts +96 -33
  60. package/src/sdk/broker/lifecycle.ts +6 -5
  61. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  62. package/src/sdk/session.ts +27 -18
  63. package/src/session/agent-session.ts +155 -15
  64. package/src/session/internal/managed-session-scope.ts +41 -6
  65. package/src/session/internal/managed-session-storage.ts +14 -0
  66. package/src/session/session-manager.ts +113 -11
  67. package/src/session/session-storage.ts +70 -0
  68. package/src/session-import/claude.ts +382 -0
  69. package/src/session-import/codex.ts +458 -0
  70. package/src/session-import/command.ts +61 -0
  71. package/src/session-import/detect.ts +133 -0
  72. package/src/session-import/index.ts +27 -0
  73. package/src/session-import/redact.ts +125 -0
  74. package/src/session-import/service.ts +749 -0
  75. package/src/session-import/types.ts +163 -0
  76. package/src/skc-runtime/launch-worktree.ts +277 -4
  77. package/src/slash-commands/acp-builtins.ts +5 -1
  78. package/src/slash-commands/builtin-registry.ts +100 -1
  79. package/src/slash-commands/types.ts +5 -0
  80. package/src/system-prompt.ts +7 -0
  81. package/src/tools/ask.ts +5 -0
  82. package/src/tools/fetch.ts +7 -2
  83. package/src/utils/markit.ts +24 -2
  84. package/src/utils/mupdf-wasm.ts +26 -0
  85. package/src/export/html/template.css +0 -1060
  86. package/src/export/html/template.html +0 -47
  87. package/src/export/html/template.js +0 -2348
  88. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  89. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  90. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  91. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  92. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  93. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  94. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
@@ -581,6 +581,25 @@ export const SETTINGS_SCHEMA = {
581
581
  },
582
582
  },
583
583
 
584
+ reasoningLanguage: {
585
+ type: "enum",
586
+ values: ["off", "english"] as const,
587
+ default: "off",
588
+ ui: {
589
+ tab: "appearance",
590
+ label: "Reasoning Language",
591
+ description: "Reason through technical work in English while answering in your own language",
592
+ options: [
593
+ { value: "off", label: "Off", description: "Do not add reasoning-language guidance" },
594
+ {
595
+ value: "english",
596
+ label: "English",
597
+ description: "Reason in English and answer in the language you used",
598
+ },
599
+ ],
600
+ },
601
+ },
602
+
584
603
  colorBlindMode: {
585
604
  type: "boolean",
586
605
  default: false,
@@ -1417,6 +1436,15 @@ export const SETTINGS_SCHEMA = {
1417
1436
  ],
1418
1437
  },
1419
1438
  },
1439
+ "startup.skipLogoAnimation": {
1440
+ type: "boolean",
1441
+ default: false,
1442
+ ui: {
1443
+ tab: "interaction",
1444
+ label: "Skip Logo Animation",
1445
+ description: "Show the final welcome logo immediately",
1446
+ },
1447
+ },
1420
1448
 
1421
1449
  "startup.checkUpdate": {
1422
1450
  type: "boolean",
@@ -1616,6 +1644,57 @@ export const SETTINGS_SCHEMA = {
1616
1644
  },
1617
1645
  },
1618
1646
 
1647
+ // Adaptive compaction. Opt-in; the fixed thresholds above keep precedence.
1648
+ "compaction.adaptive.enabled": {
1649
+ type: "boolean",
1650
+ default: false,
1651
+ ui: {
1652
+ tab: "context",
1653
+ label: "Adaptive Compaction",
1654
+ description: "Adjust compaction frequency by context size and call rate",
1655
+ },
1656
+ },
1657
+
1658
+ "compaction.adaptive.baseThresholdPercent": {
1659
+ type: "number",
1660
+ default: 85,
1661
+ ui: {
1662
+ tab: "context",
1663
+ label: "Adaptive Base Threshold %",
1664
+ description: "Base context percentage used when adaptive compaction is enabled",
1665
+ },
1666
+ },
1667
+
1668
+ "compaction.adaptive.aggression": {
1669
+ type: "number",
1670
+ default: 0.15,
1671
+ ui: {
1672
+ tab: "context",
1673
+ label: "Adaptive Aggression",
1674
+ description: "How strongly call rate lowers the compaction threshold; use 0 to 1",
1675
+ },
1676
+ },
1677
+
1678
+ "compaction.adaptive.turnWindow": {
1679
+ type: "number",
1680
+ default: 15,
1681
+ ui: {
1682
+ tab: "context",
1683
+ label: "Call-Rate Window",
1684
+ description: "Minutes of recent calls considered by adaptive compaction",
1685
+ },
1686
+ },
1687
+
1688
+ "compaction.adaptive.minThresholdPercent": {
1689
+ type: "number",
1690
+ default: 50,
1691
+ ui: {
1692
+ tab: "context",
1693
+ label: "Adaptive Floor %",
1694
+ description: "Lowest context percentage adaptive compaction may lower the threshold to",
1695
+ },
1696
+ },
1697
+
1619
1698
  "compaction.handoffSaveToDisk": {
1620
1699
  type: "boolean",
1621
1700
  default: false,
@@ -39,6 +39,7 @@ import { AgentStorage } from "../session/agent-storage";
39
39
  import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
40
40
  import {
41
41
  type AtomicYamlPatch,
42
+ AtomicYamlRetargetError,
42
43
  applyAtomicYamlPatches,
43
44
  applyAtomicYamlPatchesWithCurrent,
44
45
  atomicYamlPathHash,
@@ -830,22 +831,28 @@ export class Settings implements NotificationSettingsReader {
830
831
  if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
831
832
  this.#releasePendingSaveSlot();
832
833
  const observedSave = this.#savePromise;
834
+ let saveError: unknown;
833
835
  try {
834
836
  await observedSave;
835
- } catch {
837
+ } catch (error) {
836
838
  // Historical flush() behavior logs background failures but does not reject.
839
+ saveError = error;
837
840
  }
841
+ if (saveError instanceof AtomicYamlRetargetError) return;
838
842
  // A failed predecessor may settle just before a new mutation observes its
839
843
  // still-reserved slot. Explicit flush owns one fresh attempt for remaining
840
844
  // dirty patches instead of leaving them stranded or retrying forever.
841
845
  if (this.#modified.size > 0 && this.#savePromise === observedSave) {
842
846
  if (!this.#pendingSaveSlot) this.#queueSave();
843
847
  this.#releasePendingSaveSlot();
848
+ let retryError: unknown;
844
849
  try {
845
850
  await this.#savePromise;
846
- } catch {
851
+ } catch (error) {
847
852
  // Keep dirty state for a later explicit flush or mutation.
853
+ retryError = error;
848
854
  }
855
+ if (retryError instanceof AtomicYamlRetargetError) return;
849
856
  }
850
857
  await this.#refreshDurableSettings();
851
858
  if (this.#modified.size > 0 && !this.#pendingSaveSlot) {
@@ -871,6 +878,7 @@ export class Settings implements NotificationSettingsReader {
871
878
  saveError = error;
872
879
  }
873
880
  await this.#refreshDurableSettings();
881
+ if (saveError instanceof AtomicYamlRetargetError) throw saveError;
874
882
  if (this.#modified.size > 0 && !this.#pendingSaveSlot) {
875
883
  this.#queueSave();
876
884
  this.#releasePendingSaveSlot();
@@ -1721,6 +1729,9 @@ export class Settings implements NotificationSettingsReader {
1721
1729
  logger.warn("Settings: refresh after background save failure failed", { error: String(refreshError) });
1722
1730
  }
1723
1731
  throw error;
1732
+ })
1733
+ .finally(() => {
1734
+ if (this.#pendingSaveSlot === slot) this.#pendingSaveSlot = undefined;
1724
1735
  });
1725
1736
  this.#savePromise = save;
1726
1737
  void save.catch(() => {});
package/src/dap/client.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import * as fs from "node:fs/promises";
3
- import { logger } from "@sayknow-cli/utils";
3
+ import { isKnownSinkPeerClosedError, logger } from "@sayknow-cli/utils";
4
4
  import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
5
5
  import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
6
6
  import { type OwnedProcess, spawnOwnedProcess } from "../runtime/process-lifecycle";
@@ -62,9 +62,9 @@ function parseMessage(
62
62
 
63
63
  async function writeMessage(sink: DapWriteSink, message: DapRequestMessage | DapResponseMessage): Promise<void> {
64
64
  const content = JSON.stringify(message);
65
- sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
66
- sink.write(content);
67
- await sink.flush();
65
+ const frame = `Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n${content}`;
66
+ await Promise.resolve(sink.write(frame));
67
+ await Promise.resolve(sink.flush());
68
68
  }
69
69
 
70
70
  function toErrorMessage(value: unknown): string {
@@ -98,6 +98,9 @@ export class DapClient {
98
98
  #messageBuffer = Buffer.alloc(0);
99
99
  #isReading = false;
100
100
  #disposed = false;
101
+ #terminalError: Error | undefined;
102
+ #disposePromise: Promise<void> | undefined;
103
+ #writeQueue: Promise<void> = Promise.resolve();
101
104
  #lastActivity = Date.now();
102
105
  #capabilities?: DapCapabilities;
103
106
  #eventHandlers = new Map<string, Set<DapEventHandler>>();
@@ -119,6 +122,18 @@ export class DapClient {
119
122
  this.#socket = options?.socket;
120
123
  }
121
124
 
125
+ #queueWriteMessage(message: DapRequestMessage | DapResponseMessage): Promise<void> {
126
+ const write = this.#writeQueue
127
+ .catch(() => {})
128
+ .then(() => {
129
+ if (this.#terminalError) throw this.#terminalError;
130
+ if (this.#disposed) throw new Error(`DAP adapter ${this.adapter.name} is not running`);
131
+ return writeMessage(this.#writeSink, message);
132
+ });
133
+ this.#writeQueue = write.catch(() => {});
134
+ return write;
135
+ }
136
+
122
137
  static async spawn({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
123
138
  if (adapter.connectMode === "socket") {
124
139
  return DapClient.#spawnSocket({ adapter, cwd });
@@ -142,7 +157,7 @@ export class DapClient {
142
157
  proc.exited.then(() => {
143
158
  client.#handleProcessExit();
144
159
  });
145
- void client.#startMessageReader();
160
+ void client.startMessageReader();
146
161
  return client;
147
162
  }
148
163
 
@@ -199,7 +214,7 @@ export class DapClient {
199
214
  transport = await connectSocket({ unix: socketPath }, 10_000);
200
215
  const client = new DapClient(adapter, cwd, owner, transport);
201
216
  proc.exited.then(() => client.#handleProcessExit());
202
- void client.#startMessageReader();
217
+ void client.startMessageReader();
203
218
  return client;
204
219
  } catch (err) {
205
220
  transport?.socket.end();
@@ -268,7 +283,7 @@ export class DapClient {
268
283
  const { readable, writeSink, socket } = wrapBunSocket(rawSocket);
269
284
  const client = new DapClient(adapter, cwd, owner, { readable, writeSink, socket });
270
285
  proc.exited.then(() => client.#handleProcessExit());
271
- void client.#startMessageReader();
286
+ void client.startMessageReader();
272
287
  return client;
273
288
  }
274
289
 
@@ -364,6 +379,7 @@ export class DapClient {
364
379
  signal?: AbortSignal,
365
380
  timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
366
381
  ): Promise<TBody> {
382
+ if (this.#terminalError) throw this.#terminalError;
367
383
  if (signal?.aborted) {
368
384
  throw signal.reason instanceof Error ? signal.reason : new ToolAbortError();
369
385
  }
@@ -412,10 +428,11 @@ export class DapClient {
412
428
  });
413
429
  this.#lastActivity = Date.now();
414
430
  try {
415
- await writeMessage(this.#writeSink, request);
431
+ await this.#queueWriteMessage(request);
416
432
  } catch (error) {
417
433
  this.#pendingRequests.delete(requestSeq);
418
434
  cleanup();
435
+ if (isKnownSinkPeerClosedError(error)) throw this.#terminalizeTransport(error);
419
436
  throw error;
420
437
  }
421
438
  return promise;
@@ -431,37 +448,30 @@ export class DapClient {
431
448
  ...(message ? { message } : {}),
432
449
  ...(body !== undefined ? { body } : {}),
433
450
  };
434
- await writeMessage(this.#writeSink, response);
451
+ await this.#queueWriteMessage(response);
435
452
  }
436
453
 
437
454
  async dispose(): Promise<void> {
438
- if (this.#disposed) return;
439
- this.#disposed = true;
440
- this.#rejectPendingRequests(new Error(`DAP adapter ${this.adapter.name} disposed`));
441
- try {
442
- this.#socket?.end();
443
- } catch {
444
- /* socket may already be closed */
445
- }
446
- try {
447
- await this.#owner.dispose();
448
- await this.#owner.awaitExit({ timeoutMs: 1_000 });
449
- } catch (error) {
450
- logger.debug("Failed to dispose DAP adapter", {
451
- adapter: this.adapter.name,
452
- error: toErrorMessage(error),
453
- });
455
+ if (!this.#disposed) {
456
+ const error = new Error(`DAP adapter ${this.adapter.name} disposed`);
457
+ this.#disposed = true;
458
+ this.#terminalError = error;
459
+ this.#rejectPendingRequests(error);
454
460
  }
461
+ await this.#beginOwnerDisposal();
455
462
  }
456
463
 
457
- async #startMessageReader(): Promise<void> {
464
+ async startMessageReader(): Promise<void> {
458
465
  if (this.#isReading) return;
459
466
  this.#isReading = true;
460
467
  const reader = this.#readable.getReader();
461
468
  try {
462
469
  while (true) {
463
470
  const { done, value } = await reader.read();
464
- if (done) break;
471
+ if (done) {
472
+ this.#terminalizeTransport(new Error("DAP readable closed"));
473
+ break;
474
+ }
465
475
  const currentBuffer = Buffer.concat([this.#messageBuffer, value]);
466
476
  this.#messageBuffer = currentBuffer;
467
477
  let workingBuffer = currentBuffer;
@@ -482,7 +492,7 @@ export class DapClient {
482
492
  this.#messageBuffer = workingBuffer;
483
493
  }
484
494
  } catch (error) {
485
- this.#rejectPendingRequests(new Error(`DAP connection closed: ${toErrorMessage(error)}`));
495
+ this.#terminalizeTransport(error);
486
496
  } finally {
487
497
  reader.releaseLock();
488
498
  this.#isReading = false;
@@ -523,9 +533,9 @@ export class DapClient {
523
533
  try {
524
534
  const handler = this.#reverseRequestHandlers.get(message.command);
525
535
  if (handler) {
536
+ let body: unknown;
526
537
  try {
527
- const body = await handler(message.arguments);
528
- await this.sendResponse(message, true, body);
538
+ body = await handler(message.arguments);
529
539
  } catch (error) {
530
540
  const errorMessage = toErrorMessage(error);
531
541
  await this.sendResponse(
@@ -539,7 +549,9 @@ export class DapClient {
539
549
  },
540
550
  errorMessage,
541
551
  );
552
+ return;
542
553
  }
554
+ await this.sendResponse(message, true, body);
543
555
  return;
544
556
  }
545
557
  const errorMessage = `Unsupported DAP request: ${message.command}`;
@@ -555,6 +567,10 @@ export class DapClient {
555
567
  errorMessage,
556
568
  );
557
569
  } catch (error) {
570
+ if (isKnownSinkPeerClosedError(error)) {
571
+ this.#terminalizeTransport(error);
572
+ return;
573
+ }
558
574
  logger.warn("Failed to answer DAP adapter request", {
559
575
  adapter: this.adapter.name,
560
576
  command: message.command,
@@ -586,9 +602,41 @@ export class DapClient {
586
602
  ? `DAP adapter exited (code ${exitCode}): ${stderr}${diagnosticSuffix}`
587
603
  : `DAP adapter exited unexpectedly (code ${exitCode})${diagnosticSuffix}`,
588
604
  );
605
+ this.#terminalError = error;
589
606
  this.#rejectPendingRequests(error);
590
607
  }
591
608
 
609
+ #terminalizeTransport(cause: unknown): Error {
610
+ if (this.#terminalError) return this.#terminalError;
611
+ const error = new Error(`DAP adapter ${this.adapter.name} transport closed`, { cause });
612
+ this.#terminalError = error;
613
+ this.#disposed = true;
614
+ this.#rejectPendingRequests(error);
615
+ void this.#beginOwnerDisposal();
616
+ return error;
617
+ }
618
+
619
+ #beginOwnerDisposal(): Promise<void> {
620
+ if (this.#disposePromise) return this.#disposePromise;
621
+ this.#disposePromise = (async () => {
622
+ try {
623
+ this.#socket?.end();
624
+ } catch {
625
+ /* socket may already be closed */
626
+ }
627
+ try {
628
+ await this.#owner.dispose();
629
+ await this.#owner.awaitExit({ timeoutMs: 1_000 });
630
+ } catch (disposeError) {
631
+ logger.debug("Failed to dispose DAP adapter", {
632
+ adapter: this.adapter.name,
633
+ error: toErrorMessage(disposeError),
634
+ });
635
+ }
636
+ })();
637
+ return this.#disposePromise;
638
+ }
639
+
592
640
  #rejectPendingRequests(error: Error): void {
593
641
  for (const pending of this.#pendingRequests.values()) {
594
642
  pending.reject(error);