@adhdev/daemon-core 0.9.54 → 0.9.55

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;
@@ -136,6 +136,7 @@ export declare class ProviderCliAdapter implements CliAdapter {
136
136
  private readonly sendDelayMs;
137
137
  private readonly sendKey;
138
138
  private readonly submitStrategy;
139
+ private readonly requirePromptEchoBeforeSubmit;
139
140
  private static readonly SCRIPT_STATUS_DEBOUNCE_MS;
140
141
  constructor(provider: CliProviderModule, workingDir: string, extraArgs?: string[], transportFactory?: PtyTransportFactory);
141
142
  /** 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;
@@ -123,6 +123,8 @@ export interface CliProviderModule {
123
123
  sendDelayMs?: number;
124
124
  sendKey?: string;
125
125
  submitStrategy?: 'wait_for_echo' | 'immediate';
126
+ /** Require the typed prompt to be visible on the PTY screen before sending Enter. */
127
+ requirePromptEchoBeforeSubmit?: boolean;
126
128
  /** Allow sending another prompt while the CLI is still generating so users can intervene mid-turn. */
127
129
  allowInputDuringGeneration?: boolean;
128
130
  /** 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);
@@ -2503,6 +2505,7 @@ var init_provider_cli_adapter = __esm({
2503
2505
  sendDelayMs;
2504
2506
  sendKey;
2505
2507
  submitStrategy;
2508
+ requirePromptEchoBeforeSubmit;
2506
2509
  static SCRIPT_STATUS_DEBOUNCE_MS = 3e3;
2507
2510
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
2508
2511
  setCliScripts(scripts) {
@@ -4023,6 +4026,22 @@ var init_provider_cli_adapter = __esm({
4023
4026
  }
4024
4027
  }
4025
4028
  if (elapsed >= state.maxEchoWaitMs) {
4029
+ const diagnostic = {
4030
+ elapsed,
4031
+ maxEchoWaitMs: state.maxEchoWaitMs,
4032
+ submitDelayMs: state.submitDelayMs,
4033
+ promptSnippet: state.normalizedPromptSnippet,
4034
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
4035
+ screenText: summarizeCliTraceText(screenText, 1e3)
4036
+ };
4037
+ this.recordTrace("submit_echo_missing", diagnostic);
4038
+ if (this.requirePromptEchoBeforeSubmit) {
4039
+ const message = `${this.cliName} prompt echo was not observed on the PTY screen before submit`;
4040
+ LOG.warn("CLI", `[${this.cliType}] ${message} elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs} screen=${JSON.stringify(diagnostic.screenText).slice(0, 240)}`);
4041
+ completion.rejectOnce(new Error(message));
4042
+ return;
4043
+ }
4044
+ LOG.warn("CLI", `[${this.cliType}] prompt echo was not observed before submit; sending submit key anyway elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs}`);
4026
4045
  this.submitSendKey(state, completion);
4027
4046
  return;
4028
4047
  }
@@ -4487,6 +4506,7 @@ var init_provider_cli_adapter = __esm({
4487
4506
  sendDelayMs: this.sendDelayMs,
4488
4507
  sendKey: this.sendKey,
4489
4508
  submitStrategy: this.submitStrategy,
4509
+ requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
4490
4510
  submitPendingUntil: this.submitPendingUntil,
4491
4511
  responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
4492
4512
  resizeSuppressUntil: this.resizeSuppressUntil,
@@ -4574,6 +4594,7 @@ __export(index_exports, {
4574
4594
  ProviderLoader: () => ProviderLoader,
4575
4595
  STANDALONE_CDP_SCAN_INTERVAL_MS: () => STANDALONE_CDP_SCAN_INTERVAL_MS,
4576
4596
  SessionHostPtyTransportFactory: () => SessionHostPtyTransportFactory,
4597
+ TurnSnapshotTracker: () => TurnSnapshotTracker,
4577
4598
  VersionArchive: () => VersionArchive,
4578
4599
  appendRecentActivity: () => appendRecentActivity,
4579
4600
  buildAssistantChatMessage: () => buildAssistantChatMessage,
@@ -5585,13 +5606,8 @@ var GIT_COMMAND_NAMES = /* @__PURE__ */ new Set([
5585
5606
  "git_checkpoint",
5586
5607
  "git_stash_push",
5587
5608
  "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"
5609
+ "git_checkout_files",
5610
+ "git_remote_url"
5595
5611
  ]);
5596
5612
  var SNAPSHOT_REASONS = /* @__PURE__ */ new Set([
5597
5613
  "session_baseline",
@@ -5632,7 +5648,12 @@ function createDefaultGitCommandServices() {
5632
5648
  turnId
5633
5649
  }),
5634
5650
  compareSnapshots: ({ beforeSnapshotId, afterSnapshotId }) => defaultSnapshotStore.compare(beforeSnapshotId, afterSnapshotId),
5635
- getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until })
5651
+ getLog: ({ workspace, limit, path: filePath, since, until }) => getGitLog(workspace, { limit, path: filePath, since, until }),
5652
+ checkpoint: async ({ workspace, message, includeUntracked = false }) => gitCheckpoint(workspace, message, includeUntracked),
5653
+ stashPush: async ({ workspace, message, includeUntracked = false }) => gitStashPush(workspace, message, includeUntracked),
5654
+ stashPop: async ({ workspace, stashRef }) => gitStashPop(workspace, stashRef),
5655
+ checkoutFiles: async ({ workspace, paths }) => gitCheckoutFiles(workspace, paths),
5656
+ getRemoteUrl: async ({ workspace, remote = "origin" }) => gitGetRemoteUrl(workspace, remote)
5636
5657
  };
