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/index.js CHANGED
@@ -963,7 +963,12 @@ function createDefaultGitCommandServices() {
963
963
  turnId
964
964
  }),
965
965
  compareSnapshots: ({ beforeSnapshotId, afterSnapshotId }) => defaultSnapshotStore.compare(beforeSnapshotId, afterSnapshotId),
966
- getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until })
966
+ getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until }),
967
+ checkpoint: async ({ workspace, message, includeUntracked = false }) => gitCheckpoint(workspace, message, includeUntracked),
968
+ stashPush: async ({ workspace, message, includeUntracked = false }) => gitStashPush(workspace, message, includeUntracked),
969
+ stashPop: async ({ workspace, stashRef }) => gitStashPop(workspace, stashRef),
970
+ checkoutFiles: async ({ workspace, paths }) => gitCheckoutFiles(workspace, paths),
971
+ getRemoteUrl: async ({ workspace, remote = "origin" }) => gitGetRemoteUrl(workspace, remote)
967
972
  };
968
973
  }
969
974
  function validateWorkspace2(args) {
@@ -1026,9 +1031,6 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
1026
1031
  if (!isGitCommandName(command)) {
1027
1032
  return failure("invalid_args", `Unknown Git command: ${command}`);
1028
1033
  }
1029
- if (MUTATING_COMMAND_NAMES.has(command)) {
1030
- return failure("invalid_args", `${command} is not implemented in daemon-core read-only Git routing`);
1031
- }
1032
1034
  const workspaceResult = validateWorkspace2(args);
1033
1035
  if ("success" in workspaceResult) return workspaceResult;
1034
1036
  const { workspace } = workspaceResult;
@@ -1088,10 +1090,153 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
1088
1090
  }));
1089
1091
  return "success" in log2 ? log2 : { success: true, log: log2 };
1090
1092
  }
1093
+ case "git_checkpoint": {
1094
+ if (!services.checkpoint) return serviceNotImplemented(command);
1095
+ const msg = validateMutatingMessage(args?.message);
1096
+ if (typeof msg !== "string") return msg;
1097
+ const includeUntracked = Boolean(args?.includeUntracked);
1098
+ const checkpoint = await runService(() => services.checkpoint({ workspace, message: msg, includeUntracked }));
1099
+ return "success" in checkpoint ? checkpoint : { success: true, checkpoint };
1100
+ }
1101
+ case "git_stash_push": {
1102
+ if (!services.stashPush) return serviceNotImplemented(command);
1103
+ const msg = validateMutatingMessage(args?.message);
1104
+ if (typeof msg !== "string") return msg;
1105
+ const includeUntracked = Boolean(args?.includeUntracked);
1106
+ const stash = await runService(() => services.stashPush({ workspace, message: msg, includeUntracked }));
1107
+ return "success" in stash ? stash : { success: true, stash };
1108
+ }
1109
+ case "git_stash_pop": {
1110
+ if (!services.stashPop) return serviceNotImplemented(command);
1111
+ const stashRef = optionalString(args?.stashRef);
1112
+ if (stashRef !== void 0 && !/^stash@\{\d+\}$/.test(stashRef)) {
1113
+ return failure("invalid_args", "stashRef must match stash@{N} format");
1114
+ }
1115
+ const popResult = await runService(() => services.stashPop({ workspace, stashRef }));
1116
+ if (popResult !== void 0 && "success" in popResult) return popResult;
1117
+ return { success: true, stashPopped: true };
1118
+ }
1119
+ case "git_checkout_files": {
1120
+ if (!services.checkoutFiles) return serviceNotImplemented(command);
1121
+ const paths = args?.paths;
1122
+ if (!Array.isArray(paths) || paths.length === 0) {
1123
+ return failure("invalid_args", "paths must be a non-empty array");
1124
+ }
1125
+ if (paths.length > 50) {
1126
+ return failure("invalid_args", "paths array exceeds maximum of 50 entries");
1127
+ }
1128
+ const checkoutResult = await runService(() => services.checkoutFiles({ workspace, paths }));
1129
+ return "success" in checkoutResult ? checkoutResult : { success: true, checkedOut: checkoutResult.checkedOut };
1130
+ }
1131
+ case "git_remote_url": {
1132
+ if (!services.getRemoteUrl) return serviceNotImplemented(command);
1133
+ const remote = typeof args?.remote === "string" && args.remote.trim() ? args.remote.trim() : "origin";
1134
+ const remoteResult = await runService(() => services.getRemoteUrl({ workspace, remote }));
1135
+ if ("success" in remoteResult) return remoteResult;
1136
+ return { success: true, remoteUrl: remoteResult.remoteUrl, remote: remoteResult.remote };
1137
+ }
1091
1138
  default:
1092
1139
  return failure("invalid_args", `Unknown Git command: ${command}`);
1093
1140
  }
1094
1141
  }
