adhdev 0.9.54 → 0.9.56

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/dist/cli/index.js CHANGED
@@ -1483,7 +1483,12 @@ function createDefaultGitCommandServices() {
1483
1483
  turnId
1484
1484
  }),
1485
1485
  compareSnapshots: ({ beforeSnapshotId, afterSnapshotId }) => defaultSnapshotStore.compare(beforeSnapshotId, afterSnapshotId),
1486
- getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until })
1486
+ getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until }),
1487
+ checkpoint: async ({ workspace, message, includeUntracked = false }) => gitCheckpoint(workspace, message, includeUntracked),
1488
+ stashPush: async ({ workspace, message, includeUntracked = false }) => gitStashPush(workspace, message, includeUntracked),
1489
+ stashPop: async ({ workspace, stashRef }) => gitStashPop(workspace, stashRef),
1490
+ checkoutFiles: async ({ workspace, paths }) => gitCheckoutFiles(workspace, paths),
1491
+ getRemoteUrl: async ({ workspace, remote = "origin" }) => gitGetRemoteUrl(workspace, remote)
1487
1492
  };
1488
1493
  }
1489
1494
  function validateWorkspace2(args) {
@@ -1546,9 +1551,6 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
1546
1551
  if (!isGitCommandName(command)) {
1547
1552
  return failure("invalid_args", `Unknown Git command: ${command}`);
1548
1553
  }
1549
- if (MUTATING_COMMAND_NAMES.has(command)) {
1550
- return failure("invalid_args", `${command} is not implemented in daemon-core read-only Git routing`);
1551
- }
1552
1554
  const workspaceResult = validateWorkspace2(args);
1553
1555
  if ("success" in workspaceResult) return workspaceResult;
1554
1556
  const { workspace } = workspaceResult;
@@ -1608,10 +1610,153 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
1608
1610
  }));
1609
1611
  return "success" in log2 ? log2 : { success: true, log: log2 };
1610
1612
  }
1613
+ case "git_checkpoint": {
1614
+ if (!services.checkpoint) return serviceNotImplemented(command);
1615
+ const msg = validateMutatingMessage(args?.message);
1616
+ if (typeof msg !== "string") return msg;
1617
+ const includeUntracked = Boolean(args?.includeUntracked);
1618
+ const checkpoint = await runService(() => services.checkpoint({ workspace, message: msg, includeUntracked }));
1619
+ return "success" in checkpoint ? checkpoint : { success: true, checkpoint };
1620
+ }
1621
+ case "git_stash_push": {
1622
+ if (!services.stashPush) return serviceNotImplemented(command);
1623
+ const msg = validateMutatingMessage(args?.message);
1624
+ if (typeof msg !== "string") return msg;
1625
+ const includeUntracked = Boolean(args?.includeUntracked);
1626
+ const stash = await runService(() => services.stashPush({ workspace, message: msg, includeUntracked }));
1627
+ return "success" in stash ? stash : { success: true, stash };
1628
+ }
1629
+ case "git_stash_pop": {
1630
+ if (!services.stashPop) return serviceNotImplemented(command);
1631
+ const stashRef = optionalString(args?.stashRef);
1632
+ if (stashRef !== void 0 && !/^stash@\{\d+\}$/.test(stashRef)) {
1633
+ return failure("invalid_args", "stashRef must match stash@{N} format");
1634
+ }
1635
+ const popResult = await runService(() => services.stashPop({ workspace, stashRef }));
1636
+ if (popResult !== void 0 && "success" in popResult) return popResult;
1637
+ return { success: true, stashPopped: true };
1638
+ }
1639
+ case "git_checkout_files": {
1640
+ if (!services.checkoutFiles) return serviceNotImplemented(command);
1641
+ const paths = args?.paths;
1642
+ if (!Array.isArray(paths) || paths.length === 0) {
1643
+ return failure("invalid_args", "paths must be a non-empty array");
1644
+ }
1645
+ if (paths.length > 50) {
1646
+ return failure("invalid_args", "paths array exceeds maximum of 50 entries");
1647
+ }
1648
+ const checkoutResult = await runService(() => services.checkoutFiles({ workspace, paths }));
1649
+ return "success" in checkoutResult ? checkoutResult : { success: true, checkedOut: checkoutResult.checkedOut };
1650
+ }
1651
+ case "git_remote_url": {
1652
+ if (!services.getRemoteUrl) return serviceNotImplemented(command);
1653
+ const remote = typeof args?.remote === "string" && args.remote.trim() ? args.remote.trim() : "origin";
1654
+ const remoteResult = await runService(() => services.getRemoteUrl({ workspace, remote }));
1655
+ if ("success" in remoteResult) return remoteResult;
1656
+ return { success: true, remoteUrl: remoteResult.remoteUrl, remote: remoteResult.remote };
1657
+ }
1611
1658
  default:
1612
1659
  return failure("invalid_args", `Unknown Git command: ${command}`);
1613
1660
  }
1614
1661
  }