5637
5658
  }
5638
5659
  var defaultGitCommandServices = createDefaultGitCommandServices();
@@ -5696,9 +5717,6 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
5696
5717
  if (!isGitCommandName(command)) {
5697
5718
  return failure("invalid_args", `Unknown Git command: ${command}`);
5698
5719
  }
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
5720
  const workspaceResult = validateWorkspace2(args);
5703
5721
  if ("success" in workspaceResult) return workspaceResult;
5704
5722
  const { workspace } = workspaceResult;
@@ -5758,10 +5776,153 @@ async function handleGitCommand(command, args, services = defaultGitCommandServi
5758
5776
  }));
5759
5777
  return "success" in log ? log : { success: true, log };
5760
5778
  }
5779
+ case "git_checkpoint": {
5780
+ if (!services.checkpoint) return serviceNotImplemented(command);
5781
+ const msg = validateMutatingMessage(args?.message);
5782
+ if (typeof msg !== "string") return msg;
5783
+ const includeUntracked = Boolean(args?.includeUntracked);
5784
+ const checkpoint = await runService(() => services.checkpoint({ workspace, message: msg, includeUntracked }));
5785
+ return "success" in checkpoint ? checkpoint : { success: true, checkpoint };
5786
+ }
5787
+ case "git_stash_push": {
5788
+ if (!services.stashPush) return serviceNotImplemented(command);
5789
+ const msg = validateMutatingMessage(args?.message);
5790
+ if (typeof msg !== "string") return msg;
5791
+ const includeUntracked = Boolean(args?.includeUntracked);
5792
+ const stash = await runService(() => services.stashPush({ workspace, message: msg, includeUntracked }));
5793
+ return "success" in stash ? stash : { success: true, stash };
5794
+ }
5795
+ case "git_stash_pop": {
5796
+ if (!services.stashPop) return serviceNotImplemented(command);
5797
+ const stashRef = optionalString(args?.stashRef);
5798
+ if (stashRef !== void 0 && !/^stash@\{\d+\}$/.test(stashRef)) {
5799
+ return failure("invalid_args", "stashRef must match stash@{N} format");
5800
+ }
5801
+ const popResult = await runService(() => services.stashPop({ workspace, stashRef }));
5802
+ if (popResult !== void 0 && "success" in popResult) return popResult;
5803
+ return { success: true, stashPopped: true };
5804
+ }
5805
+ case "git_checkout_files": {
5806
+ if (!services.checkoutFiles) return serviceNotImplemented(command);
5807
+ const paths = args?.paths;
5808
+ if (!Array.isArray(paths) || paths.length === 0) {
5809
+ return failure("invalid_args", "paths must be a non-empty array");
5810
+ }
5811
+ if (paths.length > 50) {
5812
+ return failure("invalid_args", "paths array exceeds maximum of 50 entries");
5813
+ }
5814
+ const checkoutResult = await runService(() => services.checkoutFiles({ workspace, paths }));
5815
+ return "success" in checkoutResult ? checkoutResult : { success: true, checkedOut: checkoutResult.checkedOut };
5816
+ }
5817
+ case "git_remote_url": {
5818
+ if (!services.getRemoteUrl) return serviceNotImplemented(command);
5819
+ const remote = typeof args?.remote === "string" && args.remote.trim() ? args.remote.trim() : "origin";
5820
+ const remoteResult = await runService(() => services.getRemoteUrl({ workspace, remote }));
5821
+ if ("success" in remoteResult) return remoteResult;
5822
+ return { success: true, remoteUrl: remoteResult.remoteUrl, remote: remoteResult.remote };
5823
+ }
5761
5824
  default:
5762
5825
  return failure("invalid_args", `Unknown Git command: ${command}`);
