@hydra-acp/cli 0.1.24 → 0.1.25

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.d.ts CHANGED
@@ -102,6 +102,7 @@ declare const HydraConfig: z.ZodObject<{
102
102
  logMaxBytes: z.ZodDefault<z.ZodNumber>;
103
103
  cwdColumnMaxWidth: z.ZodDefault<z.ZodNumber>;
104
104
  progressIndicator: z.ZodDefault<z.ZodBoolean>;
105
+ defaultEnterAction: z.ZodDefault<z.ZodEnum<["enqueue", "amend"]>>;
105
106
  }, "strip", z.ZodTypeAny, {
106
107
  repaintThrottleMs: number;
107
108
  maxScrollbackLines: number;
@@ -109,6 +110,7 @@ declare const HydraConfig: z.ZodObject<{
109
110
  logMaxBytes: number;
110
111
  cwdColumnMaxWidth: number;
111
112
  progressIndicator: boolean;
113
+ defaultEnterAction: "enqueue" | "amend";
112
114
  }, {
113
115
  repaintThrottleMs?: number | undefined;
114
116
  maxScrollbackLines?: number | undefined;
@@ -116,6 +118,7 @@ declare const HydraConfig: z.ZodObject<{
116
118
  logMaxBytes?: number | undefined;
117
119
  cwdColumnMaxWidth?: number | undefined;
118
120
  progressIndicator?: boolean | undefined;
121
+ defaultEnterAction?: "enqueue" | "amend" | undefined;
119
122
  }>>;
120
123
  }, "strip", z.ZodTypeAny, {
121
124
  daemon: {
@@ -143,6 +146,7 @@ declare const HydraConfig: z.ZodObject<{
143
146
  logMaxBytes: number;
144
147
  cwdColumnMaxWidth: number;
145
148
  progressIndicator: boolean;
149
+ defaultEnterAction: "enqueue" | "amend";
146
150
  };
147
151
  registry: {
148
152
  url: string;
@@ -179,6 +183,7 @@ declare const HydraConfig: z.ZodObject<{
179
183
  logMaxBytes?: number | undefined;
180
184
  cwdColumnMaxWidth?: number | undefined;
181
185
  progressIndicator?: boolean | undefined;
186
+ defaultEnterAction?: "enqueue" | "amend" | undefined;
182
187
  } | undefined;
183
188
  registry?: {
184
189
  url?: string | undefined;
@@ -1534,6 +1539,40 @@ declare const UpdatePromptResult: z.ZodObject<{
1534
1539
  updated: boolean;
1535
1540
  }>;
1536
1541
  type UpdatePromptResult = z.infer<typeof UpdatePromptResult>;
1542
+ declare const AmendPromptParams: z.ZodObject<{
1543
+ sessionId: z.ZodString;
1544
+ targetMessageId: z.ZodString;
1545
+ prompt: z.ZodArray<z.ZodUnknown, "many">;
1546
+ replaceQueue: z.ZodOptional<z.ZodBoolean>;
1547
+ onTargetCompleted: z.ZodOptional<z.ZodEnum<["reject", "send_anyway"]>>;
1548
+ }, "strip", z.ZodTypeAny, {
1549
+ sessionId: string;
1550
+ prompt: unknown[];
1551
+ targetMessageId: string;
1552
+ replaceQueue?: boolean | undefined;
1553
+ onTargetCompleted?: "reject" | "send_anyway" | undefined;
1554
+ }, {
1555
+ sessionId: string;
1556
+ prompt: unknown[];
1557
+ targetMessageId: string;
1558
+ replaceQueue?: boolean | undefined;
1559
+ onTargetCompleted?: "reject" | "send_anyway" | undefined;
1560
+ }>;
1561
+ type AmendPromptParams = z.infer<typeof AmendPromptParams>;
1562
+ declare const AmendPromptResult: z.ZodObject<{
1563
+ amended: z.ZodBoolean;
1564
+ reason: z.ZodEnum<["ok", "target_completed", "target_cancelled", "target_not_found"]>;
1565
+ messageId: z.ZodOptional<z.ZodString>;
1566
+ }, "strip", z.ZodTypeAny, {
1567
+ reason: "ok" | "target_completed" | "target_cancelled" | "target_not_found";
1568
+ amended: boolean;
1569
+ messageId?: string | undefined;
1570
+ }, {
1571
+ reason: "ok" | "target_completed" | "target_cancelled" | "target_not_found";
1572
+ amended: boolean;
1573
+ messageId?: string | undefined;
1574
+ }>;
1575
+ type AmendPromptResult = z.infer<typeof AmendPromptResult>;
1537
1576
  interface SessionCapabilities {
1538
1577
  attach?: Record<string, never>;
1539
1578
  list?: boolean;
@@ -1783,6 +1822,8 @@ declare class Session {
1783
1822
  private modelHandlers;
1784
1823
  private modeHandlers;
1785
1824
  private usageHandlers;
1825
+ private amendInProgress;
1826
+ private recentlyTerminal;
1786
1827
  constructor(init: SessionInit);
1787
1828
  private broadcastMergedCommands;
1788
1829
  private broadcastAvailableModes;
@@ -1814,6 +1855,9 @@ declare class Session {
1814
1855
  prompt(clientId: string, params: unknown): Promise<unknown>;
1815
1856
  private broadcastPromptReceived;
1816
1857
  private broadcastTurnComplete;
1858
+ private recordTerminal;
1859
+ private broadcastPromptAmended;
1860
+ private findUserEntry;
1817
1861
  private visibleQueueDepth;
1818
1862
  private broadcastQueueAdded;
1819
1863
  private broadcastQueueUpdated;
@@ -1824,6 +1868,9 @@ declare class Session {
1824
1868
  replayPersistedQueue(entries: PersistedQueueEntry[]): void;
1825
1869
  cancelQueuedPrompt(messageId: string): CancelPromptResult;
1826
1870
  updateQueuedPrompt(messageId: string, prompt: unknown[]): UpdatePromptResult;
1871
+ amendPrompt(clientId: string, params: AmendPromptParams): AmendPromptResult;
1872
+ private amendOnHead;
1873
+ private enqueueAmendmentAsFollowUp;
1827
1874
  cancel(clientId: string): Promise<void>;
1828
1875
  forwardRequest(method: string, params: unknown): Promise<unknown>;
1829
1876
  private rewriteForAgent;
@@ -1872,6 +1919,7 @@ declare class Session {
1872
1919
  private persistedFromEntry;
1873
1920
  private drainQueue;
1874
1921
  private runQueueEntry;
1922
+ private clearAmendIfMatches;
1875
1923
  }
1876
1924
 
1877
1925
  declare const SessionRecord: z.ZodObject<{
package/dist/index.js CHANGED
@@ -186,7 +186,15 @@ var TuiConfig = z.object({
186
186
  // on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
187
187
  // running. Set false if your terminal renders this obnoxiously or you
188
188
  // just don't want it.
189
- progressIndicator: z.boolean().default(true)
189
+ progressIndicator: z.boolean().default(true),
190
+ // What the unmodified Enter key does in the prompt composer.
191
+ // "enqueue" (default) — Enter enqueues the prompt (sends immediately
192
+ // when idle, queues behind an in-flight turn); Shift+Enter amends
193
+ // the in-flight turn.
194
+ // "amend" — flips the two: Enter amends the in-flight turn,
195
+ // Shift+Enter enqueues. With no turn in flight either key just
196
+ // enqueues, since there's nothing to amend.
197
+ defaultEnterAction: z.enum(["enqueue", "amend"]).default("enqueue")
190
198
  });
191
199
  var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
192
200
  var ExtensionBody = z.object({
@@ -230,7 +238,8 @@ var HydraConfig = z.object({
230
238
  mouse: true,
231
239
  logMaxBytes: 5 * 1024 * 1024,
232
240
  cwdColumnMaxWidth: 24,
233
- progressIndicator: true
241
+ progressIndicator: true,
242
+ defaultEnterAction: "enqueue"
234
243
  })
235
244
  });
236
245
  function extensionList(config) {
@@ -1024,6 +1033,18 @@ function extractHydraMeta(meta) {
1024
1033
  if (typeof obj.promptQueueing === "boolean") {
1025
1034
  out.promptQueueing = obj.promptQueueing;
1026
1035
  }
1036
+ if (typeof obj.promptCancelling === "boolean") {
1037
+ out.promptCancelling = obj.promptCancelling;
1038
+ }
1039
+ if (typeof obj.promptUpdating === "boolean") {
1040
+ out.promptUpdating = obj.promptUpdating;
1041
+ }
1042
+ if (typeof obj.promptAmending === "boolean") {
1043
+ out.promptAmending = obj.promptAmending;
1044
+ }
1045
+ if (typeof obj.promptPipelining === "boolean") {
1046
+ out.promptPipelining = obj.promptPipelining;
1047
+ }
1027
1048
  if (Array.isArray(obj.queue)) {
1028
1049
  const entries = [];
1029
1050
  for (const raw of obj.queue) {
@@ -1168,6 +1189,34 @@ var UpdatePromptResult = z3.object({
1168
1189
  updated: z3.boolean(),
1169
1190
  reason: z3.enum(["ok", "not_found", "already_running"])
1170
1191
  });
1192
+ var AmendPromptParams = z3.object({
1193
+ sessionId: z3.string(),
1194
+ targetMessageId: z3.string(),
1195
+ prompt: z3.array(z3.unknown()),
1196
+ replaceQueue: z3.boolean().optional(),
1197
+ onTargetCompleted: z3.enum(["reject", "send_anyway"]).optional()
1198
+ });
1199
+ var AmendPromptResult = z3.object({
1200
+ amended: z3.boolean(),
1201
+ reason: z3.enum([
1202
+ "ok",
1203
+ "target_completed",
1204
+ "target_cancelled",
1205
+ "target_not_found"
1206
+ ]),
1207
+ // Present when a prompt was sent or replaced: the amendment's id on
1208
+ // success, or the regular follow-up's id when onTargetCompleted is
1209
+ // "send_anyway" and the daemon forwarded the prompt anyway.
1210
+ messageId: z3.string().optional()
1211
+ });
1212
+ var PromptAmendedParams = z3.object({
1213
+ sessionId: z3.string(),
1214
+ cancelledMessageId: z3.string(),
1215
+ newMessageId: z3.string(),
1216
+ prompt: z3.array(z3.unknown()),
1217
+ originator: PromptOriginatorSchema,
1218
+ amendedAt: z3.number()
1219
+ });
1171
1220
  var ProxyInitializeParams = z3.object({
1172
1221
  protocolVersion: z3.number().optional(),
1173
1222
  proxyInfo: z3.object({
@@ -1646,6 +1695,7 @@ function stripHydraSessionPrefix(id) {
1646
1695
  return id.startsWith(HYDRA_SESSION_PREFIX) ? id.slice(HYDRA_SESSION_PREFIX.length) : id;
1647
1696
  }
1648
1697
  var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
1698
+ var RECENTLY_TERMINAL_LIMIT = 64;
1649
1699
  var Session = class {
1650
1700
  sessionId;
1651
1701
  cwd;
@@ -1745,6 +1795,20 @@ var Session = class {
1745
1795
  modelHandlers = [];
1746
1796
  modeHandlers = [];
1747
1797
  usageHandlers = [];
1798
+ // Set by amendPrompt at the start of a cancel-and-resubmit dance.
1799
+ // broadcastTurnComplete reads it to attach the _meta.amended marker
1800
+ // to the cancelled turn's turn_complete notification, and to fire the
1801
+ // dedicated prompt_amended notification. Cleared when the cancelled
1802
+ // turn's task completes (runQueueEntry) OR if the amendment is
1803
+ // cancelled mid-window via cancel_prompt(M2) before drainQueue picks
1804
+ // it up.
1805
+ amendInProgress;
1806
+ // LRU of recently-terminal messageIds → stopReason. Used by
1807
+ // amendPrompt to resolve targets that completed/cancelled before
1808
+ // the amend arrived. Capped at RECENTLY_TERMINAL_LIMIT entries;
1809
+ // older entries fall out and resolve to target_not_found, which is
1810
+ // the correct behavior.
1811
+ recentlyTerminal = /* @__PURE__ */ new Map();
1748
1812
  constructor(init) {
1749
1813
  this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
1750
1814
  this.cwd = init.cwd;
@@ -2174,7 +2238,7 @@ var Session = class {
2174
2238
  );
2175
2239
  }
2176
2240
  }
2177
- broadcastTurnComplete(originatorClientId, response) {
2241
+ broadcastTurnComplete(originatorClientId, response, promptMessageId, wasAmend) {
2178
2242
  const stopReason = response && typeof response === "object" && "stopReason" in response && typeof response.stopReason === "string" ? response.stopReason : void 0;
2179
2243
  const update = {
2180
2244
  sessionUpdate: "turn_complete",
@@ -2183,15 +2247,83 @@ var Session = class {
2183
2247
  if (stopReason !== void 0) {
2184
2248
  update.stopReason = stopReason;
2185
2249
  }
2250
+ const amend = this.amendInProgress;
2251
+ if (amend && promptMessageId !== void 0 && amend.cancelledMessageId === promptMessageId) {
2252
+ update._meta = {
2253
+ "hydra-acp": {
2254
+ amended: {
2255
+ cancelledMessageId: amend.cancelledMessageId,
2256
+ newMessageId: amend.newMessageId
2257
+ }
2258
+ }
2259
+ };
2260
+ }
2186
2261
  this.promptStartedAt = void 0;
2262
+ if (promptMessageId !== void 0 && stopReason !== void 0) {
2263
+ this.recordTerminal(promptMessageId, stopReason);
2264
+ }
2187
2265
  this.recordAndBroadcast(
2188
2266
  "session/update",
2189
2267
  {
2190
2268
  sessionId: this.sessionId,
2191
2269
  update
2192
2270
  },
2193
- originatorClientId
2271
+ wasAmend ? void 0 : originatorClientId
2194
2272
  );
2273
+ if (amend && promptMessageId !== void 0 && amend.cancelledMessageId === promptMessageId) {
2274
+ this.broadcastPromptAmended(amend);
2275
+ }
2276
+ }
2277
+ // Record that a prompt's turn has ended, with its terminal stopReason.
2278
+ // Used by amendPrompt to resolve targetMessageIds that completed/cancelled
2279
+ // before the amend arrived. LRU-trimmed at RECENTLY_TERMINAL_LIMIT.
2280
+ recordTerminal(messageId, stopReason) {
2281
+ this.recentlyTerminal.set(messageId, {
2282
+ stopReason,
2283
+ terminatedAt: Date.now()
2284
+ });
2285
+ while (this.recentlyTerminal.size > RECENTLY_TERMINAL_LIMIT) {
2286
+ const oldest = this.recentlyTerminal.keys().next().value;
2287
+ if (oldest === void 0) {
2288
+ break;
2289
+ }
2290
+ this.recentlyTerminal.delete(oldest);
2291
+ }
2292
+ }
2293
+ // Fire hydra-acp/prompt_amended for the M1→M2 linkage. The amendment's
2294
+ // current content is read live from the queue entry so any update_prompt
2295
+ // calls during the amend window are reflected. Best-effort: if M2 has
2296
+ // already been cancelled out of the queue by the time we get here, we
2297
+ // skip — the amendInProgress clearing in cancelQueuedPrompt should have
2298
+ // prevented this code path from running in that case.
2299
+ broadcastPromptAmended(amend) {
2300
+ const entry = this.findUserEntry(amend.newMessageId);
2301
+ if (!entry) {
2302
+ return;
2303
+ }
2304
+ const params = {
2305
+ sessionId: this.sessionId,
2306
+ cancelledMessageId: amend.cancelledMessageId,
2307
+ newMessageId: amend.newMessageId,
2308
+ prompt: entry.prompt,
2309
+ originator: entry.originator,
2310
+ amendedAt: Date.now()
2311
+ };
2312
+ this.broadcastQueueNotification(
2313
+ "hydra-acp/prompt_amended",
2314
+ params
2315
+ );
2316
+ }
2317
+ // Look up a user-prompt queue entry by messageId, searching both the
2318
+ // currentEntry slot and the waiting queue.
2319
+ findUserEntry(messageId) {
2320
+ if (this.currentEntry?.messageId === messageId && this.currentEntry.kind === "user") {
2321
+ return this.currentEntry;
2322
+ }
2323
+ const queued = this.promptQueue.find(
2324
+ (e) => e.messageId === messageId && e.kind === "user"
2325
+ );
2326
+ return queued?.kind === "user" ? queued : void 0;
2195
2327
  }
2196
2328
  // Total visible-or-running entries: the in-flight head (if any) plus
2197
2329
  // the queue's user-visible waiting entries. Internal entries don't
@@ -2204,9 +2336,9 @@ var Session = class {
2204
2336
  }
2205
2337
  return count;
2206
2338
  }
2207
- broadcastQueueAdded(entry) {
2339
+ broadcastQueueAdded(entry, options) {
2208
2340
  const depth = this.visibleQueueDepth();
2209
- const position = Math.max(0, depth - 1);
2341
+ const position = options?.position ?? Math.max(0, depth - 1);
2210
2342
  const params = {
2211
2343
  sessionId: this.sessionId,
2212
2344
  messageId: entry.messageId,
@@ -2216,6 +2348,11 @@ var Session = class {
2216
2348
  queueDepth: depth,
2217
2349
  enqueuedAt: entry.enqueuedAt
2218
2350
  };
2351
+ if (options?.amending !== void 0) {
2352
+ params._meta = {
2353
+ "hydra-acp": { amending: options.amending }
2354
+ };
2355
+ }
2219
2356
  this.broadcastQueueNotification("hydra-acp/prompt_queue_added", params);
2220
2357
  }
2221
2358
  broadcastQueueUpdated(messageId, prompt) {
@@ -2338,6 +2475,9 @@ var Session = class {
2338
2475
  this.broadcastQueueRemoved(messageId, "cancelled");
2339
2476
  this.persistRewrite();
2340
2477
  }
2478
+ if (this.amendInProgress?.newMessageId === messageId) {
2479
+ this.amendInProgress = void 0;
2480
+ }
2341
2481
  entry.resolve({ stopReason: "cancelled" });
2342
2482
  return { cancelled: true, reason: "ok" };
2343
2483
  }
@@ -2359,6 +2499,143 @@ var Session = class {
2359
2499
  this.persistRewrite();
2360
2500
  return { updated: true, reason: "ok" };
2361
2501
  }
2502
+ // Amend the head prompt: cancel the in-flight turn and submit a
2503
+ // replacement that sits at the head of the queue. Resolves the
2504
+ // request immediately (the caller doesn't wait on cancel-settle).
2505
+ // Honours race outcomes — if the target finished or was cancelled
2506
+ // before this arrived, the request resolves with an outcome explaining
2507
+ // why and (depending on onTargetCompleted) optionally forwards as a
2508
+ // plain prompt. Queued targets are edited in place (same machinery
2509
+ // as updateQueuedPrompt).
2510
+ amendPrompt(clientId, params) {
2511
+ const client = this.clients.get(clientId);
2512
+ if (!client) {
2513
+ throw withCode(
2514
+ new Error("client not attached"),
2515
+ JsonRpcErrorCodes.SessionNotFound
2516
+ );
2517
+ }
2518
+ const { targetMessageId, prompt, replaceQueue, onTargetCompleted } = params;
2519
+ if (this.currentEntry?.messageId === targetMessageId && this.currentEntry.kind === "user" && !this.currentEntry.cancelled && this.amendInProgress === void 0) {
2520
+ return this.amendOnHead(client, prompt, targetMessageId, replaceQueue);
2521
+ }
2522
+ const queuedEntry = this.promptQueue.find(
2523
+ (e) => e.messageId === targetMessageId && e.kind === "user"
2524
+ );
2525
+ if (queuedEntry && queuedEntry.kind === "user" && !queuedEntry.cancelled) {
2526
+ queuedEntry.prompt = prompt;
2527
+ this.broadcastQueueUpdated(targetMessageId, prompt);
2528
+ this.persistRewrite();
2529
+ return { amended: true, reason: "ok", messageId: targetMessageId };
2530
+ }
2531
+ const terminal = this.recentlyTerminal.get(targetMessageId);
2532
+ if (terminal) {
2533
+ if (terminal.stopReason === "cancelled") {
2534
+ return { amended: false, reason: "target_cancelled" };
2535
+ }
2536
+ if (onTargetCompleted === "send_anyway") {
2537
+ const newMessageId = this.enqueueAmendmentAsFollowUp(client, prompt);
2538
+ return {
2539
+ amended: false,
2540
+ reason: "target_completed",
2541
+ messageId: newMessageId
2542
+ };
2543
+ }
2544
+ return { amended: false, reason: "target_completed" };
2545
+ }
2546
+ return { amended: false, reason: "target_not_found" };
2547
+ }
2548
+ // Head-of-queue amendment: splice M2 in front of any waiting entries,
2549
+ // broadcast the amend window's queue_added with the amending hint,
2550
+ // mark amendInProgress so the cancelled turn's broadcastTurnComplete
2551
+ // attaches the _meta marker and fires prompt_amended, then fire the
2552
+ // upstream session/cancel without awaiting it. drainQueue is already
2553
+ // running on the head; when its session/prompt returns, it advances
2554
+ // to M2 in the normal way.
2555
+ amendOnHead(client, prompt, targetMessageId, replaceQueue) {
2556
+ const newMessageId = generateMessageId();
2557
+ const originator = { clientId: client.clientId };
2558
+ if (client.clientInfo?.name) {
2559
+ originator.name = client.clientInfo.name;
2560
+ }
2561
+ if (client.clientInfo?.version) {
2562
+ originator.version = client.clientInfo.version;
2563
+ }
2564
+ if (replaceQueue) {
2565
+ const survivors = [];
2566
+ for (const entry2 of this.promptQueue) {
2567
+ if (entry2.kind === "user" && !entry2.cancelled) {
2568
+ entry2.cancelled = true;
2569
+ this.broadcastQueueRemoved(entry2.messageId, "cancelled");
2570
+ entry2.resolve({ stopReason: "cancelled" });
2571
+ continue;
2572
+ }
2573
+ survivors.push(entry2);
2574
+ }
2575
+ this.promptQueue = survivors;
2576
+ }
2577
+ const entry = {
2578
+ kind: "user",
2579
+ messageId: newMessageId,
2580
+ originator,
2581
+ clientId: client.clientId,
2582
+ prompt,
2583
+ enqueuedAt: Date.now(),
2584
+ cancelled: false,
2585
+ wasAmend: true,
2586
+ // No-op resolve/reject: there's no client request awaiting M2's
2587
+ // session/prompt response. The amend_prompt request has already
2588
+ // returned by this point. drainQueue calls these unconditionally
2589
+ // when runQueueEntry settles; making them no-ops is safe.
2590
+ resolve: () => void 0,
2591
+ reject: () => void 0
2592
+ };
2593
+ this.promptQueue.unshift(entry);
2594
+ this.persistRewrite();
2595
+ this.broadcastQueueAdded(entry, {
2596
+ amending: targetMessageId,
2597
+ position: 1
2598
+ });
2599
+ this.amendInProgress = {
2600
+ cancelledMessageId: targetMessageId,
2601
+ newMessageId
2602
+ };
2603
+ void this.agent.connection.notify("session/cancel", { sessionId: this.upstreamSessionId }).catch(() => void 0);
2604
+ return {
2605
+ amended: true,
2606
+ reason: "ok",
2607
+ messageId: newMessageId
2608
+ };
2609
+ }
2610
+ // Send the amendment as a plain follow-up prompt — used when the
2611
+ // target already completed and the caller opted in to send_anyway.
2612
+ // Returns the new prompt's messageId so the result can surface it.
2613
+ enqueueAmendmentAsFollowUp(client, prompt) {
2614
+ const messageId = generateMessageId();
2615
+ const originator = { clientId: client.clientId };
2616
+ if (client.clientInfo?.name) {
2617
+ originator.name = client.clientInfo.name;
2618
+ }
2619
+ if (client.clientInfo?.version) {
2620
+ originator.version = client.clientInfo.version;
2621
+ }
2622
+ const entry = {
2623
+ kind: "user",
2624
+ messageId,
2625
+ originator,
2626
+ clientId: client.clientId,
2627
+ prompt,
2628
+ enqueuedAt: Date.now(),
2629
+ cancelled: false,
2630
+ resolve: () => void 0,
2631
+ reject: () => void 0
2632
+ };
2633
+ this.promptQueue.push(entry);
2634
+ this.persistRewrite();
2635
+ this.broadcastQueueAdded(entry);
2636
+ void this.drainQueue();
2637
+ return messageId;
2638
+ }
2362
2639
  async cancel(clientId) {
2363
2640
  const client = this.clients.get(clientId);
2364
2641
  if (!client) {
@@ -3217,6 +3494,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
3217
3494
  try {
3218
3495
  const result = await this.runQueueEntry(next);
3219
3496
  next.resolve(result);
3497
+ await Promise.resolve();
3220
3498
  } catch (err) {
3221
3499
  next.reject(err);
3222
3500
  } finally {
@@ -3253,12 +3531,33 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
3253
3531
  }
3254
3532
  );
3255
3533
  } catch (err) {
3256
- this.broadcastTurnComplete(entry.clientId, { stopReason: "error" });
3534
+ this.broadcastTurnComplete(
3535
+ entry.clientId,
3536
+ { stopReason: "error" },
3537
+ entry.messageId,
3538
+ entry.wasAmend
3539
+ );
3540
+ this.clearAmendIfMatches(entry.messageId);
3257
3541
  throw err;
3258
3542
  }
3259
- this.broadcastTurnComplete(entry.clientId, response);
3543
+ this.broadcastTurnComplete(
3544
+ entry.clientId,
3545
+ response,
3546
+ entry.messageId,
3547
+ entry.wasAmend
3548
+ );
3549
+ this.clearAmendIfMatches(entry.messageId);
3260
3550
  return response;
3261
3551
  }
3552
+ // Clear amendInProgress once the cancelled turn's task has fully
3553
+ // settled. broadcastTurnComplete needs the marker still set when it
3554
+ // fires, so the clear must happen *after*. Called from runQueueEntry's
3555
+ // settle path for both success and error.
3556
+ clearAmendIfMatches(messageId) {
3557
+ if (this.amendInProgress?.cancelledMessageId === messageId) {
3558
+ this.amendInProgress = void 0;
3559
+ }
3560
+ }
3262
3561
  };
3263
3562
  function withCode(err, code) {
3264
3563
  err.code = code;
@@ -5992,7 +6291,16 @@ function mapModel(u) {
5992
6291
  }
5993
6292
  function mapTurnComplete(u) {
5994
6293
  const stopReason = readString(u, "stopReason");
5995
- return stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" };
6294
+ const meta = u._meta;
6295
+ const amended = meta?.["hydra-acp"]?.amended !== void 0 && meta["hydra-acp"].amended !== null;
6296
+ const out = { kind: "turn-complete" };
6297
+ if (stopReason !== void 0) {
6298
+ out.stopReason = stopReason;
6299
+ }
6300
+ if (amended) {
6301
+ out.amended = true;
6302
+ }
6303
+ return out;
5996
6304
  }
5997
6305
  function extractContentText(content) {
5998
6306
  if (typeof content === "string") {
@@ -7111,6 +7419,22 @@ function registerAcpWsEndpoint(app, deps) {
7111
7419
  }
7112
7420
  return session.updateQueuedPrompt(params.messageId, params.prompt);
7113
7421
  });
7422
+ connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
7423
+ const params = AmendPromptParams.parse(raw);
7424
+ const att = state.attached.get(params.sessionId);
7425
+ if (!att) {
7426
+ const err = new Error("not attached to session");
7427
+ err.code = JsonRpcErrorCodes.SessionNotFound;
7428
+ throw err;
7429
+ }
7430
+ const session = deps.manager.get(params.sessionId);
7431
+ if (!session) {
7432
+ const err = new Error(`session ${params.sessionId} not found`);
7433
+ err.code = JsonRpcErrorCodes.SessionNotFound;
7434
+ throw err;
7435
+ }
7436
+ return session.amendPrompt(att.clientId, params);
7437
+ });
7114
7438
  connection.onRequest("session/load", async (raw) => {
7115
7439
  const rawObj = raw ?? {};
7116
7440
  const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
@@ -7263,10 +7587,17 @@ function buildInitializeResult() {
7263
7587
  ],
7264
7588
  // Advertise hydra-only capabilities via _meta["hydra-acp"]. Generic
7265
7589
  // ACP clients ignore the field; capability-aware clients learn here
7266
- // that hydra accepts concurrent session/prompt requests and emits
7267
- // prompt_queue_* notifications so they can stop running their own
7268
- // local queue.
7269
- _meta: mergeMeta(void 0, { promptQueueing: true })
7590
+ // which hydra-acp extensions the daemon supports so they can gate
7591
+ // UI surface accordingly. promptPipelining is false until the
7592
+ // streaming-input probe lands (Option A in the steering brief);
7593
+ // the others are unconditional method-availability flags.
7594
+ _meta: mergeMeta(void 0, {
7595
+ promptQueueing: true,
7596
+ promptCancelling: true,
7597
+ promptUpdating: true,
7598
+ promptAmending: true,
7599
+ promptPipelining: false
7600
+ })
7270
7601
  };
7271
7602
  }
7272
7603
  function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/cli",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "Multi-client ACP session daemon: spawn agents, attach over WSS, multiplex sessions across editors.",
5
5
  "license": "MIT",
6
6
  "type": "module",