1662
+ function validateMutatingMessage(value) {
1663
+ if (typeof value !== "string" || !value.trim()) {
1664
+ return failure("invalid_args", "message must be a non-empty string");
1665
+ }
1666
+ const msg = value.trim();
1667
+ if (msg.length > 200) {
1668
+ return failure("invalid_args", "message must be 200 characters or fewer");
1669
+ }
1670
+ return msg;
1671
+ }
1672
+ async function gitCheckpoint(workspace, message, includeUntracked) {
1673
+ const repo = await resolveGitRepository(workspace);
1674
+ const repoRoot = repo.repoRoot;
1675
+ const statusResult = await getGitRepoStatus(workspace);
1676
+ if (statusResult.hasConflicts) {
1677
+ throw new GitCommandError("conflict", "Repository has conflicts \u2014 resolve before checkpointing");
1678
+ }
1679
+ const addArgs = includeUntracked ? ["-A"] : ["-u"];
1680
+ await runGit(repo, ["add", ...addArgs], { cwd: repoRoot });
1681
+ const fullMsg = `adhdev: checkpoint ${message}`;
1682
+ let commitSha;
1683
+ try {
1684
+ await runGit(repo, ["commit", "-m", fullMsg], { cwd: repoRoot });
1685
+ const revResult = await runGit(repo, ["rev-parse", "HEAD"], { cwd: repoRoot });
1686
+ commitSha = revResult.stdout.trim();
1687
+ } catch (err) {
1688
+ const output = (err?.stdout || "") + (err?.stderr || "");
1689
+ if (/nothing to commit/i.test(output)) {
1690
+ throw new GitCommandError("git_command_failed", "Nothing to commit");
1691
+ }
1692
+ throw err;
1693
+ }
1694
+ return {
1695
+ workspace: repo.workspace,
1696
+ repoRoot,
1697
+ isGitRepo: true,
1698
+ commit: commitSha,
1699
+ message: fullMsg,
1700
+ lastCheckedAt: Date.now()
1701
+ };
1702
+ }
1703
+ async function gitStashPush(workspace, message, includeUntracked) {
1704
+ const repo = await resolveGitRepository(workspace);
1705
+ const repoRoot = repo.repoRoot;
1706
+ const stashArgs = ["stash", "push", "-m", message];
1707
+ if (includeUntracked) stashArgs.push("--include-untracked");
1708
+ const result = await runGit(repo, stashArgs, { cwd: repoRoot });
1709
+ if (/No local changes to save/i.test(result.stdout + result.stderr)) {
1710
+ throw new GitCommandError("git_command_failed", "Nothing to stash");
1711
+ }
1712
+ return {
1713
+ workspace: repo.workspace,
1714
+ repoRoot,
1715
+ isGitRepo: true,
1716
+ stashRef: "stash@{0}",
1717
+ message,
1718
+ lastCheckedAt: Date.now()
1719
+ };
1720
+ }
1721
+ async function gitStashPop(workspace, stashRef) {
1722
+ const repo = await resolveGitRepository(workspace);
1723
+ const repoRoot = repo.repoRoot;
1724
+ const popArgs = stashRef ? ["stash", "pop", stashRef] : ["stash", "pop"];
1725
+ await runGit(repo, popArgs, { cwd: repoRoot });
1726
+ }
1727
+ async function gitCheckoutFiles(workspace, paths) {
1728
+ const repo = await resolveGitRepository(workspace);
1729
+ const repoRoot = repo.repoRoot;
1730
+ const normalizedPaths = [];
1731
+ for (const p of paths) {
1732
+ if (typeof p !== "string" || !p.trim() || p.includes("\0")) {
1733
+ throw new GitCommandError("invalid_args", `Invalid path: ${String(p)}`);
1734
+ }
1735
+ if (path3.isAbsolute(p)) {
1736
+ throw new GitCommandError("invalid_args", `Path must be repository-relative, not absolute: ${p}`);
1737
+ }
1738
+ const normalized = path3.normalize(p.trim()).split(path3.sep).join("/");
1739
+ if (normalized.startsWith("../") || normalized === "..") {
1740
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
1741
+ }
1742
+ const absolutePath = path3.resolve(repoRoot, normalized);
1743
+ if (!isPathInside(repoRoot, absolutePath)) {
1744
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
1745
+ }
1746
+ normalizedPaths.push(normalized);
1747
+ }
1748
+ await runGit(repo, ["checkout", "--", ...normalizedPaths], { cwd: repoRoot });
1749
+ return { checkedOut: normalizedPaths };
1750
+ }
1751
+ async function gitGetRemoteUrl(workspace, remote) {
1752
+ const repo = await resolveGitRepository(workspace);
1753
+ const result = await runGit(repo, ["remote", "get-url", remote], { cwd: repo.repoRoot });
1754
+ const remoteUrl = result.stdout.trim();
1755
+ if (!remoteUrl) {
1756
+ throw new GitCommandError("git_command_failed", `Remote '${remote}' has no URL`);
1757
+ }
1758
+ return { remoteUrl, remote };
1759
+ }
1615
1760
  function formatOptionalGitLogRangeArg(flag, value) {
1616
1761
  return value ? [`${flag}=${value}`] : [];
1617
1762
  }
@@ -1669,7 +1814,7 @@ function validateGitLogPath(repoRoot, filePath) {
1669
1814
  }
1670
1815
  return normalized;
1671
1816
  }