5763
5826
  }
5764
5827
  }
5828
+ function validateMutatingMessage(value) {
5829
+ if (typeof value !== "string" || !value.trim()) {
5830
+ return failure("invalid_args", "message must be a non-empty string");
5831
+ }
5832
+ const msg = value.trim();
5833
+ if (msg.length > 200) {
5834
+ return failure("invalid_args", "message must be 200 characters or fewer");
5835
+ }
5836
+ return msg;
5837
+ }
5838
+ async function gitCheckpoint(workspace, message, includeUntracked) {
5839
+ const repo = await resolveGitRepository(workspace);
5840
+ const repoRoot = repo.repoRoot;
5841
+ const statusResult = await getGitRepoStatus(workspace);
5842
+ if (statusResult.hasConflicts) {
5843
+ throw new GitCommandError("conflict", "Repository has conflicts \u2014 resolve before checkpointing");
5844
+ }
5845
+ const addArgs = includeUntracked ? ["-A"] : ["-u"];
5846
+ await runGit(repo, ["add", ...addArgs], { cwd: repoRoot });
5847
+ const fullMsg = `adhdev: checkpoint ${message}`;
5848
+ let commitSha;
5849
+ try {
5850
+ await runGit(repo, ["commit", "-m", fullMsg], { cwd: repoRoot });
5851
+ const revResult = await runGit(repo, ["rev-parse", "HEAD"], { cwd: repoRoot });
5852
+ commitSha = revResult.stdout.trim();
5853
+ } catch (err) {
5854
+ const output = (err?.stdout || "") + (err?.stderr || "");
5855
+ if (/nothing to commit/i.test(output)) {
5856
+ throw new GitCommandError("git_command_failed", "Nothing to commit");
5857
+ }
5858
+ throw err;
5859
+ }
5860
+ return {
5861
+ workspace: repo.workspace,
5862
+ repoRoot,
5863
+ isGitRepo: true,
5864
+ commit: commitSha,
5865
+ message: fullMsg,
5866
+ lastCheckedAt: Date.now()
5867
+ };
5868
+ }
5869
+ async function gitStashPush(workspace, message, includeUntracked) {
5870
+ const repo = await resolveGitRepository(workspace);
5871
+ const repoRoot = repo.repoRoot;
5872
+ const stashArgs = ["stash", "push", "-m", message];
5873
+ if (includeUntracked) stashArgs.push("--include-untracked");
5874
+ const result = await runGit(repo, stashArgs, { cwd: repoRoot });
5875
+ if (/No local changes to save/i.test(result.stdout + result.stderr)) {
5876
+ throw new GitCommandError("git_command_failed", "Nothing to stash");
5877
+ }
5878
+ return {
5879
+ workspace: repo.workspace,
5880
+ repoRoot,
5881
+ isGitRepo: true,
5882
+ stashRef: "stash@{0}",
5883
+ message,
5884
+ lastCheckedAt: Date.now()
5885
+ };
5886
+ }
5887
+ async function gitStashPop(workspace, stashRef) {
5888
+ const repo = await resolveGitRepository(workspace);
5889
+ const repoRoot = repo.repoRoot;
5890
+ const popArgs = stashRef ? ["stash", "pop", stashRef] : ["stash", "pop"];
5891
+ await runGit(repo, popArgs, { cwd: repoRoot });
5892
+ }
5893
+ async function gitCheckoutFiles(workspace, paths) {
5894
+ const repo = await resolveGitRepository(workspace);
5895
+ const repoRoot = repo.repoRoot;
5896
+ const normalizedPaths = [];
5897
+ for (const p of paths) {
5898
+ if (typeof p !== "string" || !p.trim() || p.includes("\0")) {
5899
+ throw new GitCommandError("invalid_args", `Invalid path: ${String(p)}`);
5900
+ }
5901
+ if (path3.isAbsolute(p)) {
5902
+ throw new GitCommandError("invalid_args", `Path must be repository-relative, not absolute: ${p}`);
5903
+ }
5904
+ const normalized = path3.normalize(p.trim()).split(path3.sep).join("/");
5905
+ if (normalized.startsWith("../") || normalized === "..") {
5906
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
5907
+ }
5908
+ const absolutePath = path3.resolve(repoRoot, normalized);
5909
+ if (!isPathInside(repoRoot, absolutePath)) {
5910
+ throw new GitCommandError("path_outside_repo", `Path is outside repository root: ${p}`);
5911
+ }
5912
+ normalizedPaths.push(normalized);
5913
+ }
5914
+ await runGit(repo, ["checkout", "--", ...normalizedPaths], { cwd: repoRoot });
5915
+ return { checkedOut: normalizedPaths };
5916
+ }
5917
+ async function gitGetRemoteUrl(workspace, remote) {
5918
+ const repo = await resolveGitRepository(workspace);
5919
+ const result = await runGit(repo, ["remote", "get-url", remote], { cwd: repo.repoRoot });
5920
+ const remoteUrl = result.stdout.trim();
5921
+ if (!remoteUrl) {
5922
+ throw new GitCommandError("git_command_failed", `Remote '${remote}' has no URL`);
5923
+ }
5924
+ return { remoteUrl, remote };
5925
+ }
5765
5926
  function formatOptionalGitLogRangeArg(flag, value) {
5766
5927
  return value ? [`${flag}=${value}`] : [];
5767
5928
  }