1142
+ function validateMutatingMessage(value) {
1143
+ if (typeof value !== "string" || !value.trim()) {
1144
+ return failure("invalid_args", "message must be a non-empty string");
1145
+ }
1146
+ const msg = value.trim();
1147
+ if (msg.length > 200) {
1148
+ return failure("invalid_args", "message must be 200 characters or fewer");
1149
+ }
1150
+ return msg;
1151
+ }
1152
+ async function gitCheckpoint(workspace, message, includeUntracked) {
1153
+ const repo = await resolveGitRepository(workspace);
1154
+ const repoRoot = repo.repoRoot;
1155
+ const statusResult = await getGitRepoStatus(workspace);
1156
+ if (statusResult.hasConflicts) {
1157
+ throw new GitCommandError("conflict", "Repository has conflicts \u2014 resolve before checkpointing");
1158
+ }
1159
+ const addArgs = includeUntracked ? ["-A"] : ["-u"];
1160
+ await runGit(repo, ["add", ...addArgs], { cwd: repoRoot });
1161
+ const fullMsg = `adhdev: checkpoint ${message}`;
1162
+ let commitSha;
1163
+ try {
1164
+ await runGit(repo, ["commit", "-m", fullMsg], { cwd: repoRoot });
1165
+ const revResult = await runGit(repo, ["rev-parse", "HEAD"], { cwd: repoRoot });
1166
+ commitSha = revResult.stdout.trim();
1167
+ } catch (err) {
1168
+ const output = (err?.stdout || "") + (err?.stderr || "");
1169
+ if (/nothing to commit/i.test(output)) {
1170
+ throw new GitCommandError("git_command_failed", "Nothing to commit");
1171
+ }
1172
+ throw err;
1173
+ }
1174
+ return {
1175
+ workspace: repo.workspace,
1176
+ repoRoot,
1177
+ isGitRepo: true,
1178
+ commit: commitSha,
1179
+ message: fullMsg,
1180
+ lastCheckedAt: Date.now()
1181
+ };
1182
+ }
1183
+ async function gitStashPush(workspace, message, includeUntracked) {
1184
+ const repo = await resolveGitRepository(workspace);
1185
+ const repoRoot = repo.repoRoot;
1186
+ const stashArgs = ["stash", "push", "-m", message];
1187
+ if (includeUntracked) stashArgs.push("--include-untracked");
1188
+ const result = await runGit(repo, stashArgs, { cwd: repoRoot });
1189
+ if (/No local changes to save/i.test(result.stdout + result.stderr)) {
1190
+ throw new GitCommandError("git_command_failed", "Nothing to stash");
1191
+ }
1192
+ return {
1193
+ workspace: repo.workspace,
1194
+ repoRoot,
1195
+ isGitRepo: true,
1196
+ stashRef: "stash@{0}",
1197
+ message,
1198
+ lastCheckedAt: Date.now()
1199
+ };
1200
+ }
1201
+ async function gitStashPop(workspace, stashRef) {
1202
+ const repo = await resolveGitRepository(workspace);
1203
+ const repoRoot = repo.repoRoot;
1204
+ const popArgs = stashRef ? ["stash", "pop", stashRef] : ["stash", "pop"];
1205
+ await runGit(repo, popArgs, { cwd: repoRoot });
1206
+ }
1207
+ async function gitCheckoutFiles(workspace, paths) {
1208
+ const repo = await resolveGitRepository(workspace);
1209
+ const repoRoot = repo.repoRoot;
1210
+ const normalizedPaths = [];
1211
+ for (const p of paths) {
1212
+ if (typeof p !== "string" || !p.trim() || p.includes("\0")) {
1213
+ throw new GitCommandError("invalid_args", `Invalid path: ${String(p)}`);
1214
+ }
1215
+ if (path3.isAbsolute(p)) {
1216
+ throw new GitCommandError("invalid_args", `Path must be repository-relative, not absolute: ${p}`);
1217
+ }
1218
+ const normalized = path3.normalize(p.trim()).split(path3.sep).join("/");
1219
+ if (normalized.startsWith("../") || normalized === "..") {
1220
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
1221
+ }
1222
+ const absolutePath = path3.resolve(repoRoot, normalized);
1223
+ if (!isPathInside(repoRoot, absolutePath)) {
1224
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
1225
+ }
1226
+ normalizedPaths.push(normalized);
1227
+ }
1228
+ await runGit(repo, ["checkout", "--", ...normalizedPaths], { cwd: repoRoot });
1229
+ return { checkedOut: normalizedPaths };
1230
+ }
1231
+ async function gitGetRemoteUrl(workspace, remote) {
1232
+ const repo = await resolveGitRepository(workspace);
1233
+ const result = await runGit(repo, ["remote", "get-url", remote], { cwd: repo.repoRoot });
1234
+ const remoteUrl = result.stdout.trim();
1235
+ if (!remoteUrl) {
1236
+ throw new GitCommandError("git_command_failed", `Remote '${remote}' has no URL`);
1237
+ }
1238
+ return { remoteUrl, remote };
1239
+ }
1095
1240
  function formatOptionalGitLogRangeArg(flag, value) {
1096
1241
  return value ? [`${flag}=${value}`] : [];
1097
1242
  }
@@ -1149,7 +1294,7 @@ function validateGitLogPath(repoRoot, filePath) {
1149
1294
  }
1150
1295
  return normalized;
1151
1296
  }
