@adhdev/daemon-core 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.
@@ -52,6 +52,11 @@ export interface DaemonInitConfig {
52
52
  statusInstanceId?: string;
53
53
  statusVersion?: string;
54
54
  statusDaemonMode?: boolean;
55
+ /** Fired before send_chat is dispatched — used for turn snapshot hooks */
56
+ onBeforeSendChat?: (params: {
57
+ workspace: string;
58
+ sessionId: string;
59
+ }) => void;
55
60
  }
56
61
  export interface DaemonComponents {
57
62
  providerLoader: ProviderLoader;
@@ -94,6 +94,12 @@ export declare class ProviderCliAdapter implements CliAdapter {
94
94
  private accumulatedRawBuffer;
95
95
  /** Current visible terminal screen snapshot */
96
96
  private terminalScreen;
97
+ private static readonly MAX_RESPONSE_BUFFER;
98
+ private static readonly MAX_RECENT_OUTPUT_BUFFER;
99
+ private responseBufferDroppedChars;
100
+ private recentOutputDroppedChars;
101
+ private accumulatedBufferDroppedChars;
102
+ private accumulatedRawBufferDroppedChars;
97
103
  /** Max accumulated buffer size. Sized to comfortably hold a single long
98
104
  * Hermes turn (tool calls + reasoning + final bubble) without the
99
105
  * rolling window pushing the turn's ╭─ opening line out of view. */
@@ -111,6 +117,8 @@ export declare class ProviderCliAdapter implements CliAdapter {
111
117
  private readonly providerResolutionMeta;
112
118
  private static readonly FINISH_RETRY_DELAY_MS;
113
119
  private static readonly MAX_FINISH_RETRIES;
120
+ private getBufferState;
121
+ private recordBoundedAppendDrop;
114
122
  private buildCommittedMessagesActivitySignature;
115
123
  private syncMessageViews;
116
124
  getLastCommittedMessageActivityAt(): number;
@@ -136,6 +144,7 @@ export declare class ProviderCliAdapter implements CliAdapter {
136
144
  private readonly sendDelayMs;
137
145
  private readonly sendKey;
138
146
  private readonly submitStrategy;
147
+ private readonly requirePromptEchoBeforeSubmit;
139
148
  private static readonly SCRIPT_STATUS_DEBOUNCE_MS;
140
149
  constructor(provider: CliProviderModule, workingDir: string, extraArgs?: string[], transportFactory?: PtyTransportFactory);
141
150
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
@@ -27,6 +27,7 @@ export interface ResolvedCliAdapterConfig {
27
27
  sendDelayMs: number;
28
28
  sendKey: string;
29
29
  submitStrategy: 'wait_for_echo' | 'immediate';
30
+ requirePromptEchoBeforeSubmit: boolean;
30
31
  providerResolutionMeta: ProviderResolutionMeta;
31
32
  }
32
33
  export declare function resolveCliAdapterConfig(provider: CliProviderModule): ResolvedCliAdapterConfig;
@@ -22,6 +22,28 @@ export interface CliSessionStatus {
22
22
  } | null;
23
23
  errorMessage?: string;
24
24
  errorReason?: string;
25
+ bufferState?: {
26
+ responseBuffer?: {
27
+ truncated: boolean;
28
+ droppedChars: number;
29
+ maxChars: number;
30
+ };
31
+ recentOutputBuffer?: {
32
+ truncated: boolean;
33
+ droppedChars: number;
34
+ maxChars: number;
35
+ };
36
+ accumulatedBuffer?: {
37
+ truncated: boolean;
38
+ droppedChars: number;
39
+ maxChars: number;
40
+ };
41
+ accumulatedRawBuffer?: {
42
+ truncated: boolean;
43
+ droppedChars: number;
44
+ maxChars: number;
45
+ };
46
+ };
25
47
  }
26
48
  export interface ParsedSession {
27
49
  status: string;
@@ -123,6 +145,8 @@ export interface CliProviderModule {
123
145
  sendDelayMs?: number;
124
146
  sendKey?: string;
125
147
  submitStrategy?: 'wait_for_echo' | 'immediate';
148
+ /** Require the typed prompt to be visible on the PTY screen before sending Enter. */
149
+ requirePromptEchoBeforeSubmit?: boolean;
126
150
  /** Allow sending another prompt while the CLI is still generating so users can intervene mid-turn. */
127
151
  allowInputDuringGeneration?: boolean;
128
152
  /** When provider-owned, daemon treats provider parser output as canonical transcript authority. */
@@ -34,6 +34,11 @@ export interface CommandContext {
34
34
  onProviderSettingChanged?: (providerType: string, key: string, value: any) => Promise<void> | void;
35
35
  onProviderSourceConfigChanged?: () => Promise<void> | void;
36
36
  gitCommandServices?: GitCommandServices;
37
+ /** Fired synchronously before send_chat is dispatched; fire-and-forget for callers */
38
+ onBeforeSendChat?: (params: {
39
+ workspace: string;
40
+ sessionId: string;
41
+ }) => void;
37
42
  }
38
43
  /**
39
44
  * Shared helpers interface — passed to sub-module command functions
@@ -22,6 +22,16 @@ export interface GitLogResult extends GitRepoIdentity {
22
22
  truncated: boolean;
23
23
  lastCheckedAt: number;
24
24
  }
25
+ export interface GitCheckpointResult extends GitRepoIdentity {
26
+ commit: string;
27
+ message: string;
28
+ lastCheckedAt: number;
29
+ }
30
+ export interface GitStashPushResult extends GitRepoIdentity {
31
+ stashRef: string;
32
+ message: string;
33
+ lastCheckedAt: number;
34
+ }
25
35
  export interface GitCommandServices {
26
36
  getStatus?: (params: {
27
37
  workspace: string;
@@ -53,6 +63,33 @@ export interface GitCommandServices {
53
63
  since?: string;
54
64
  until?: string;
55
65
  }) => Promise<GitLogResult> | GitLogResult;
66
+ checkpoint?: (params: {
67
+ workspace: string;
68
+ message: string;
69
+ includeUntracked?: boolean;
70
+ }) => Promise<GitCheckpointResult> | GitCheckpointResult;
71
+ stashPush?: (params: {
72
+ workspace: string;
73
+ message: string;
74
+ includeUntracked?: boolean;
75
+ }) => Promise<GitStashPushResult> | GitStashPushResult;
76
+ stashPop?: (params: {
77
+ workspace: string;
78
+ stashRef?: string;
79
+ }) => Promise<void>;
80
+ checkoutFiles?: (params: {
81
+ workspace: string;
82
+ paths: string[];
83
+ }) => Promise<{
84
+ checkedOut: string[];
85
+ }>;
86
+ getRemoteUrl?: (params: {
87
+ workspace: string;
88
+ remote?: string;
89
+ }) => Promise<{
90
+ remoteUrl: string;
91
+ remote: string;
92
+ }>;
56
93
  }
57
94
  type GitCommandFailure = {
58
95
  success: false;
@@ -77,6 +114,22 @@ type GitCommandSuccess = {
77
114
  } | {
78
115
  success: true;
79
116
  log: GitLogResult;
117
+ } | {
118
+ success: true;
119
+ checkpoint: GitCheckpointResult;
120
+ } | {
121
+ success: true;
122
+ stash: GitStashPushResult;
123
+ } | {
124
+ success: true;
125
+ stashPopped: true;
126
+ } | {
127
+ success: true;
128
+ checkedOut: string[];
129
+ } | {
130
+ success: true;
131
+ remoteUrl: string;
132
+ remote: string;
80
133
  };
81
134
  export type GitCommandResult = GitCommandSuccess | GitCommandFailure;
82
135
  export declare function createDefaultGitCommandServices(): GitCommandServices;
@@ -110,4 +110,4 @@ export interface GitWorkspaceUpdate {
110
110
  seq: number;
111
111
  timestamp: number;
112
112
  }
113
- export type GitCommandName = 'git_status' | 'git_diff_summary' | 'git_diff_file' | 'git_snapshot_create' | 'git_snapshot_compare' | 'git_log' | 'git_checkpoint' | 'git_stash_push' | 'git_stash_pop' | 'git_checkout_files';
113
+ export type GitCommandName = 'git_status' | 'git_diff_summary' | 'git_diff_file' | 'git_snapshot_create' | 'git_snapshot_compare' | 'git_log' | 'git_checkpoint' | 'git_stash_push' | 'git_stash_pop' | 'git_checkout_files' | 'git_remote_url';
@@ -12,3 +12,5 @@ export { createGitWorkspaceMonitor, DEFAULT_GIT_WORKSPACE_POLL_INTERVAL_MS, GitW
12
12
  export type { GitWorkspaceCacheEntry, GitWorkspaceMonitorOptions, GitWorkspaceSubscription, GitWorkspaceUpdateListener, NormalizedWorkspaceGitSubscriptionParams, NormalizeGitWorkspaceSubscriptionOptions, } from './git-monitor.js';
13
13
  export { createDefaultGitCommandServices, handleGitCommand, isGitCommandName } from './git-commands.js';
14
14
  export type { GitCommandResult, GitCommandServices, GitFileDiff, GitLogEntry, GitLogResult, } from './git-commands.js';
15
+ export { TurnSnapshotTracker } from './turn-snapshot-tracker.js';
16
+ export type { TurnCompletedCallback } from './turn-snapshot-tracker.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Tracks agent session status transitions and fires snapshot callbacks on turn completion.
3
+ * "Busy" = streaming | waiting_approval
4
+ * "Completed" = idle | error (transition from busy)
5
+ */
6
+ export type TurnCompletedCallback = (params: {
7
+ sessionId: string;
8
+ workspace: string;
9
+ }) => void;
10
+ export declare class TurnSnapshotTracker {
11
+ private lastStatus;
12
+ private onTurnCompleted;
13
+ constructor(onTurnCompleted: TurnCompletedCallback);
14
+ record(sessionId: string, status: string, workspace: string | null | undefined): void;
15
+ forget(sessionId: string): void;
16
+ }
package/dist/index.js CHANGED
@@ -1942,6 +1942,7 @@ function resolveCliAdapterConfig(provider) {
1942
1942
  sendDelayMs: typeof provider.sendDelayMs === "number" ? Math.max(0, provider.sendDelayMs) : 0,
1943
1943
  sendKey: typeof provider.sendKey === "string" && provider.sendKey.length > 0 ? provider.sendKey : "\r",
1944
1944
  submitStrategy: provider.submitStrategy === "immediate" ? "immediate" : "wait_for_echo",
1945
+ requirePromptEchoBeforeSubmit: provider.requirePromptEchoBeforeSubmit === true,
1945
1946
  providerResolutionMeta: {
1946
1947
  type: provider.type,
1947
1948
  name: provider.name,
@@ -2207,6 +2208,7 @@ var init_provider_cli_adapter = __esm({
2207
2208
  this.sendDelayMs = resolvedConfig.sendDelayMs;
2208
2209
  this.sendKey = resolvedConfig.sendKey;
2209
2210
  this.submitStrategy = resolvedConfig.submitStrategy;
2211
+ this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
2210
2212
  this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
2211
2213
  this.cliScripts = provider.scripts || {};
2212
2214
  const scriptNames = listCliScriptNames(this.cliScripts);
@@ -2303,6 +2305,12 @@ var init_provider_cli_adapter = __esm({
2303
2305
  accumulatedRawBuffer = "";
2304
2306
  /** Current visible terminal screen snapshot */
2305
2307
  terminalScreen = new TerminalScreen(24, 80);
2308
+ static MAX_RESPONSE_BUFFER = 8e3;
2309
+ static MAX_RECENT_OUTPUT_BUFFER = 1e3;
2310
+ responseBufferDroppedChars = 0;
2311
+ recentOutputDroppedChars = 0;
2312
+ accumulatedBufferDroppedChars = 0;
2313
+ accumulatedRawBufferDroppedChars = 0;
2306
2314
  /** Max accumulated buffer size. Sized to comfortably hold a single long
2307
2315
  * Hermes turn (tool calls + reasoning + final bubble) without the
2308
2316
  * rolling window pushing the turn's ╭─ opening line out of view. */
@@ -2320,6 +2328,23 @@ var init_provider_cli_adapter = __esm({
2320
2328
  providerResolutionMeta;
2321
2329
  static FINISH_RETRY_DELAY_MS = 300;
2322
2330
  static MAX_FINISH_RETRIES = 2;
2331
+ getBufferState() {
2332
+ const build = (droppedChars, maxChars) => droppedChars > 0 ? { truncated: true, droppedChars, maxChars } : void 0;
2333
+ const responseBuffer = build(this.responseBufferDroppedChars, _ProviderCliAdapter.MAX_RESPONSE_BUFFER);
2334
+ const recentOutputBuffer = build(this.recentOutputDroppedChars, _ProviderCliAdapter.MAX_RECENT_OUTPUT_BUFFER);
2335
+ const accumulatedBuffer = build(this.accumulatedBufferDroppedChars, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
2336
+ const accumulatedRawBuffer = build(this.accumulatedRawBufferDroppedChars, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
2337
+ if (!responseBuffer && !recentOutputBuffer && !accumulatedBuffer && !accumulatedRawBuffer) return void 0;
2338
+ return {
2339
+ ...responseBuffer ? { responseBuffer } : {},
2340
+ ...recentOutputBuffer ? { recentOutputBuffer } : {},
2341
+ ...accumulatedBuffer ? { accumulatedBuffer } : {},
2342
+ ...accumulatedRawBuffer ? { accumulatedRawBuffer } : {}
2343
+ };
2344
+ }
2345
+ recordBoundedAppendDrop(previousLength, appendedLength, nextLength) {
2346
+ return Math.max(0, previousLength + appendedLength - nextLength);
2347
+ }
2323
2348
  buildCommittedMessagesActivitySignature() {
2324
2349
  const last = this.committedMessages[this.committedMessages.length - 1];
2325
2350
  return [
@@ -2503,6 +2528,7 @@ var init_provider_cli_adapter = __esm({
2503
2528
  sendDelayMs;
2504
2529
  sendKey;
2505
2530
  submitStrategy;
2531
+ requirePromptEchoBeforeSubmit;
2506
2532
  static SCRIPT_STATUS_DEBOUNCE_MS = 3e3;
2507
2533
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
2508
2534
  setCliScripts(scripts) {
@@ -2684,7 +2710,9 @@ var init_provider_cli_adapter = __esm({
2684
2710
  this.scheduleStartupSettleCheck();
2685
2711
  }
2686
2712
  if (this.isWaitingForResponse && cleanData) {
2687
- this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, 8e3);
2713
+ const previousResponseLen = this.responseBuffer.length;
2714
+ this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, _ProviderCliAdapter.MAX_RESPONSE_BUFFER);
2715
+ this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
2688
2716
  }
2689
2717
  if (cleanData.trim()) {
2690
2718
  if (this.serverConn) {
@@ -2693,14 +2721,19 @@ var init_provider_cli_adapter = __esm({
2693
2721
  this.logBuffer.push({ message: cleanData.trim(), level: "info" });
2694
2722
  }
2695
2723
  }
2724
+ const prevRecentLen = this.recentOutputBuffer.length;
2696
2725
  const prevAccumulatedLen = this.accumulatedBuffer.length;
2697
2726
  const prevAccumulatedRawLen = this.accumulatedRawBuffer.length;
2698
- this.recentOutputBuffer = appendBoundedText(this.recentOutputBuffer, cleanData, 1e3);
2727
+ this.recentOutputBuffer = appendBoundedText(this.recentOutputBuffer, cleanData, _ProviderCliAdapter.MAX_RECENT_OUTPUT_BUFFER);
2699
2728
  this.accumulatedBuffer = appendBoundedText(this.accumulatedBuffer, cleanData, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
2700
2729
  this.accumulatedRawBuffer = appendBoundedText(this.accumulatedRawBuffer, rawData, _ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
2730
+ const droppedRecent = this.recordBoundedAppendDrop(prevRecentLen, cleanData.length, this.recentOutputBuffer.length);
2731
+ const droppedClean = this.recordBoundedAppendDrop(prevAccumulatedLen, cleanData.length, this.accumulatedBuffer.length);
2732
+ const droppedRaw = this.recordBoundedAppendDrop(prevAccumulatedRawLen, rawData.length, this.accumulatedRawBuffer.length);
2733
+ this.recentOutputDroppedChars += droppedRecent;
2734
+ this.accumulatedBufferDroppedChars += droppedClean;
2735
+ this.accumulatedRawBufferDroppedChars += droppedRaw;
2701
2736
  if (this.currentTurnScope) {
2702
- const droppedClean = prevAccumulatedLen + cleanData.length - this.accumulatedBuffer.length;
2703
- const droppedRaw = prevAccumulatedRawLen + rawData.length - this.accumulatedRawBuffer.length;
2704
2737
  if (droppedClean > 0) {
2705
2738
  this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
2706
2739
  }
@@ -3530,13 +3563,15 @@ var init_provider_cli_adapter = __esm({
3530
3563
  effectiveModal = parsedModal;
3531
3564
  }
3532
3565
  }
3566
+ const bufferState = this.getBufferState();
3533
3567
  return {
3534
3568
  status: effectiveStatus,
3535
3569
  messages: [...this.committedMessages],
3536
3570
  workingDir: this.workingDir,
3537
3571
  activeModal: effectiveModal,
3538
3572
  errorMessage: this.parseErrorMessage || void 0,
3539
- errorReason: this.parseErrorMessage ? "parse_error" : void 0
3573
+ errorReason: this.parseErrorMessage ? "parse_error" : void 0,
3574
+ ...bufferState ? { bufferState } : {}
3540
3575
  };
3541
3576
  }
3542
3577
  seedCommittedMessages(messages) {
@@ -3718,10 +3753,12 @@ var init_provider_cli_adapter = __esm({
3718
3753
  messages: hydratedMessages,
3719
3754
  activeModal: parsed.activeModal ?? this.activeModal,
3720
3755
  providerSessionId: typeof parsed.providerSessionId === "string" ? parsed.providerSessionId : void 0,
3756
+ ...this.getBufferState() ? { bufferState: this.getBufferState() } : {},
3721
3757
  ...this.providerOwnsTranscript() ? { transcriptAuthority: "provider", coverage: this.shouldUseFullProviderTranscriptContext() ? "full" : "tail" } : {}
3722
3758
  };
3723
3759
  } else {
3724
3760
  const messages = [...this.committedMessages];
3761
+ const bufferState = this.getBufferState();
3725
3762
  result = {
3726
3763
  id: "cli_session",
3727
3764
  status: this.currentStatus,
@@ -3732,7 +3769,8 @@ var init_provider_cli_adapter = __esm({
3732
3769
  index: typeof message.index === "number" ? message.index : index,
3733
3770
  receivedAt: typeof message.receivedAt === "number" ? message.receivedAt : message.timestamp
3734
3771
  })),
3735
- activeModal: this.activeModal
3772
+ activeModal: this.activeModal,
3773
+ ...bufferState ? { bufferState } : {}
3736
3774
  };
3737
3775
  }
3738
3776
  const hasVisibleAssistantMessage = Array.isArray(result?.messages) && result.messages.some((message) => message?.role === "assistant" && typeof message?.content === "string" && message.content.trim());
@@ -4023,6 +4061,22 @@ var init_provider_cli_adapter = __esm({
4023
4061
  }
4024
4062
  }
4025
4063
  if (elapsed >= state.maxEchoWaitMs) {
4064
+ const diagnostic = {
4065
+ elapsed,
4066
+ maxEchoWaitMs: state.maxEchoWaitMs,
4067
+ submitDelayMs: state.submitDelayMs,
4068
+ promptSnippet: state.normalizedPromptSnippet,
4069
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
4070
+ screenText: summarizeCliTraceText(screenText, 1e3)
4071
+ };
4072
+ this.recordTrace("submit_echo_missing", diagnostic);
4073
+ if (this.requirePromptEchoBeforeSubmit) {
4074
+ const message = `${this.cliName} prompt echo was not observed on the PTY screen before submit`;
4075
+ LOG.warn("CLI", `[${this.cliType}] ${message} elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs} screen=${JSON.stringify(diagnostic.screenText).slice(0, 240)}`);
4076
+ completion.rejectOnce(new Error(message));
4077
+ return;
4078
+ }
4079
+ LOG.warn("CLI", `[${this.cliType}] prompt echo was not observed before submit; sending submit key anyway elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs}`);
4026
4080
  this.submitSendKey(state, completion);
4027
4081
  return;
4028
4082
  }
@@ -4487,6 +4541,7 @@ var init_provider_cli_adapter = __esm({
4487
4541
  sendDelayMs: this.sendDelayMs,
4488
4542
  sendKey: this.sendKey,
4489
4543
  submitStrategy: this.submitStrategy,
4544
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
4490
4545
  submitPendingUntil: this.submitPendingUntil,
4491
4546
  responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
4492
4547
  resizeSuppressUntil: this.resizeSuppressUntil,
@@ -4574,6 +4629,7 @@ __export(index_exports, {
4574
4629
  ProviderLoader: () => ProviderLoader,
4575
4630
  STANDALONE_CDP_SCAN_INTERVAL_MS: () => STANDALONE_CDP_SCAN_INTERVAL_MS,
4576
4631
  SessionHostPtyTransportFactory: () => SessionHostPtyTransportFactory,
4632
+ TurnSnapshotTracker: () => TurnSnapshotTracker,
4577
4633
  VersionArchive: () => VersionArchive,
4578
4634
  appendRecentActivity: () => appendRecentActivity,
4579
4635
  buildAssistantChatMessage: () => buildAssistantChatMessage,
@@ -5585,13 +5641,8 @@ var GIT_COMMAND_NAMES = /* @__PURE__ */ new Set([
5585
5641
  "git_checkpoint",
5586
5642
  "git_stash_push",
5587
5643
  "git_stash_pop",
5588
- "git_checkout_files"
5589
- ]);
5590
- var MUTATING_COMMAND_NAMES = /* @__PURE__ */ new Set([
5591
- "git_checkpoint",
5592
- "git_stash_push",
5593
- "git_stash_pop",
5594
- "git_checkout_files"
5644
+ "git_checkout_files",
5645
+ "git_remote_url"
5595
5646
  ]);
5596
5647
  var SNAPSHOT_REASONS = /* @__PURE__ */ new Set([
5597
5648
  "session_baseline",
@@ -5632,7 +5683,12 @@ function createDefaultGitCommandServices() {
5632
5683
  turnId
5633
5684
  }),
5634
5685
  compareSnapshots: ({ beforeSnapshotId, afterSnapshotId }) => defaultSnapshotStore.compare(beforeSnapshotId, afterSnapshotId),
5635
- getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until })
5686
+ getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until }),
5687
+ checkpoint: async ({ workspace, message, includeUntracked = false }) => gitCheckpoint(workspace, message, includeUntracked),
5688
+ stashPush: async ({ workspace, message, includeUntracked = false }) => gitStashPush(workspace, message, includeUntracked),
5689
+ stashPop: async ({ workspace, stashRef }) => gitStashPop(workspace, stashRef),
5690
+ checkoutFiles: async ({ workspace, paths }) => gitCheckoutFiles(workspace, paths),
5691
+ getRemoteUrl: async ({ workspace, remote = "origin" }) => gitGetRemoteUrl(workspace, remote)
5636
5692
  };
5637
5693
  }
5638
5694
  var defaultGitCommandServices = createDefaultGitCommandServices();
@@ -5696,9 +5752,6 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
5696
5752
  if (!isGitCommandName(command)) {
5697
5753
  return failure("invalid_args", `Unknown Git command: ${command}`);
5698
5754
  }
5699
- if (MUTATING_COMMAND_NAMES.has(command)) {
5700
- return failure("invalid_args", `${command} is not implemented in daemon-core read-only Git routing`);
5701
- }
5702
5755
  const workspaceResult = validateWorkspace2(args);
5703
5756
  if ("success" in workspaceResult) return workspaceResult;
5704
5757
  const { workspace } = workspaceResult;
@@ -5758,10 +5811,153 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
5758
5811
  }));
5759
5812
  return "success" in log ? log : { success: true, log };
5760
5813
  }
5814
+ case "git_checkpoint": {
5815
+ if (!services.checkpoint) return serviceNotImplemented(command);
5816
+ const msg = validateMutatingMessage(args?.message);
5817
+ if (typeof msg !== "string") return msg;
5818
+ const includeUntracked = Boolean(args?.includeUntracked);
5819
+ const checkpoint = await runService(() => services.checkpoint({ workspace, message: msg, includeUntracked }));
5820
+ return "success" in checkpoint ? checkpoint : { success: true, checkpoint };
5821
+ }
5822
+ case "git_stash_push": {
5823
+ if (!services.stashPush) return serviceNotImplemented(command);
5824
+ const msg = validateMutatingMessage(args?.message);
5825
+ if (typeof msg !== "string") return msg;
5826
+ const includeUntracked = Boolean(args?.includeUntracked);
5827
+ const stash = await runService(() => services.stashPush({ workspace, message: msg, includeUntracked }));
5828
+ return "success" in stash ? stash : { success: true, stash };
5829
+ }
5830
+ case "git_stash_pop": {
5831
+ if (!services.stashPop) return serviceNotImplemented(command);
5832
+ const stashRef = optionalString(args?.stashRef);
5833
+ if (stashRef !== void 0 && !/^stash@\{\d+\}$/.test(stashRef)) {
5834
+ return failure("invalid_args", "stashRef must match stash@{N} format");
5835
+ }
5836
+ const popResult = await runService(() => services.stashPop({ workspace, stashRef }));
5837
+ if (popResult !== void 0 && "success" in popResult) return popResult;
5838
+ return { success: true, stashPopped: true };
5839
+ }
5840
+ case "git_checkout_files": {
5841
+ if (!services.checkoutFiles) return serviceNotImplemented(command);
5842
+ const paths = args?.paths;
5843
+ if (!Array.isArray(paths) || paths.length === 0) {
5844
+ return failure("invalid_args", "paths must be a non-empty array");
5845
+ }
5846
+ if (paths.length > 50) {
5847
+ return failure("invalid_args", "paths array exceeds maximum of 50 entries");
5848
+ }
5849
+ const checkoutResult = await runService(() => services.checkoutFiles({ workspace, paths }));
5850
+ return "success" in checkoutResult ? checkoutResult : { success: true, checkedOut: checkoutResult.checkedOut };
5851
+ }
5852
+ case "git_remote_url": {
5853
+ if (!services.getRemoteUrl) return serviceNotImplemented(command);
5854
+ const remote = typeof args?.remote === "string" && args.remote.trim() ? args.remote.trim() : "origin";
5855
+ const remoteResult = await runService(() => services.getRemoteUrl({ workspace, remote }));
5856
+ if ("success" in remoteResult) return remoteResult;
5857
+ return { success: true, remoteUrl: remoteResult.remoteUrl, remote: remoteResult.remote };
5858
+ }
5761
5859
  default:
5762
5860
  return failure("invalid_args", `Unknown Git command: ${command}`);
5763
5861
  }
5764
5862
  }
5863
+ function validateMutatingMessage(value) {
5864
+ if (typeof value !== "string" || !value.trim()) {
5865
+ return failure("invalid_args", "message must be a non-empty string");
5866
+ }
5867
+ const msg = value.trim();
5868
+ if (msg.length > 200) {
5869
+ return failure("invalid_args", "message must be 200 characters or fewer");
5870
+ }
5871
+ return msg;
5872
+ }
5873
+ async function gitCheckpoint(workspace, message, includeUntracked) {
5874
+ const repo = await resolveGitRepository(workspace);
5875
+ const repoRoot = repo.repoRoot;
5876
+ const statusResult = await getGitRepoStatus(workspace);
5877
+ if (statusResult.hasConflicts) {
5878
+ throw new GitCommandError("conflict", "Repository has conflicts \u2014 resolve before checkpointing");
5879
+ }
5880
+ const addArgs = includeUntracked ? ["-A"] : ["-u"];
5881
+ await runGit(repo, ["add", ...addArgs], { cwd: repoRoot });
5882
+ const fullMsg = `adhdev: checkpoint ${message}`;
5883
+ let commitSha;
5884
+ try {
5885
+ await runGit(repo, ["commit", "-m", fullMsg], { cwd: repoRoot });
5886
+ const revResult = await runGit(repo, ["rev-parse", "HEAD"], { cwd: repoRoot });
5887
+ commitSha = revResult.stdout.trim();
5888
+ } catch (err) {
5889
+ const output = (err?.stdout || "") + (err?.stderr || "");
5890
+ if (/nothing to commit/i.test(output)) {
5891
+ throw new GitCommandError("git_command_failed", "Nothing to commit");
5892
+ }
5893
+ throw err;
5894
+ }
5895
+ return {
5896
+ workspace: repo.workspace,
5897
+ repoRoot,
5898
+ isGitRepo: true,
5899
+ commit: commitSha,
5900
+ message: fullMsg,
5901
+ lastCheckedAt: Date.now()
5902
+ };
5903
+ }
5904
+ async function gitStashPush(workspace, message, includeUntracked) {
5905
+ const repo = await resolveGitRepository(workspace);
5906
+ const repoRoot = repo.repoRoot;
5907
+ const stashArgs = ["stash", "push", "-m", message];
5908
+ if (includeUntracked) stashArgs.push("--include-untracked");
5909
+ const result = await runGit(repo, stashArgs, { cwd: repoRoot });
5910
+ if (/No local changes to save/i.test(result.stdout + result.stderr)) {
5911
+ throw new GitCommandError("git_command_failed", "Nothing to stash");
5912
+ }
5913
+ return {
5914
+ workspace: repo.workspace,
5915
+ repoRoot,
5916
+ isGitRepo: true,
5917
+ stashRef: "stash@{0}",
5918
+ message,
5919
+ lastCheckedAt: Date.now()
5920
+ };
5921
+ }
5922
+ async function gitStashPop(workspace, stashRef) {
5923
+ const repo = await resolveGitRepository(workspace);
5924
+ const repoRoot = repo.repoRoot;
5925
+ const popArgs = stashRef ? ["stash", "pop", stashRef] : ["stash", "pop"];
5926
+ await runGit(repo, popArgs, { cwd: repoRoot });
5927
+ }
5928
+ async function gitCheckoutFiles(workspace, paths) {
5929
+ const repo = await resolveGitRepository(workspace);
5930
+ const repoRoot = repo.repoRoot;
5931
+ const normalizedPaths = [];
5932
+ for (const p of paths) {
5933
+ if (typeof p !== "string" || !p.trim() || p.includes("\0")) {
5934
+ throw new GitCommandError("invalid_args", `Invalid path: ${String(p)}`);
5935
+ }
5936
+ if (path3.isAbsolute(p)) {
5937
+ throw new GitCommandError("invalid_args", `Path must be repository-relative, not absolute: ${p}`);
5938
+ }
5939
+ const normalized = path3.normalize(p.trim()).split(path3.sep).join("/");
5940
+ if (normalized.startsWith("../") || normalized === "..") {
5941
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
5942
+ }
5943
+ const absolutePath = path3.resolve(repoRoot, normalized);
5944
+ if (!isPathInside(repoRoot, absolutePath)) {
5945
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
5946
+ }
5947
+ normalizedPaths.push(normalized);
5948
+ }
5949
+ await runGit(repo, ["checkout", "--", ...normalizedPaths], { cwd: repoRoot });
5950
+ return { checkedOut: normalizedPaths };
5951
+ }
5952
+ async function gitGetRemoteUrl(workspace, remote) {
5953
+ const repo = await resolveGitRepository(workspace);
5954
+ const result = await runGit(repo, ["remote", "get-url", remote], { cwd: repo.repoRoot });
5955
+ const remoteUrl = result.stdout.trim();
5956
+ if (!remoteUrl) {
5957
+ throw new GitCommandError("git_command_failed", `Remote '${remote}' has no URL`);
5958
+ }
5959
+ return { remoteUrl, remote };
5960
+ }
5765
5961
  function formatOptionalGitLogRangeArg(flag, value) {
5766
5962
  return value ? [`${flag}=${value}`] : [];
5767
5963
  }
@@ -5820,6 +6016,27 @@ function validateGitLogPath(repoRoot, filePath) {
5820
6016
  return normalized;
5821
6017
  }
5822
6018
 
6019
+ // src/git/turn-snapshot-tracker.ts
6020
+ var BUSY_STATUSES = /* @__PURE__ */ new Set(["streaming", "waiting_approval"]);
6021
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["idle", "error"]);
6022
+ var TurnSnapshotTracker = class {
6023
+ lastStatus = /* @__PURE__ */ new Map();
6024
+ onTurnCompleted;
6025
+ constructor(onTurnCompleted) {
6026
+ this.onTurnCompleted = onTurnCompleted;
6027
+ }
6028
+ record(sessionId, status, workspace) {
6029
+ const prev = this.lastStatus.get(sessionId);
6030
+ this.lastStatus.set(sessionId, status);
6031
+ if (workspace && prev && BUSY_STATUSES.has(prev) && TERMINAL_STATUSES.has(status)) {
6032
+ this.onTurnCompleted({ sessionId, workspace });
6033
+ }
6034
+ }
6035
+ forget(sessionId) {
6036
+ this.lastStatus.delete(sessionId);
6037
+ }
6038
+ };
6039
+
5823
6040
  // src/index.ts
5824
6041
  init_config();
5825
6042
 
@@ -14707,6 +14924,16 @@ var DaemonCommandHandler = class {
14707
14924
  return result;
14708
14925
  }
14709
14926
  }
14927
+ if (cmd === "send_chat" && this._ctx.onBeforeSendChat) {
14928
+ const sessionId = this._currentRoute.session?.sessionId;
14929
+ const workspace = sessionId ? this._ctx.instanceManager?.getInstance(sessionId)?.getState?.()?.workspace : void 0;
14930
+ if (workspace && sessionId) {
14931
+ try {
14932
+ this._ctx.onBeforeSendChat({ workspace, sessionId });
14933
+ } catch {
14934
+ }
14935
+ }
14936
+ }
14710
14937
  try {
14711
14938
  result = await this.dispatch(cmd, args);
14712
14939
  this.logCommandEnd(cmd, result, startedAt);
@@ -17942,6 +18169,7 @@ var KNOWN_PROVIDER_FIELDS = /* @__PURE__ */ new Set([
17942
18169
  "sendDelayMs",
17943
18170
  "sendKey",
17944
18171
  "submitStrategy",
18172
+ "requirePromptEchoBeforeSubmit",
17945
18173
  "timeouts",
17946
18174
  "disableUpstream"
17947
18175
  ]);
@@ -29461,6 +29689,7 @@ async function initDaemonComponents(config) {
29461
29689
  providerLoader,
29462
29690
  instanceManager,
29463
29691
  sessionRegistry,
29692
+ gitCommandServices: createDefaultGitCommandServices(),
29464
29693
  onProviderSettingChanged: async (providerType) => {
29465
29694
  await refreshProviderAvailability(providerType);
29466
29695
  config.onStatusChange?.();
@@ -29468,7 +29697,8 @@ async function initDaemonComponents(config) {
29468
29697
  onProviderSourceConfigChanged: async () => {
29469
29698
  await refreshProviderAvailability();
29470
29699
  config.onStatusChange?.();
29471
- }
29700
+ },
29701
+ onBeforeSendChat: config.onBeforeSendChat
29472
29702
  });
29473
29703
  agentStreamManager = new DaemonAgentStreamManager(
29474
29704
  LOG.forComponent("AgentStream").asLogFn(),
@@ -29620,6 +29850,7 @@ async function shutdownDaemonComponents(components) {
29620
29850
  ProviderLoader,
29621
29851
  STANDALONE_CDP_SCAN_INTERVAL_MS,
29622
29852
  SessionHostPtyTransportFactory,
29853
+ TurnSnapshotTracker,
29623
29854
  VersionArchive,
29624
29855
  appendRecentActivity,
29625
29856
  buildAssistantChatMessage,