@@ -5820,6 +5981,27 @@ function validateGitLogPath(repoRoot, filePath) {
5820
5981
  return normalized;
5821
5982
  }
5822
5983
 
5984
+ // src/git/turn-snapshot-tracker.ts
5985
+ var BUSY_STATUSES = /* @__PURE__ */ new Set(["streaming", "waiting_approval"]);
5986
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["idle", "error"]);
5987
+ var TurnSnapshotTracker = class {
5988
+ lastStatus = /* @__PURE__ */ new Map();
5989
+ onTurnCompleted;
5990
+ constructor(onTurnCompleted) {
5991
+ this.onTurnCompleted = onTurnCompleted;
5992
+ }
5993
+ record(sessionId, status, workspace) {
5994
+ const prev = this.lastStatus.get(sessionId);
5995
+ this.lastStatus.set(sessionId, status);
5996
+ if (workspace && prev && BUSY_STATUSES.has(prev) && TERMINAL_STATUSES.has(status)) {
5997
+ this.onTurnCompleted({ sessionId, workspace });
5998
+ }
5999
+ }
6000
+ forget(sessionId) {
6001
+ this.lastStatus.delete(sessionId);
6002
+ }
6003
+ };
6004
+
5823
6005
  // src/index.ts
5824
6006
  init_config();
5825
6007
 
@@ -14707,6 +14889,16 @@ var DaemonCommandHandler = class {
14707
14889
  return result;
14708
14890
  }
14709
14891
  }
14892
+ if (cmd === "send_chat" && this._ctx.onBeforeSendChat) {
14893
+ const sessionId = this._currentRoute.session?.sessionId;
14894
+ const workspace = sessionId ? this._ctx.instanceManager?.getInstance(sessionId)?.getState?.()?.workspace : void 0;
14895
+ if (workspace && sessionId) {
14896
+ try {
14897
+ this._ctx.onBeforeSendChat({ workspace, sessionId });
14898
+ } catch {
14899
+ }
14900
+ }
14901
+ }
14710
14902
  try {
14711
14903
  result = await this.dispatch(cmd, args);
14712
14904
  this.logCommandEnd(cmd, result, startedAt);
@@ -17942,6 +18134,7 @@ var KNOWN_PROVIDER_FIELDS = /* @__PURE__ */ new Set([
17942
18134
  "sendDelayMs",
17943
18135
  "sendKey",
17944
18136
  "submitStrategy",
18137
+ "requirePromptEchoBeforeSubmit",
17945
18138
  "timeouts",
17946
18139
  "disableUpstream"
17947
18140
  ]);
@@ -29461,6 +29654,7 @@ async function initDaemonComponents(config) {
29461
29654
  providerLoader,
29462
29655
  instanceManager,
29463
29656
  sessionRegistry,
29657
+ gitCommandServices: createDefaultGitCommandServices(),
29464
29658
  onProviderSettingChanged: async (providerType) => {
29465
29659
  await refreshProviderAvailability(providerType);
29466
29660
  config.onStatusChange?.();
@@ -29468,7 +29662,8 @@ async function initDaemonComponents(config) {
29468
29662
  onProviderSourceConfigChanged: async () => {
29469
29663
  await refreshProviderAvailability();
29470
29664
  config.onStatusChange?.();
29471
- }
29665
+ },
29666
+ onBeforeSendChat: config.onBeforeSendChat
29472
29667
  });
29473
29668
  agentStreamManager = new DaemonAgentStreamManager(
29474
29669
  LOG.forComponent("AgentStream").asLogFn(),
@@ -29620,6 +29815,7 @@ async function shutdownDaemonComponents(components) {
29620
29815
  ProviderLoader,
29621
29816
  STANDALONE_CDP_SCAN_INTERVAL_MS,
29622
29817
  SessionHostPtyTransportFactory,
29818
+ TurnSnapshotTracker,
29623
29819
  VersionArchive,
29624
29820
  appendRecentActivity,
29625
29821
  buildAssistantChatMessage,