1672
- var path3, GIT_COMMAND_NAMES, MUTATING_COMMAND_NAMES, SNAPSHOT_REASONS, FAILURE_REASONS, defaultSnapshotStore, defaultGitCommandServices;
1817
+ var path3, GIT_COMMAND_NAMES, SNAPSHOT_REASONS, FAILURE_REASONS, defaultSnapshotStore, defaultGitCommandServices;
1673
1818
  var init_git_commands = __esm({
1674
1819
  "../../oss/packages/daemon-core/src/git/git-commands.ts"() {
1675
1820
  "use strict";
@@ -1688,13 +1833,8 @@ var init_git_commands = __esm({
1688
1833
  "git_checkpoint",
1689
1834
  "git_stash_push",
1690
1835
  "git_stash_pop",
1691
- "git_checkout_files"
1692
- ]);
1693
- MUTATING_COMMAND_NAMES = /* @__PURE__ */ new Set([
1694
- "git_checkpoint",
1695
- "git_stash_push",
1696
- "git_stash_pop",
1697
- "git_checkout_files"
1836
+ "git_checkout_files",
1837
+ "git_remote_url"
1698
1838
  ]);
1699
1839
  SNAPSHOT_REASONS = /* @__PURE__ */ new Set([
1700
1840
  "session_baseline",
@@ -1721,6 +1861,33 @@ var init_git_commands = __esm({
1721
1861
  }
1722
1862
  });
1723
1863
 
1864
+ // ../../oss/packages/daemon-core/src/git/turn-snapshot-tracker.ts
1865
+ var BUSY_STATUSES, TERMINAL_STATUSES, TurnSnapshotTracker;
1866
+ var init_turn_snapshot_tracker = __esm({
1867
+ "../../oss/packages/daemon-core/src/git/turn-snapshot-tracker.ts"() {
1868
+ "use strict";
1869
+ BUSY_STATUSES = /* @__PURE__ */ new Set(["streaming", "waiting_approval"]);
1870
+ TERMINAL_STATUSES = /* @__PURE__ */ new Set(["idle", "error"]);
1871
+ TurnSnapshotTracker = class {
1872
+ lastStatus = /* @__PURE__ */ new Map();
1873
+ onTurnCompleted;
1874
+ constructor(onTurnCompleted) {
1875
+ this.onTurnCompleted = onTurnCompleted;
1876
+ }
1877
+ record(sessionId, status, workspace) {
1878
+ const prev = this.lastStatus.get(sessionId);
1879
+ this.lastStatus.set(sessionId, status);
1880
+ if (workspace && prev && BUSY_STATUSES.has(prev) && TERMINAL_STATUSES.has(status)) {
1881
+ this.onTurnCompleted({ sessionId, workspace });
1882
+ }
1883
+ }
1884
+ forget(sessionId) {
1885
+ this.lastStatus.delete(sessionId);
1886
+ }
1887
+ };
1888
+ }
1889
+ });
1890
+
1724
1891
  // ../../oss/packages/daemon-core/src/git/index.ts
1725
1892
  var init_git = __esm({
1726
1893
  "../../oss/packages/daemon-core/src/git/index.ts"() {
@@ -1732,6 +1899,7 @@ var init_git = __esm({
1732
1899
  init_git_snapshot_store();
1733
1900
  init_git_monitor();
1734
1901
  init_git_commands();
1902
+ init_turn_snapshot_tracker();
1735
1903
  }
1736
1904
  });
1737
1905
 
@@ -11887,6 +12055,16 @@ var init_handler = __esm({
11887
12055
  return result;
11888
12056
  }
11889
12057
  }
12058
+ if (cmd === "send_chat" && this._ctx.onBeforeSendChat) {
12059
+ const sessionId = this._currentRoute.session?.sessionId;
12060
+ const workspace = sessionId ? this._ctx.instanceManager?.getInstance(sessionId)?.getState?.()?.workspace : void 0;
12061
+ if (workspace && sessionId) {
12062
+ try {
12063
+ this._ctx.onBeforeSendChat({ workspace, sessionId });
12064
+ } catch {
12065
+ }
12066
+ }
12067
+ }
11890
12068
  try {
11891
12069
  result = await this.dispatch(cmd, args);
11892
12070
  this.logCommandEnd(cmd, result, startedAt);
@@ -14164,6 +14342,7 @@ function resolveCliAdapterConfig(provider) {
14164
14342
  sendDelayMs: typeof provider.sendDelayMs === "number" ? Math.max(0, provider.sendDelayMs) : 0,
14165
14343
  sendKey: typeof provider.sendKey === "string" && provider.sendKey.length > 0 ? provider.sendKey : "\r",
14166
14344
  submitStrategy: provider.submitStrategy === "immediate" ? "immediate" : "wait_for_echo",
14345
+ requirePromptEchoBeforeSubmit: provider.requirePromptEchoBeforeSubmit === true,
14167
14346
  providerResolutionMeta: {
14168
14347
  type: provider.type,
14169
14348
  name: provider.name,
@@ -14429,6 +14608,7 @@ var init_provider_cli_adapter = __esm({
14429
14608
  this.sendDelayMs = resolvedConfig.sendDelayMs;
14430
14609
  this.sendKey = resolvedConfig.sendKey;
14431
14610
  this.submitStrategy = resolvedConfig.submitStrategy;
14611
+ this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
14432
14612
  this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
14433
14613
  this.cliScripts = provider.scripts || {};
14434
14614
  const scriptNames = listCliScriptNames(this.cliScripts);
@@ -14525,6 +14705,12 @@ var init_provider_cli_adapter = __esm({
14525
14705
  accumulatedRawBuffer = "";
14526
14706
  /** Current visible terminal screen snapshot */
14527
14707
  terminalScreen = new TerminalScreen(24, 80);
14708
+ static MAX_RESPONSE_BUFFER = 8e3;
14709
+ static MAX_RECENT_OUTPUT_BUFFER = 1e3;
14710
+ responseBufferDroppedChars = 0;
14711
+ recentOutputDroppedChars = 0;
14712
+ accumulatedBufferDroppedChars = 0;
14713
+ accumulatedRawBufferDroppedChars = 0;
14528
14714
  /** Max accumulated buffer size. Sized to comfortably hold a single long
14529
14715
  * Hermes turn (tool calls + reasoning + final bubble) without the
14530
14716
  * rolling window pushing the turn's ╭─ opening line out of view. */
@@ -14542,6 +14728,23 @@ var init_provider_cli_adapter = __esm({
14542
14728
  providerResolutionMeta;
14543
14729
  static FINISH_RETRY_DELAY_MS = 300;
14544
14730
  static MAX_FINISH_RETRIES = 2;
14731
+ getBufferState() {
14732
+ const build = (droppedChars, maxChars) => droppedChars > 0 ? { truncated: true, droppedChars, maxChars } : void 0;
14733
+ const responseBuffer = build(this.responseBufferDroppedChars, _ProviderCliAdapter.MAX_RESPONSE_BUFFER);
14734
+ const recentOutputBuffer = build(this.recentOutputDroppedChars, _ProviderCliAdapter.MAX_RECENT_OUTPUT_BUFFER);
14735
+ const accumulatedBuffer = build(this.accumulatedBufferDroppedChars, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
14736
+ const accumulatedRawBuffer = build(this.accumulatedRawBufferDroppedChars, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
14737
+ if (!responseBuffer && !recentOutputBuffer && !accumulatedBuffer && !accumulatedRawBuffer) return void 0;
14738
+ return {
14739
+ ...responseBuffer ? { responseBuffer } : {},
14740
+ ...recentOutputBuffer ? { recentOutputBuffer } : {},
14741
+ ...accumulatedBuffer ? { accumulatedBuffer } : {},
14742
+ ...accumulatedRawBuffer ? { accumulatedRawBuffer } : {}
14743
+ };
14744
+ }
14745
+ recordBoundedAppendDrop(previousLength, appendedLength, nextLength) {
14746
+ return Math.max(0, previousLength + appendedLength - nextLength);
14747
+ }
14545
14748
  buildCommittedMessagesActivitySignature() {
14546
14749
  const last = this.committedMessages[this.committedMessages.length - 1];
14547
14750
  return [
@@ -14725,6 +14928,7 @@ var init_provider_cli_adapter = __esm({
14725
14928
  sendDelayMs;
14726
14929
  sendKey;
14727
14930
  submitStrategy;
14931
+ requirePromptEchoBeforeSubmit;
14728
14932
  static SCRIPT_STATUS_DEBOUNCE_MS = 3e3;
14729
14933
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
14730
14934
  setCliScripts(scripts) {
@@ -14906,7 +15110,9 @@ var init_provider_cli_adapter = __esm({
14906
15110
  this.scheduleStartupSettleCheck();
14907
15111
  }
14908
15112
  if (this.isWaitingForResponse && cleanData) {
14909
- this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, 8e3);
15113
+ const previousResponseLen = this.responseBuffer.length;
15114
+ this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, _ProviderCliAdapter.MAX_RESPONSE_BUFFER);
15115
+ this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
14910
15116
  }
14911
15117
  if (cleanData.trim()) {
14912
15118
  if (this.serverConn) {
@@ -14915,14 +15121,19 @@ var init_provider_cli_adapter = __esm({
14915
15121
  this.logBuffer.push({ message: cleanData.trim(), level: "info" });
14916
15122
  }
14917
15123
  }
15124
+ const prevRecentLen = this.recentOutputBuffer.length;
14918
15125
  const prevAccumulatedLen = this.accumulatedBuffer.length;
14919
15126
  const prevAccumulatedRawLen = this.accumulatedRawBuffer.length;
14920
- this.recentOutputBuffer = appendBoundedText(this.recentOutputBuffer, cleanData, 1e3);
15127
+ this.recentOutputBuffer = appendBoundedText(this.recentOutputBuffer, cleanData, _ProviderCliAdapter.MAX_RECENT_OUTPUT_BUFFER);
14921
15128
  this.accumulatedBuffer = appendBoundedText(this.accumulatedBuffer, cleanData, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
14922
15129
  this.accumulatedRawBuffer = appendBoundedText(this.accumulatedRawBuffer, rawData, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
15130
+ const droppedRecent = this.recordBoundedAppendDrop(prevRecentLen, cleanData.length, this.recentOutputBuffer.length);
15131
+ const droppedClean = this.recordBoundedAppendDrop(prevAccumulatedLen, cleanData.length, this.accumulatedBuffer.length);
15132
+ const droppedRaw = this.recordBoundedAppendDrop(prevAccumulatedRawLen, rawData.length, this.accumulatedRawBuffer.length);
15133
+ this.recentOutputDroppedChars += droppedRecent;
15134
+ this.accumulatedBufferDroppedChars += droppedClean;
15135
+ this.accumulatedRawBufferDroppedChars += droppedRaw;
14923
15136
  if (this.currentTurnScope) {
14924
- const droppedClean = prevAccumulatedLen + cleanData.length - this.accumulatedBuffer.length;
14925
- const droppedRaw = prevAccumulatedRawLen + rawData.length - this.accumulatedRawBuffer.length;
14926
15137
  if (droppedClean > 0) {
14927
15138
  this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
14928
15139
  }
@@ -15752,13 +15963,15 @@ var init_provider_cli_adapter = __esm({
15752
15963
  effectiveModal = parsedModal;
15753
15964
  }
15754
15965
  }
15966
+ const bufferState = this.getBufferState();
15755
15967
  return {
15756
15968
  status: effectiveStatus,
15757
15969
  messages: [...this.committedMessages],
15758
15970
  workingDir: this.workingDir,
15759
15971
  activeModal: effectiveModal,
15760
15972
  errorMessage: this.parseErrorMessage || void 0,
15761
- errorReason: this.parseErrorMessage ? "parse_error" : void 0
15973
+ errorReason: this.parseErrorMessage ? "parse_error" : void 0,
15974
+ ...bufferState ? { bufferState } : {}
15762
15975
  };
15763
15976
  }
15764
15977
  seedCommittedMessages(messages) {
@@ -15940,10 +16153,12 @@ var init_provider_cli_adapter = __esm({
15940
16153
  messages: hydratedMessages,
15941
16154
  activeModal: parsed.activeModal ?? this.activeModal,
15942
16155
  providerSessionId: typeof parsed.providerSessionId === "string" ? parsed.providerSessionId : void 0,
16156
+ ...this.getBufferState() ? { bufferState: this.getBufferState() } : {},
15943
16157
  ...this.providerOwnsTranscript() ? { transcriptAuthority: "provider", coverage: this.shouldUseFullProviderTranscriptContext() ? "full" : "tail" } : {}
15944
16158
  };
15945
16159
  } else {
15946
16160
  const messages = [...this.committedMessages];
16161
+ const bufferState = this.getBufferState();
15947
16162
  result = {
15948
16163
  id: "cli_session",
15949
16164
  status: this.currentStatus,
@@ -15954,7 +16169,8 @@ var init_provider_cli_adapter = __esm({
15954
16169
  index: typeof message.index === "number" ? message.index : index,
15955
16170
  receivedAt: typeof message.receivedAt === "number" ? message.receivedAt : message.timestamp
15956
16171
  })),
15957
- activeModal: this.activeModal
16172
+ activeModal: this.activeModal,
16173
+ ...bufferState ? { bufferState } : {}
15958
16174
  };
15959
16175
  }
15960
16176
  const hasVisibleAssistantMessage = Array.isArray(result?.messages) && result.messages.some((message) => message?.role === "assistant" && typeof message?.content === "string" && message.content.trim());
@@ -16245,6 +16461,22 @@ var init_provider_cli_adapter = __esm({
16245
16461
  }
16246
16462
  }
16247
16463
  if (elapsed >= state.maxEchoWaitMs) {
16464
+ const diagnostic = {
16465
+ elapsed,
16466
+ maxEchoWaitMs: state.maxEchoWaitMs,
16467
+ submitDelayMs: state.submitDelayMs,
16468
+ promptSnippet: state.normalizedPromptSnippet,
16469
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
16470
+ screenText: summarizeCliTraceText(screenText, 1e3)
16471
+ };
16472
+ this.recordTrace("submit_echo_missing", diagnostic);
16473
+ if (this.requirePromptEchoBeforeSubmit) {
16474
+ const message = `${this.cliName} prompt echo was not observed on the PTY screen before submit`;
16475
+ LOG.warn("CLI", `[${this.cliType}] ${message} elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs} screen=${JSON.stringify(diagnostic.screenText).slice(0, 240)}`);
16476
+ completion.rejectOnce(new Error(message));
16477
+ return;
16478
+ }
16479
+ LOG.warn("CLI", `[${this.cliType}] prompt echo was not observed before submit; sending submit key anyway elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs}`);
16248
16480
  this.submitSendKey(state, completion);
16249
16481
  return;
16250
16482
  }
@@ -16709,6 +16941,7 @@ var init_provider_cli_adapter = __esm({
16709
16941
  sendDelayMs: this.sendDelayMs,
16710
16942
  sendKey: this.sendKey,
16711
16943
  submitStrategy: this.submitStrategy,
16944
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
16712
16945
  submitPendingUntil: this.submitPendingUntil,
16713
16946
  responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
16714
16947
  resizeSuppressUntil: this.resizeSuppressUntil,
@@ -38009,6 +38242,7 @@ var init_provider_schema = __esm({
38009
38242
  "sendDelayMs",
38010
38243
  "sendKey",
38011
38244
  "submitStrategy",
38245
+ "requirePromptEchoBeforeSubmit",
38012
38246
  "timeouts",
38013
38247
  "disableUpstream"
38014
38248
  ]);
@@ -49584,6 +49818,7 @@ async function initDaemonComponents(config2) {
49584
49818
  providerLoader,
49585
49819
  instanceManager,
49586
49820
  sessionRegistry,
49821
+ gitCommandServices: createDefaultGitCommandServices(),
49587
49822
  onProviderSettingChanged: async (providerType) => {
49588
49823
  await refreshProviderAvailability(providerType);
49589
49824
  config2.onStatusChange?.();
@@ -49591,7 +49826,8 @@ async function initDaemonComponents(config2) {
49591
49826
  onProviderSourceConfigChanged: async () => {
49592
49827
  await refreshProviderAvailability();
49593
49828
  config2.onStatusChange?.();
49594
- }
49829
+ },
49830
+ onBeforeSendChat: config2.onBeforeSendChat
49595
49831
  });
49596
49832
  agentStreamManager = new DaemonAgentStreamManager(
49597
49833
  LOG.forComponent("AgentStream").asLogFn(),
@@ -49717,6 +49953,7 @@ var init_daemon_lifecycle = __esm({
49717
49953
  init_logger();
49718
49954
  init_runtime_defaults();
49719
49955
  init_config();
49956
+ init_git_commands();
49720
49957
  }
49721
49958
  });
49722
49959
 
@@ -49767,6 +50004,7 @@ __export(src_exports, {
49767
50004
  ProviderLoader: () => ProviderLoader,
49768
50005
  STANDALONE_CDP_SCAN_INTERVAL_MS: () => STANDALONE_CDP_SCAN_INTERVAL_MS,
49769
50006
  SessionHostPtyTransportFactory: () => SessionHostPtyTransportFactory,
50007
+ TurnSnapshotTracker: () => TurnSnapshotTracker,
49770
50008
  VersionArchive: () => VersionArchive,
49771
50009
  appendRecentActivity: () => appendRecentActivity,
49772
50010
  buildAssistantChatMessage: () => buildAssistantChatMessage,
@@ -81050,7 +81288,7 @@ function sendToPeer(peer, data) {
81050
81288
  return false;
81051
81289
  }
81052
81290
  const json2 = JSON.stringify(data);
81053
- if (messageType === "command_result" && json2.length > MAX_INLINE_JSON_MESSAGE_CHARS) {
81291
+ if (messageType === "command_result" && Buffer.byteLength(json2, "utf8") > MAX_INLINE_JSON_MESSAGE_CHARS) {
81054
81292
  return sendChunkedCommandResult(peer, peerId, requestId, json2);
81055
81293
  }
81056
81294
  try {
@@ -81063,21 +81301,51 @@ function sendToPeer(peer, data) {
81063
81301
  return false;
81064
81302
  }
81065
81303
  }
81304
+ function buildCommandResultChunkEnvelope(requestId, chunkId, index, total, data) {
81305
+ return JSON.stringify({
81306
+ type: "command_result_chunk",
81307
+ id: requestId,
81308
+ chunkId,
81309
+ index,
81310
+ total,
81311
+ data
81312
+ });
81313
+ }
81314
+ function splitJsonIntoCommandResultChunks(json2, requestId, chunkId) {
81315
+ const chunks = [];
81316
+ let offset = 0;
81317
+ while (offset < json2.length) {
81318
+ let end = Math.min(json2.length, offset + JSON_CHUNK_PAYLOAD_CHARS);
81319
+ while (end > offset) {
81320
+ const candidate = json2.slice(offset, end);
81321
+ const envelope = buildCommandResultChunkEnvelope(requestId, chunkId, chunks.length, MAX_JSON_CHUNKS, candidate);
81322
+ if (Buffer.byteLength(envelope, "utf8") <= MAX_INLINE_JSON_MESSAGE_BYTES) break;
81323
+ end = offset + Math.max(1, Math.floor((end - offset) * 0.8));
81324
+ }
81325
+ chunks.push(json2.slice(offset, end));
81326
+ if (chunks.length > MAX_JSON_CHUNKS) return [];
81327
+ offset = end;
81328
+ }
81329
+ return chunks;
81330
+ }
81066
81331
  function sendChunkedCommandResult(peer, peerId, requestId, json2) {
81067
81332
  const chunkId = requestId || `command_result_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
81068
- const total = Math.ceil(json2.length / JSON_CHUNK_PAYLOAD_CHARS);
81333
+ const chunks = splitJsonIntoCommandResultChunks(json2, requestId, chunkId);
81334
+ const total = chunks.length;
81335
+ if (total === 0 || total > MAX_JSON_CHUNKS) {
81336
+ log(`command_result send failed: peer=${peerId} id=${requestId || "-"} reason=too_many_chunks chunks=${total || "unknown"}`);
81337
+ return false;
81338
+ }
81069
81339
  logDebug(`command_result chunked: peer=${peerId} id=${requestId || "-"} chars=${json2.length} chunks=${total}`);
81070
81340
  for (let index = 0; index < total; index += 1) {
81071
- const chunk = json2.slice(index * JSON_CHUNK_PAYLOAD_CHARS, (index + 1) * JSON_CHUNK_PAYLOAD_CHARS);
81341
+ const chunk = chunks[index] || "";
81072
81342
  try {
81073
- peer.dataChannel?.sendMessage(JSON.stringify({
81074
- type: "command_result_chunk",
81075
- id: requestId,
81076
- chunkId,
81077
- index,
81078
- total,
81079
- data: chunk
81080
- }));
81343
+ const envelope = buildCommandResultChunkEnvelope(requestId, chunkId, index, total, chunk);
81344
+ if (Buffer.byteLength(envelope, "utf8") > MAX_INLINE_JSON_MESSAGE_BYTES) {
81345
+ log(`command_result send failed: peer=${peerId} id=${requestId || "-"} chunk=${index + 1}/${total} reason=chunk_too_large`);
81346
+ return false;
81347
+ }
81348
+ peer.dataChannel?.sendMessage(envelope);
81081
81349
  } catch (error48) {
81082
81350
  log(`command_result send failed: peer=${peerId} id=${requestId || "-"} chunk=${index + 1}/${total} error=${error48?.message || error48}`);
81083
81351
  return false;
@@ -81085,10 +81353,58 @@ function sendChunkedCommandResult(peer, peerId, requestId, json2) {
81085
81353
  }
81086
81354
  return true;
81087
81355
  }
81356
+ function parseChunkedJsonMessage(peerId, parsed) {
81357
+ const chunkType = typeof parsed?.type === "string" ? parsed.type : "";
81358
+ if (!chunkType.endsWith("_chunk")) return parsed;
81359
+ const chunkId = typeof parsed?.chunkId === "string" ? parsed.chunkId : "";
81360
+ const id = typeof parsed?.id === "string" ? parsed.id : "";
81361
+ const index = Number(parsed?.index);
81362
+ const total = Number(parsed?.total);
81363
+ const data = typeof parsed?.data === "string" ? parsed.data : "";
81364
+ if (!chunkId || !Number.isInteger(index) || !Number.isInteger(total) || index < 0 || total <= 0 || total > MAX_JSON_CHUNKS || index >= total || !data) {
81365
+ log(`chunked JSON ignored: peer=${peerId} type=${chunkType || "unknown"} reason=malformed`);
81366
+ return null;
81367
+ }
81368
+ const now = Date.now();
81369
+ incomingJsonChunks.forEach((entry2, key2) => {
81370
+ if (now - entry2.createdAt > JSON_CHUNK_TTL_MS) incomingJsonChunks.delete(key2);
81371
+ });
81372
+ const key = `${peerId}:${chunkId}`;
81373
+ let entry = incomingJsonChunks.get(key);
81374
+ if (!entry || entry.total !== total || entry.type !== chunkType) {
81375
+ entry = { total, chunks: new Array(total).fill(""), received: 0, createdAt: now, type: chunkType, id };
81376
+ incomingJsonChunks.set(key, entry);
81377
+ }
81378
+ if (!entry.chunks[index]) {
81379
+ entry.chunks[index] = data;
81380
+ entry.received += 1;
81381
+ }
81382
+ if (entry.received < entry.total) return null;
81383
+ incomingJsonChunks.delete(key);
81384
+ try {
81385
+ return JSON.parse(entry.chunks.join(""));
81386
+ } catch (error48) {
81387
+ log(`chunked JSON parse failed: peer=${peerId} type=${chunkType} id=${entry.id || "-"} error=${error48?.message || error48}`);
81388
+ return CHUNK_PARSE_FAILED;
81389
+ }
81390
+ }
81088
81391
  function routeDataChannelMessage(peerId, msg, peers, handlers) {
81089
81392
  const text = typeof msg === "string" ? msg : msg.toString("utf-8");
81090
81393
  try {
81091
- const parsed = JSON.parse(text);
81394
+ let parsed = JSON.parse(text);
81395
+ const chunked = parseChunkedJsonMessage(peerId, parsed);
81396
+ if (chunked === null) return;
81397
+ if (chunked === CHUNK_PARSE_FAILED) {
81398
+ const peer = peers.get(peerId);
81399
+ const id = typeof parsed?.id === "string" ? parsed.id : "";
81400
+ if (parsed?.type === "command_chunk") {
81401
+ sendToPeer(peer, { type: "command_result", id, success: false, error: "Failed to reassemble chunked command" });
81402
+ } else if (id) {
81403
+ sendToPeer(peer, { id, type: "response", success: false, error: "Failed to reassemble chunked request" });
81404
+ }
81405
+ return;
81406
+ }
81407
+ parsed = chunked;
81092
81408
  if (parsed.type !== "command" && parsed.type !== "pty_input" && parsed.type !== "pty_resize" && parsed.type !== "ping" && parsed.type !== "pong") {
81093
81409
  logDebug(`Files message from peer ${peerId}: type=${parsed.type}`);
81094
81410
  }
@@ -81384,7 +81700,7 @@ async function handleFileRequest(peerId, req, peers, handlers) {
81384
81700
  }
81385
81701
  sendToPeer(peer, response);
81386
81702
  }
81387
- var MAX_INLINE_JSON_MESSAGE_CHARS, JSON_CHUNK_PAYLOAD_CHARS;
81703
+ var MAX_INLINE_JSON_MESSAGE_CHARS, MAX_INLINE_JSON_MESSAGE_BYTES, JSON_CHUNK_PAYLOAD_CHARS, JSON_CHUNK_TTL_MS, MAX_JSON_CHUNKS, CHUNK_PARSE_FAILED, incomingJsonChunks;
81388
81704
  var init_data_channel_router = __esm({
81389
81705
  "src/daemon-p2p/data-channel-router.ts"() {
81390
81706
  "use strict";
@@ -81392,18 +81708,23 @@ var init_data_channel_router = __esm({
81392
81708
  init_permission();
81393
81709
  init_log();
81394
81710
  MAX_INLINE_JSON_MESSAGE_CHARS = 6e4;
81395
- JSON_CHUNK_PAYLOAD_CHARS = 32e3;
81711
+ MAX_INLINE_JSON_MESSAGE_BYTES = 6e4;
81712
+ JSON_CHUNK_PAYLOAD_CHARS = 16e3;
81713
+ JSON_CHUNK_TTL_MS = 6e4;
81714
+ MAX_JSON_CHUNKS = 1024;
81715
+ CHUNK_PARSE_FAILED = /* @__PURE__ */ Symbol("chunk_parse_failed");
81716
+ incomingJsonChunks = /* @__PURE__ */ new Map();
81396
81717
  }
81397
81718
  });
81398
81719
 
81399
81720
  // src/daemon-p2p/screenshot-sender.ts
81400
- var CHUNK_SIZE, MAX_INLINE_JSON_MESSAGE_BYTES, JSON_CHUNK_PAYLOAD_CHARS2, ScreenshotSender;
81721
+ var CHUNK_SIZE, MAX_INLINE_JSON_MESSAGE_BYTES2, JSON_CHUNK_PAYLOAD_CHARS2, ScreenshotSender;
81401
81722
  var init_screenshot_sender = __esm({
81402
81723
  "src/daemon-p2p/screenshot-sender.ts"() {
81403
81724
  "use strict";
81404
81725
  init_log();
81405
81726
  CHUNK_SIZE = 6e4;
81406
- MAX_INLINE_JSON_MESSAGE_BYTES = 6e4;
81727
+ MAX_INLINE_JSON_MESSAGE_BYTES2 = 6e4;
81407
81728
  JSON_CHUNK_PAYLOAD_CHARS2 = 16e3;
81408
81729
  ScreenshotSender = class {
81409
81730
  _ssDebugDone = false;
@@ -81447,7 +81768,7 @@ var init_screenshot_sender = __esm({
81447
81768
  type: "topic_update",
81448
81769
  update
81449
81770
  });
81450
- if (Buffer.byteLength(json2, "utf8") > MAX_INLINE_JSON_MESSAGE_BYTES) {
81771
+ if (Buffer.byteLength(json2, "utf8") > MAX_INLINE_JSON_MESSAGE_BYTES2) {
81451
81772
  return this.sendChunkedTopicUpdate(peer, update, json2);
81452
81773
  }
81453
81774
  try {
@@ -81486,6 +81807,10 @@ var init_screenshot_sender = __esm({
81486
81807
  let sentAny = false;
81487
81808
  for (const peer of peers.values()) {
81488
81809
  if (peer.state !== "connected" || !peer.dataChannel?.isOpen()) continue;
81810
+ if (Buffer.byteLength(msg, "utf8") > MAX_INLINE_JSON_MESSAGE_BYTES2) {
81811
+ if (this.sendChunkedSessionOutput(peer, sessionId, msg)) sentAny = true;
81812
+ continue;
81813
+ }
81489
81814
  try {
81490
81815
  peer.dataChannel.sendMessage(msg);
81491
81816
  sentAny = true;
@@ -81494,6 +81819,27 @@ var init_screenshot_sender = __esm({
81494
81819
  }
81495
81820
  return sentAny;
81496
81821
  }
81822
+ sendChunkedSessionOutput(peer, sessionId, json2) {
81823
+ const chunkId = `session_output:${sessionId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
81824
+ const total = Math.ceil(json2.length / JSON_CHUNK_PAYLOAD_CHARS2);
81825
+ logDebug(`session_output chunked: peer=${peer.peerId || "unknown"} sessionId=${sessionId || "-"} chars=${json2.length} chunks=${total}`);
81826
+ for (let index = 0; index < total; index += 1) {
81827
+ const chunk = json2.slice(index * JSON_CHUNK_PAYLOAD_CHARS2, (index + 1) * JSON_CHUNK_PAYLOAD_CHARS2);
81828
+ try {
81829
+ peer.dataChannel?.sendMessage(JSON.stringify({
81830
+ type: "session_output_chunk",
81831
+ sessionId,
81832
+ chunkId,
81833
+ index,
81834
+ total,
81835
+ data: chunk
81836
+ }));
81837
+ } catch {
81838
+ return false;
81839
+ }
81840
+ }
81841
+ return true;
81842
+ }
81497
81843
  sendScreenshot(peers, base64Data, targetSessionId) {
81498
81844
  const buffer = Buffer.from(base64Data, "base64");
81499
81845
  return this.sendScreenshotBuffer(peers, buffer, targetSessionId);
@@ -89833,7 +90179,7 @@ var init_adhdev_daemon = __esm({
89833
90179
  init_version();
89834
90180
  init_src();
89835
90181
  init_runtime_defaults();
89836
- pkgVersion = resolvePackageVersion({ injectedVersion: "0.9.54" });
90182
+ pkgVersion = resolvePackageVersion({ injectedVersion: "0.9.56" });
89837
90183
  AdhdevDaemon = class _AdhdevDaemon {
89838
90184
  localHttpServer = null;
89839
90185
  localWss = null;
@@ -89857,6 +90203,19 @@ var init_adhdev_daemon = __esm({
89857
90203
  sessionHostEndpoint = null;
89858
90204
  sessionHostController = null;
89859
90205
  gitWorkspaceMonitor = createGitWorkspaceMonitor();
90206
+ turnSnapshotTracker = new TurnSnapshotTracker(({ sessionId, workspace }) => {
90207
+ const gitServices = this.components?.commandHandler?.ctx?.gitCommandServices;
90208
+ if (gitServices?.createSnapshot) {
90209
+ void Promise.resolve(gitServices.createSnapshot({
90210
+ workspace,
90211
+ reason: "after_agent_work",
90212
+ sessionId
90213
+ })).catch(() => {
90214
+ });
90215
+ }
90216
+ void this.gitWorkspaceMonitor.refresh({ workspace, includeDiffSummary: false }).catch(() => {
90217
+ });
90218
+ });
89860
90219
  running = false;
89861
90220
  localPort;
89862
90221
  ideType = "unknown";
@@ -90028,7 +90387,8 @@ var init_adhdev_daemon = __esm({
90028
90387
  })),
90029
90388
  instanceId: `daemon_${loadConfig().machineId || "daemon"}`,
90030
90389
  version: pkgVersion,
90031
- profile: "live"
90390
+ profile: "live",
90391
+ getGitSummaryForWorkspace: (workspace) => this.gitWorkspaceMonitor.getCompactSummary(workspace)
90032
90392
  });
90033
90393
  }
90034
90394
  invalidateHotChatSnapshotCache() {
@@ -90214,7 +90574,8 @@ var init_adhdev_daemon = __esm({
90214
90574
  })),
90215
90575
  instanceId: `daemon_${loadConfig().machineId || "daemon"}`,
90216
90576
  version: pkgVersion,
90217
- profile: "metadata"
90577
+ profile: "metadata",
90578
+ getGitSummaryForWorkspace: (workspace) => this.gitWorkspaceMonitor.getCompactSummary(workspace)
90218
90579
  });
90219
90580
  }
90220
90581
  buildDaemonMetadataUpdateForSubscription(subscription) {
@@ -90358,6 +90719,27 @@ ${err?.stack || ""}`);
90358
90719
  onStreamsUpdated: (ideType, streams) => {
90359
90720
  if (!this.components) return;
90360
90721
  forwardAgentStreamsToIdeInstance(this.components.instanceManager, ideType, streams);
90722
+ for (const stream of streams) {
90723
+ if (stream.sessionId) {
90724
+ const workspace = this.components?.instanceManager?.getInstance(stream.sessionId)?.getState()?.workspace;
90725
+ this.turnSnapshotTracker.record(stream.sessionId, stream.status || "idle", workspace);
90726
+ }
90727
+ }
90728
+ },
90729
+ onBeforeSendChat: ({ workspace }) => {
90730
+ void this.gitWorkspaceMonitor.refresh({
90731
+ workspace,
90732
+ includeDiffSummary: false
90733
+ }).catch(() => {
90734
+ });
90735
+ const gitServices = this.components?.commandHandler?.ctx?.gitCommandServices;
90736
+ if (gitServices?.createSnapshot) {
90737
+ void Promise.resolve(gitServices.createSnapshot({
90738
+ workspace,
90739
+ reason: "before_user_input_dispatch"
90740
+ })).catch(() => {
90741
+ });
90742
+ }
90361
90743
  }
90362
90744
  });
90363
90745
  const providerSourceConfig = this.components.providerLoader.getSourceConfig();