1152
- var path3, GIT_COMMAND_NAMES, MUTATING_COMMAND_NAMES, SNAPSHOT_REASONS, FAILURE_REASONS, defaultSnapshotStore, defaultGitCommandServices;
1297
+ var path3, GIT_COMMAND_NAMES, SNAPSHOT_REASONS, FAILURE_REASONS, defaultSnapshotStore, defaultGitCommandServices;
1153
1298
  var init_git_commands = __esm({
1154
1299
  "../../oss/packages/daemon-core/src/git/git-commands.ts"() {
1155
1300
  "use strict";
@@ -1168,13 +1313,8 @@ var init_git_commands = __esm({
1168
1313
  "git_checkpoint",
1169
1314
  "git_stash_push",
1170
1315
  "git_stash_pop",
1171
- "git_checkout_files"
1172
- ]);
1173
- MUTATING_COMMAND_NAMES = /* @__PURE__ */ new Set([
1174
- "git_checkpoint",
1175
- "git_stash_push",
1176
- "git_stash_pop",
1177
- "git_checkout_files"
1316
+ "git_checkout_files",
1317
+ "git_remote_url"
1178
1318
  ]);
1179
1319
  SNAPSHOT_REASONS = /* @__PURE__ */ new Set([
1180
1320
  "session_baseline",
@@ -1201,6 +1341,33 @@ var init_git_commands = __esm({
1201
1341
  }
1202
1342
  });
1203
1343
 
1344
+ // ../../oss/packages/daemon-core/src/git/turn-snapshot-tracker.ts
1345
+ var BUSY_STATUSES, TERMINAL_STATUSES, TurnSnapshotTracker;
1346
+ var init_turn_snapshot_tracker = __esm({
1347
+ "../../oss/packages/daemon-core/src/git/turn-snapshot-tracker.ts"() {
1348
+ "use strict";
1349
+ BUSY_STATUSES = /* @__PURE__ */ new Set(["streaming", "waiting_approval"]);
1350
+ TERMINAL_STATUSES = /* @__PURE__ */ new Set(["idle", "error"]);
1351
+ TurnSnapshotTracker = class {
1352
+ lastStatus = /* @__PURE__ */ new Map();
1353
+ onTurnCompleted;
1354
+ constructor(onTurnCompleted) {
1355
+ this.onTurnCompleted = onTurnCompleted;
1356
+ }
1357
+ record(sessionId, status, workspace) {
1358
+ const prev = this.lastStatus.get(sessionId);
1359
+ this.lastStatus.set(sessionId, status);
1360
+ if (workspace && prev && BUSY_STATUSES.has(prev) && TERMINAL_STATUSES.has(status)) {
1361
+ this.onTurnCompleted({ sessionId, workspace });
1362
+ }
1363
+ }
1364
+ forget(sessionId) {
1365
+ this.lastStatus.delete(sessionId);
1366
+ }
1367
+ };
1368
+ }
1369
+ });
1370
+
1204
1371
  // ../../oss/packages/daemon-core/src/git/index.ts
1205
1372
  var init_git = __esm({
1206
1373
  "../../oss/packages/daemon-core/src/git/index.ts"() {
@@ -1212,6 +1379,7 @@ var init_git = __esm({
1212
1379
  init_git_snapshot_store();
1213
1380
  init_git_monitor();
1214
1381
  init_git_commands();
1382
+ init_turn_snapshot_tracker();
1215
1383
  }
1216
1384
  });
1217
1385
 
@@ -11367,6 +11535,16 @@ var init_handler = __esm({
11367
11535
  return result;
11368
11536
  }
11369
11537
  }
11538
+ if (cmd === "send_chat" && this._ctx.onBeforeSendChat) {
11539
+ const sessionId = this._currentRoute.session?.sessionId;
11540
+ const workspace = sessionId ? this._ctx.instanceManager?.getInstance(sessionId)?.getState?.()?.workspace : void 0;
11541
+ if (workspace && sessionId) {
11542
+ try {
11543
+ this._ctx.onBeforeSendChat({ workspace, sessionId });
11544
+ } catch {
11545
+ }
11546
+ }
11547
+ }
11370
11548
  try {
11371
11549
  result = await this.dispatch(cmd, args);
11372
11550
  this.logCommandEnd(cmd, result, startedAt);
@@ -13207,6 +13385,7 @@ function resolveCliAdapterConfig(provider) {
13207
13385
  sendDelayMs: typeof provider.sendDelayMs === "number" ? Math.max(0, provider.sendDelayMs) : 0,
13208
13386
  sendKey: typeof provider.sendKey === "string" && provider.sendKey.length > 0 ? provider.sendKey : "\r",
13209
13387
  submitStrategy: provider.submitStrategy === "immediate" ? "immediate" : "wait_for_echo",
13388
+ requirePromptEchoBeforeSubmit: provider.requirePromptEchoBeforeSubmit === true,
13210
13389
  providerResolutionMeta: {
13211
13390
  type: provider.type,
13212
13391
  name: provider.name,
@@ -13472,6 +13651,7 @@ var init_provider_cli_adapter = __esm({
13472
13651
  this.sendDelayMs = resolvedConfig.sendDelayMs;
13473
13652
  this.sendKey = resolvedConfig.sendKey;
13474
13653
  this.submitStrategy = resolvedConfig.submitStrategy;
13654
+ this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
13475
13655
  this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
13476
13656
  this.cliScripts = provider.scripts || {};
13477
13657
  const scriptNames = listCliScriptNames(this.cliScripts);
@@ -13568,6 +13748,12 @@ var init_provider_cli_adapter = __esm({
13568
13748
  accumulatedRawBuffer = "";
13569
13749
  /** Current visible terminal screen snapshot */
13570
13750
  terminalScreen = new TerminalScreen(24, 80);
13751
+ static MAX_RESPONSE_BUFFER = 8e3;
13752
+ static MAX_RECENT_OUTPUT_BUFFER = 1e3;
13753
+ responseBufferDroppedChars = 0;
13754
+ recentOutputDroppedChars = 0;
13755
+ accumulatedBufferDroppedChars = 0;
13756
+ accumulatedRawBufferDroppedChars = 0;
13571
13757
  /** Max accumulated buffer size. Sized to comfortably hold a single long
13572
13758
  * Hermes turn (tool calls + reasoning + final bubble) without the
13573
13759
  * rolling window pushing the turn's ╭─ opening line out of view. */
@@ -13585,6 +13771,23 @@ var init_provider_cli_adapter = __esm({
13585
13771
  providerResolutionMeta;
13586
13772
  static FINISH_RETRY_DELAY_MS = 300;
13587
13773
  static MAX_FINISH_RETRIES = 2;
13774
+ getBufferState() {
13775
+ const build = (droppedChars, maxChars) => droppedChars > 0 ? { truncated: true, droppedChars, maxChars } : void 0;
13776
+ const responseBuffer = build(this.responseBufferDroppedChars, _ProviderCliAdapter.MAX_RESPONSE_BUFFER);
13777
+ const recentOutputBuffer = build(this.recentOutputDroppedChars, _ProviderCliAdapter.MAX_RECENT_OUTPUT_BUFFER);
13778
+ const accumulatedBuffer = build(this.accumulatedBufferDroppedChars, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
13779
+ const accumulatedRawBuffer = build(this.accumulatedRawBufferDroppedChars, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
13780
+ if (!responseBuffer && !recentOutputBuffer && !accumulatedBuffer && !accumulatedRawBuffer) return void 0;
13781
+ return {
13782
+ ...responseBuffer ? { responseBuffer } : {},
13783
+ ...recentOutputBuffer ? { recentOutputBuffer } : {},
13784
+ ...accumulatedBuffer ? { accumulatedBuffer } : {},
13785
+ ...accumulatedRawBuffer ? { accumulatedRawBuffer } : {}
13786
+ };
13787
+ }
13788
+ recordBoundedAppendDrop(previousLength, appendedLength, nextLength) {
13789
+ return Math.max(0, previousLength + appendedLength - nextLength);
13790
+ }
13588
13791
  buildCommittedMessagesActivitySignature() {
13589
13792
  const last = this.committedMessages[this.committedMessages.length - 1];
13590
13793
  return [
@@ -13768,6 +13971,7 @@ var init_provider_cli_adapter = __esm({
13768
13971
  sendDelayMs;
13769
13972
  sendKey;
13770
13973
  submitStrategy;
13974
+ requirePromptEchoBeforeSubmit;
13771
13975
  static SCRIPT_STATUS_DEBOUNCE_MS = 3e3;
13772
13976
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
13773
13977
  setCliScripts(scripts) {
@@ -13949,7 +14153,9 @@ var init_provider_cli_adapter = __esm({
13949
14153
  this.scheduleStartupSettleCheck();
13950
14154
  }
13951
14155
  if (this.isWaitingForResponse && cleanData) {
13952
- this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, 8e3);
14156
+ const previousResponseLen = this.responseBuffer.length;
14157
+ this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, _ProviderCliAdapter.MAX_RESPONSE_BUFFER);
14158
+ this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
13953
14159
  }
13954
14160
  if (cleanData.trim()) {
13955
14161
  if (this.serverConn) {
@@ -13958,14 +14164,19 @@ var init_provider_cli_adapter = __esm({
13958
14164
  this.logBuffer.push({ message: cleanData.trim(), level: "info" });
13959
14165
  }
13960
14166
  }
14167
+ const prevRecentLen = this.recentOutputBuffer.length;
13961
14168
  const prevAccumulatedLen = this.accumulatedBuffer.length;
13962
14169
  const prevAccumulatedRawLen = this.accumulatedRawBuffer.length;
13963
- this.recentOutputBuffer = appendBoundedText(this.recentOutputBuffer, cleanData, 1e3);
14170
+ this.recentOutputBuffer = appendBoundedText(this.recentOutputBuffer, cleanData, _ProviderCliAdapter.MAX_RECENT_OUTPUT_BUFFER);
13964
14171
  this.accumulatedBuffer = appendBoundedText(this.accumulatedBuffer, cleanData, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
13965
14172
  this.accumulatedRawBuffer = appendBoundedText(this.accumulatedRawBuffer, rawData, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
14173
+ const droppedRecent = this.recordBoundedAppendDrop(prevRecentLen, cleanData.length, this.recentOutputBuffer.length);
14174
+ const droppedClean = this.recordBoundedAppendDrop(prevAccumulatedLen, cleanData.length, this.accumulatedBuffer.length);
14175
+ const droppedRaw = this.recordBoundedAppendDrop(prevAccumulatedRawLen, rawData.length, this.accumulatedRawBuffer.length);
14176
+ this.recentOutputDroppedChars += droppedRecent;
14177
+ this.accumulatedBufferDroppedChars += droppedClean;
14178
+ this.accumulatedRawBufferDroppedChars += droppedRaw;
13966
14179
  if (this.currentTurnScope) {
13967
- const droppedClean = prevAccumulatedLen + cleanData.length - this.accumulatedBuffer.length;
13968
- const droppedRaw = prevAccumulatedRawLen + rawData.length - this.accumulatedRawBuffer.length;
13969
14180
  if (droppedClean > 0) {
13970
14181
  this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
13971
14182
  }
@@ -14795,13 +15006,15 @@ var init_provider_cli_adapter = __esm({
14795
15006
  effectiveModal = parsedModal;
14796
15007
  }
14797
15008
  }
15009
+ const bufferState = this.getBufferState();
14798
15010
  return {
14799
15011
  status: effectiveStatus,
14800
15012
  messages: [...this.committedMessages],
14801
15013
  workingDir: this.workingDir,
14802
15014
  activeModal: effectiveModal,
14803
15015
  errorMessage: this.parseErrorMessage || void 0,
14804
- errorReason: this.parseErrorMessage ? "parse_error" : void 0
15016
+ errorReason: this.parseErrorMessage ? "parse_error" : void 0,
15017
+ ...bufferState ? { bufferState } : {}
14805
15018
  };
14806
15019
  }
14807
15020
  seedCommittedMessages(messages) {
@@ -14983,10 +15196,12 @@ var init_provider_cli_adapter = __esm({
14983
15196
  messages: hydratedMessages,
14984
15197
  activeModal: parsed.activeModal ?? this.activeModal,
14985
15198
  providerSessionId: typeof parsed.providerSessionId === "string" ? parsed.providerSessionId : void 0,
15199
+ ...this.getBufferState() ? { bufferState: this.getBufferState() } : {},
14986
15200
  ...this.providerOwnsTranscript() ? { transcriptAuthority: "provider", coverage: this.shouldUseFullProviderTranscriptContext() ? "full" : "tail" } : {}
14987
15201
  };
14988
15202
  } else {
14989
15203
  const messages = [...this.committedMessages];
15204
+ const bufferState = this.getBufferState();
14990
15205
  result = {
14991
15206
  id: "cli_session",
14992
15207
  status: this.currentStatus,
@@ -14997,7 +15212,8 @@ var init_provider_cli_adapter = __esm({
14997
15212
  index: typeof message.index === "number" ? message.index : index,
14998
15213
  receivedAt: typeof message.receivedAt === "number" ? message.receivedAt : message.timestamp
14999
15214
  })),
15000
- activeModal: this.activeModal
15215
+ activeModal: this.activeModal,
15216
+ ...bufferState ? { bufferState } : {}
15001
15217
  };
15002
15218
  }
15003
15219
  const hasVisibleAssistantMessage = Array.isArray(result?.messages) && result.messages.some((message) => message?.role === "assistant" && typeof message?.content === "string" && message.content.trim());
@@ -15288,6 +15504,22 @@ var init_provider_cli_adapter = __esm({
15288
15504
  }
15289
15505
  }
15290
15506
  if (elapsed >= state.maxEchoWaitMs) {
15507
+ const diagnostic = {
15508
+ elapsed,
15509
+ maxEchoWaitMs: state.maxEchoWaitMs,
15510
+ submitDelayMs: state.submitDelayMs,
15511
+ promptSnippet: state.normalizedPromptSnippet,
15512
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
15513
+ screenText: summarizeCliTraceText(screenText, 1e3)
15514
+ };
15515
+ this.recordTrace("submit_echo_missing", diagnostic);
15516
+ if (this.requirePromptEchoBeforeSubmit) {
15517
+ const message = `${this.cliName} prompt echo was not observed on the PTY screen before submit`;
15518
+ LOG.warn("CLI", `[${this.cliType}] ${message} elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs} screen=${JSON.stringify(diagnostic.screenText).slice(0, 240)}`);
15519
+ completion.rejectOnce(new Error(message));
15520
+ return;
15521
+ }
15522
+ LOG.warn("CLI", `[${this.cliType}] prompt echo was not observed before submit; sending submit key anyway elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs}`);
15291
15523
  this.submitSendKey(state, completion);
15292
15524
  return;
15293
15525
  }
@@ -15752,6 +15984,7 @@ var init_provider_cli_adapter = __esm({
15752
15984
  sendDelayMs: this.sendDelayMs,
15753
15985
  sendKey: this.sendKey,
15754
15986
  submitStrategy: this.submitStrategy,
15987
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
15755
15988
  submitPendingUntil: this.submitPendingUntil,
15756
15989
  responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
15757
15990
  resizeSuppressUntil: this.resizeSuppressUntil,
@@ -37052,6 +37285,7 @@ var init_provider_schema = __esm({
37052
37285
  "sendDelayMs",
37053
37286
  "sendKey",
37054
37287
  "submitStrategy",
37288
+ "requirePromptEchoBeforeSubmit",
37055
37289
  "timeouts",
37056
37290
  "disableUpstream"
37057
37291
  ]);
@@ -48627,6 +48861,7 @@ async function initDaemonComponents(config2) {
48627
48861
  providerLoader,
48628
48862
  instanceManager,
48629
48863
  sessionRegistry,
48864
+ gitCommandServices: createDefaultGitCommandServices(),
48630
48865
  onProviderSettingChanged: async (providerType) => {
48631
48866
  await refreshProviderAvailability(providerType);
48632
48867
  config2.onStatusChange?.();
@@ -48634,7 +48869,8 @@ async function initDaemonComponents(config2) {
48634
48869
  onProviderSourceConfigChanged: async () => {
48635
48870
  await refreshProviderAvailability();
48636
48871
  config2.onStatusChange?.();
48637
- }
48872
+ },
48873
+ onBeforeSendChat: config2.onBeforeSendChat
48638
48874
  });
48639
48875
  agentStreamManager = new DaemonAgentStreamManager(
48640
48876
  LOG.forComponent("AgentStream").asLogFn(),
@@ -48760,6 +48996,7 @@ var init_daemon_lifecycle = __esm({
48760
48996
  init_logger();
48761
48997
  init_runtime_defaults();
48762
48998
  init_config();
48999
+ init_git_commands();
48763
49000
  }
48764
49001
  });
48765
49002
 
@@ -48810,6 +49047,7 @@ __export(src_exports, {
48810
49047
  ProviderLoader: () => ProviderLoader,
48811
49048
  STANDALONE_CDP_SCAN_INTERVAL_MS: () => STANDALONE_CDP_SCAN_INTERVAL_MS,
48812
49049
  SessionHostPtyTransportFactory: () => SessionHostPtyTransportFactory,
49050
+ TurnSnapshotTracker: () => TurnSnapshotTracker,
48813
49051
  VersionArchive: () => VersionArchive,
48814
49052
  appendRecentActivity: () => appendRecentActivity,
48815
49053
  buildAssistantChatMessage: () => buildAssistantChatMessage,
@@ -49414,7 +49652,7 @@ function sendToPeer(peer, data) {
49414
49652
  return false;
49415
49653
  }
49416
49654
  const json2 = JSON.stringify(data);
49417
- if (messageType === "command_result" && json2.length > MAX_INLINE_JSON_MESSAGE_CHARS) {
49655
+ if (messageType === "command_result" && Buffer.byteLength(json2, "utf8") > MAX_INLINE_JSON_MESSAGE_CHARS) {
49418
49656
  return sendChunkedCommandResult(peer, peerId, requestId, json2);
49419
49657
  }
49420
49658
  try {
@@ -49427,21 +49665,51 @@ function sendToPeer(peer, data) {
49427
49665
  return false;
49428
49666
  }
49429
49667
  }
49668
+ function buildCommandResultChunkEnvelope(requestId, chunkId, index, total, data) {
49669
+ return JSON.stringify({
49670
+ type: "command_result_chunk",
49671
+ id: requestId,
49672
+ chunkId,
49673
+ index,
49674
+ total,
49675
+ data
49676
+ });
49677
+ }
49678
+ function splitJsonIntoCommandResultChunks(json2, requestId, chunkId) {
49679
+ const chunks = [];
49680
+ let offset = 0;
49681
+ while (offset < json2.length) {
49682
+ let end = Math.min(json2.length, offset + JSON_CHUNK_PAYLOAD_CHARS);
49683
+ while (end > offset) {
49684
+ const candidate = json2.slice(offset, end);
49685
+ const envelope = buildCommandResultChunkEnvelope(requestId, chunkId, chunks.length, MAX_JSON_CHUNKS, candidate);
49686
+ if (Buffer.byteLength(envelope, "utf8") <= MAX_INLINE_JSON_MESSAGE_BYTES) break;
49687
+ end = offset + Math.max(1, Math.floor((end - offset) * 0.8));
49688
+ }
49689
+ chunks.push(json2.slice(offset, end));
49690
+ if (chunks.length > MAX_JSON_CHUNKS) return [];
49691
+ offset = end;
49692
+ }
49693
+ return chunks;
49694
+ }
49430
49695
  function sendChunkedCommandResult(peer, peerId, requestId, json2) {
49431
49696
  const chunkId = requestId || `command_result_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
49432
- const total = Math.ceil(json2.length / JSON_CHUNK_PAYLOAD_CHARS);
49697
+ const chunks = splitJsonIntoCommandResultChunks(json2, requestId, chunkId);
49698
+ const total = chunks.length;
49699
+ if (total === 0 || total > MAX_JSON_CHUNKS) {
49700
+ log(`command_result send failed: peer=${peerId} id=${requestId || "-"} reason=too_many_chunks chunks=${total || "unknown"}`);
49701
+ return false;
49702
+ }
49433
49703
  logDebug(`command_result chunked: peer=${peerId} id=${requestId || "-"} chars=${json2.length} chunks=${total}`);
49434
49704
  for (let index = 0; index < total; index += 1) {
49435
- const chunk = json2.slice(index * JSON_CHUNK_PAYLOAD_CHARS, (index + 1) * JSON_CHUNK_PAYLOAD_CHARS);
49705
+ const chunk = chunks[index] || "";
49436
49706
  try {
49437
- peer.dataChannel?.sendMessage(JSON.stringify({
49438
- type: "command_result_chunk",
49439
- id: requestId,
49440
- chunkId,
49441
- index,
49442
- total,
49443
- data: chunk
49444
- }));
49707
+ const envelope = buildCommandResultChunkEnvelope(requestId, chunkId, index, total, chunk);
49708
+ if (Buffer.byteLength(envelope, "utf8") > MAX_INLINE_JSON_MESSAGE_BYTES) {
49709
+ log(`command_result send failed: peer=${peerId} id=${requestId || "-"} chunk=${index + 1}/${total} reason=chunk_too_large`);
49710
+ return false;
49711
+ }
49712
+ peer.dataChannel?.sendMessage(envelope);
49445
49713
  } catch (error48) {
49446
49714
  log(`command_result send failed: peer=${peerId} id=${requestId || "-"} chunk=${index + 1}/${total} error=${error48?.message || error48}`);
49447
49715
  return false;
@@ -49449,10 +49717,58 @@ function sendChunkedCommandResult(peer, peerId, requestId, json2) {
49449
49717
  }
49450
49718
  return true;
49451
49719
  }
49720
+ function parseChunkedJsonMessage(peerId, parsed) {
49721
+ const chunkType = typeof parsed?.type === "string" ? parsed.type : "";
49722
+ if (!chunkType.endsWith("_chunk")) return parsed;
49723
+ const chunkId = typeof parsed?.chunkId === "string" ? parsed.chunkId : "";
49724
+ const id = typeof parsed?.id === "string" ? parsed.id : "";
49725
+ const index = Number(parsed?.index);
49726
+ const total = Number(parsed?.total);
49727
+ const data = typeof parsed?.data === "string" ? parsed.data : "";
49728
+ if (!chunkId || !Number.isInteger(index) || !Number.isInteger(total) || index < 0 || total <= 0 || total > MAX_JSON_CHUNKS || index >= total || !data) {
49729
+ log(`chunked JSON ignored: peer=${peerId} type=${chunkType || "unknown"} reason=malformed`);
49730
+ return null;
49731
+ }
49732
+ const now = Date.now();
49733
+ incomingJsonChunks.forEach((entry2, key2) => {
49734
+ if (now - entry2.createdAt > JSON_CHUNK_TTL_MS) incomingJsonChunks.delete(key2);
49735
+ });
49736
+ const key = `${peerId}:${chunkId}`;
49737
+ let entry = incomingJsonChunks.get(key);
49738
+ if (!entry || entry.total !== total || entry.type !== chunkType) {
49739
+ entry = { total, chunks: new Array(total).fill(""), received: 0, createdAt: now, type: chunkType, id };
49740
+ incomingJsonChunks.set(key, entry);
49741
+ }
49742
+ if (!entry.chunks[index]) {
49743
+ entry.chunks[index] = data;
49744
+ entry.received += 1;
49745
+ }
49746
+ if (entry.received < entry.total) return null;
49747
+ incomingJsonChunks.delete(key);
49748
+ try {
49749
+ return JSON.parse(entry.chunks.join(""));
49750
+ } catch (error48) {
49751
+ log(`chunked JSON parse failed: peer=${peerId} type=${chunkType} id=${entry.id || "-"} error=${error48?.message || error48}`);
49752
+ return CHUNK_PARSE_FAILED;
49753
+ }
49754
+ }
49452
49755
  function routeDataChannelMessage(peerId, msg, peers, handlers) {
49453
49756
  const text = typeof msg === "string" ? msg : msg.toString("utf-8");
49454
49757
  try {
49455
- const parsed = JSON.parse(text);
49758
+ let parsed = JSON.parse(text);
49759
+ const chunked = parseChunkedJsonMessage(peerId, parsed);
49760
+ if (chunked === null) return;
49761
+ if (chunked === CHUNK_PARSE_FAILED) {
49762
+ const peer = peers.get(peerId);
49763
+ const id = typeof parsed?.id === "string" ? parsed.id : "";
49764
+ if (parsed?.type === "command_chunk") {
49765
+ sendToPeer(peer, { type: "command_result", id, success: false, error: "Failed to reassemble chunked command" });
49766
+ } else if (id) {
49767
+ sendToPeer(peer, { id, type: "response", success: false, error: "Failed to reassemble chunked request" });
49768
+ }
49769
+ return;
49770
+ }
49771
+ parsed = chunked;
49456
49772
  if (parsed.type !== "command" && parsed.type !== "pty_input" && parsed.type !== "pty_resize" && parsed.type !== "ping" && parsed.type !== "pong") {
49457
49773
  logDebug(`Files message from peer ${peerId}: type=${parsed.type}`);
49458
49774
  }
@@ -49748,7 +50064,7 @@ async function handleFileRequest(peerId, req, peers, handlers) {
49748
50064
  }
49749
50065
  sendToPeer(peer, response);
49750
50066
  }
49751
- var MAX_INLINE_JSON_MESSAGE_CHARS, JSON_CHUNK_PAYLOAD_CHARS;
50067
+ 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;
49752
50068
  var init_data_channel_router = __esm({
49753
50069
  "src/daemon-p2p/data-channel-router.ts"() {
49754
50070
  "use strict";
@@ -49756,18 +50072,23 @@ var init_data_channel_router = __esm({
49756
50072
  init_permission();
49757
50073
  init_log();
49758
50074
  MAX_INLINE_JSON_MESSAGE_CHARS = 6e4;
49759
- JSON_CHUNK_PAYLOAD_CHARS = 32e3;
50075
+ MAX_INLINE_JSON_MESSAGE_BYTES = 6e4;
50076
+ JSON_CHUNK_PAYLOAD_CHARS = 16e3;
50077
+ JSON_CHUNK_TTL_MS = 6e4;
50078
+ MAX_JSON_CHUNKS = 1024;
50079
+ CHUNK_PARSE_FAILED = /* @__PURE__ */ Symbol("chunk_parse_failed");
50080
+ incomingJsonChunks = /* @__PURE__ */ new Map();
49760
50081
  }
49761
50082
  });
49762
50083
 
49763
50084
  // src/daemon-p2p/screenshot-sender.ts
49764
- var CHUNK_SIZE, MAX_INLINE_JSON_MESSAGE_BYTES, JSON_CHUNK_PAYLOAD_CHARS2, ScreenshotSender;
50085
+ var CHUNK_SIZE, MAX_INLINE_JSON_MESSAGE_BYTES2, JSON_CHUNK_PAYLOAD_CHARS2, ScreenshotSender;
49765
50086
  var init_screenshot_sender = __esm({
49766
50087
  "src/daemon-p2p/screenshot-sender.ts"() {
49767
50088
  "use strict";
49768
50089
  init_log();
49769
50090
  CHUNK_SIZE = 6e4;
49770
- MAX_INLINE_JSON_MESSAGE_BYTES = 6e4;
50091
+ MAX_INLINE_JSON_MESSAGE_BYTES2 = 6e4;
49771
50092
  JSON_CHUNK_PAYLOAD_CHARS2 = 16e3;
49772
50093
  ScreenshotSender = class {
49773
50094
  _ssDebugDone = false;
@@ -49811,7 +50132,7 @@ var init_screenshot_sender = __esm({
49811
50132
  type: "topic_update",
49812
50133
  update
49813
50134
  });
49814
- if (Buffer.byteLength(json2, "utf8") > MAX_INLINE_JSON_MESSAGE_BYTES) {
50135
+ if (Buffer.byteLength(json2, "utf8") > MAX_INLINE_JSON_MESSAGE_BYTES2) {
49815
50136
  return this.sendChunkedTopicUpdate(peer, update, json2);
49816
50137
  }
49817
50138
  try {
@@ -49850,6 +50171,10 @@ var init_screenshot_sender = __esm({
49850
50171
  let sentAny = false;
49851
50172
  for (const peer of peers.values()) {
49852
50173
  if (peer.state !== "connected" || !peer.dataChannel?.isOpen()) continue;
50174
+ if (Buffer.byteLength(msg, "utf8") > MAX_INLINE_JSON_MESSAGE_BYTES2) {
50175
+ if (this.sendChunkedSessionOutput(peer, sessionId, msg)) sentAny = true;
50176
+ continue;
50177
+ }
49853
50178
  try {
49854
50179
  peer.dataChannel.sendMessage(msg);
49855
50180
  sentAny = true;
@@ -49858,6 +50183,27 @@ var init_screenshot_sender = __esm({
49858
50183
  }
49859
50184
  return sentAny;
49860
50185
  }
50186
+ sendChunkedSessionOutput(peer, sessionId, json2) {
50187
+ const chunkId = `session_output:${sessionId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
50188
+ const total = Math.ceil(json2.length / JSON_CHUNK_PAYLOAD_CHARS2);
50189
+ logDebug(`session_output chunked: peer=${peer.peerId || "unknown"} sessionId=${sessionId || "-"} chars=${json2.length} chunks=${total}`);
50190
+ for (let index = 0; index < total; index += 1) {
50191
+ const chunk = json2.slice(index * JSON_CHUNK_PAYLOAD_CHARS2, (index + 1) * JSON_CHUNK_PAYLOAD_CHARS2);
50192
+ try {
50193
+ peer.dataChannel?.sendMessage(JSON.stringify({
50194
+ type: "session_output_chunk",
50195
+ sessionId,
50196
+ chunkId,
50197
+ index,
50198
+ total,
50199
+ data: chunk
50200
+ }));
50201
+ } catch {
50202
+ return false;
50203
+ }
50204
+ }
50205
+ return true;
50206
+ }
49861
50207
  sendScreenshot(peers, base64Data, targetSessionId) {
49862
50208
  const buffer = Buffer.from(base64Data, "base64");
49863
50209
  return this.sendScreenshotBuffer(peers, buffer, targetSessionId);
@@ -58691,7 +59037,7 @@ var init_adhdev_daemon = __esm({
58691
59037
  init_version();
58692
59038
  init_src();
58693
59039
  init_runtime_defaults();
58694
- pkgVersion = resolvePackageVersion({ injectedVersion: "0.9.54" });
59040
+ pkgVersion = resolvePackageVersion({ injectedVersion: "0.9.56" });
58695
59041
  AdhdevDaemon = class _AdhdevDaemon {
58696
59042
  localHttpServer = null;
58697
59043
  localWss = null;
@@ -58715,6 +59061,19 @@ var init_adhdev_daemon = __esm({
58715
59061
  sessionHostEndpoint = null;
58716
59062
  sessionHostController = null;
58717
59063
  gitWorkspaceMonitor = createGitWorkspaceMonitor();
59064
+ turnSnapshotTracker = new TurnSnapshotTracker(({ sessionId, workspace }) => {
59065
+ const gitServices = this.components?.commandHandler?.ctx?.gitCommandServices;
59066
+ if (gitServices?.createSnapshot) {
59067
+ void Promise.resolve(gitServices.createSnapshot({
59068
+ workspace,
59069
+ reason: "after_agent_work",
59070
+ sessionId
59071
+ })).catch(() => {
59072
+ });
59073
+ }
59074
+ void this.gitWorkspaceMonitor.refresh({ workspace, includeDiffSummary: false }).catch(() => {
59075
+ });
59076
+ });
58718
59077
  running = false;
58719
59078
  localPort;
58720
59079
  ideType = "unknown";
@@ -58886,7 +59245,8 @@ var init_adhdev_daemon = __esm({
58886
59245
  })),
58887
59246
  instanceId: `daemon_${loadConfig().machineId || "daemon"}`,
58888
59247
  version: pkgVersion,
58889
- profile: "live"
59248
+ profile: "live",
59249
+ getGitSummaryForWorkspace: (workspace) => this.gitWorkspaceMonitor.getCompactSummary(workspace)
58890
59250
  });
58891
59251
  }
58892
59252
  invalidateHotChatSnapshotCache() {
@@ -59072,7 +59432,8 @@ var init_adhdev_daemon = __esm({
59072
59432
  })),
59073
59433
  instanceId: `daemon_${loadConfig().machineId || "daemon"}`,
59074
59434
  version: pkgVersion,
59075
- profile: "metadata"
59435
+ profile: "metadata",
59436
+ getGitSummaryForWorkspace: (workspace) => this.gitWorkspaceMonitor.getCompactSummary(workspace)
59076
59437
  });
59077
59438
  }
59078
59439
  buildDaemonMetadataUpdateForSubscription(subscription) {
@@ -59216,6 +59577,27 @@ ${err?.stack || ""}`);
59216
59577
  onStreamsUpdated: (ideType, streams) => {
59217
59578
  if (!this.components) return;
59218
59579
  forwardAgentStreamsToIdeInstance(this.components.instanceManager, ideType, streams);
59580
+ for (const stream of streams) {
59581
+ if (stream.sessionId) {
59582
+ const workspace = this.components?.instanceManager?.getInstance(stream.sessionId)?.getState()?.workspace;
59583
+ this.turnSnapshotTracker.record(stream.sessionId, stream.status || "idle", workspace);
59584
+ }
59585
+ }
59586
+ },
59587
+ onBeforeSendChat: ({ workspace }) => {
59588
+ void this.gitWorkspaceMonitor.refresh({
59589
+ workspace,
59590
+ includeDiffSummary: false
59591
+ }).catch(() => {
59592
+ });
59593
+ const gitServices = this.components?.commandHandler?.ctx?.gitCommandServices;
59594
+ if (gitServices?.createSnapshot) {
59595
+ void Promise.resolve(gitServices.createSnapshot({
59596
+ workspace,
59597
+ reason: "before_user_input_dispatch"
59598
+ })).catch(() => {
59599
+ });
59600
+ }
59219
59601
  }
59220
59602
  });
59221
59603
  const providerSourceConfig = this.components.providerLoader.getSourceConfig();