@rubytech/create-maxy-code 0.1.491 → 0.1.493

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.
Files changed (73) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +46 -0
  3. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  4. package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.d.ts +2 -0
  5. package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.d.ts.map +1 -0
  6. package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.js +135 -0
  7. package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.js.map +1 -0
  8. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +7 -1
  9. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  10. package/payload/platform/lib/storage-broker/dist/cf-exec.js +26 -14
  11. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  12. package/payload/platform/lib/storage-broker/dist/pages-output-dir.d.ts +19 -0
  13. package/payload/platform/lib/storage-broker/dist/pages-output-dir.d.ts.map +1 -0
  14. package/payload/platform/lib/storage-broker/dist/pages-output-dir.js +185 -0
  15. package/payload/platform/lib/storage-broker/dist/pages-output-dir.js.map +1 -0
  16. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +65 -0
  17. package/payload/platform/lib/storage-broker/src/__tests__/pages-output-dir.test.ts +167 -0
  18. package/payload/platform/lib/storage-broker/src/cf-exec.ts +40 -16
  19. package/payload/platform/lib/storage-broker/src/pages-output-dir.ts +194 -0
  20. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  21. package/payload/platform/plugins/admin/hooks/__tests__/quote-render-pdf-conformance.test.sh +99 -0
  22. package/payload/platform/plugins/admin/hooks/quote-render-pdf-conformance.sh +108 -0
  23. package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +18 -6
  24. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +13 -1
  25. package/payload/platform/plugins/cloudflare/bin/__tests__/portal-brand-css.test.sh +1 -1
  26. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +22 -0
  27. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +111 -3
  28. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +13 -0
  29. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +8 -4
  30. package/payload/platform/plugins/cloudflare/references/hosting-sites.md +1 -1
  31. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +4 -4
  32. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +12 -0
  33. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +8 -0
  34. package/payload/platform/scripts/lib/provision-account-dir.sh +6 -0
  35. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  36. package/payload/platform/services/claude-session-manager/dist/http-server.js +35 -6
  37. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/index.js +62 -1
  39. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +7 -0
  41. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +9 -0
  43. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.d.ts +80 -0
  45. package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.d.ts.map +1 -0
  46. package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.js +216 -0
  47. package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.js.map +1 -0
  48. package/payload/platform/templates/agents/admin/IDENTITY.md +5 -1
  49. package/payload/platform/templates/agents/passive/IDENTITY.md +2 -0
  50. package/payload/platform/templates/agents/public/IDENTITY.md +2 -0
  51. package/payload/platform/templates/specialists/agents/citation-auditor.md +1 -0
  52. package/payload/platform/templates/specialists/agents/coding-assistant.md +1 -0
  53. package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +3 -0
  54. package/payload/platform/templates/specialists/agents/content-producer.md +1 -0
  55. package/payload/platform/templates/specialists/agents/data-manager.md +1 -0
  56. package/payload/platform/templates/specialists/agents/database-operator.md +1 -0
  57. package/payload/platform/templates/specialists/agents/librarian.md +1 -0
  58. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -0
  59. package/payload/platform/templates/specialists/agents/project-manager.md +1 -0
  60. package/payload/platform/templates/specialists/agents/public-session-reviewer.md +1 -0
  61. package/payload/platform/templates/specialists/agents/research-assistant.md +3 -0
  62. package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +1 -0
  63. package/payload/premium-plugins/writer-craft/agents/writer-craft--manuscript-reviewer.md +3 -0
  64. package/payload/server/{chunk-KZFE3MOY.js → chunk-RPUTKSAJ.js} +134 -16
  65. package/payload/server/public/assets/{chat-DWvYv9jq.js → chat-1SSmFsjd.js} +1 -1
  66. package/payload/server/public/assets/{operator-CldXXilj.js → operator-Cxq9TxaH.js} +1 -1
  67. package/payload/server/public/assets/{page-BIwdWXnv.js → page-DBHzC59_.js} +1 -1
  68. package/payload/server/public/assets/{public-BKQEQGPE.js → public-jIzvdjt_.js} +1 -1
  69. package/payload/server/public/chat.html +2 -2
  70. package/payload/server/public/operator.html +2 -2
  71. package/payload/server/public/public.html +2 -2
  72. package/payload/server/server.js +40 -31
  73. package/payload/server/{src-UYDFMWF4.js → src-JN6PIAJN.js} +1 -1
@@ -309,13 +309,85 @@ function generationClient() {
309
309
  return []
310
310
  }
311
311
  if (/DELETE FROM directory\b/i.test(sql)) {
312
- const keep = Number(params[1])
313
- for (let i = rows.length - 1; i >= 0; i--) {
314
- if (rows[i].generation !== keep) rows.splice(i, 1)
312
+ const g = Number(params[1])
313
+ // Two DELETE shapes reach this client: the post-flip sweep removes
314
+ // every OTHER generation (`generation != ?`); the pre-stage clear
315
+ // (Task 1923) removes exactly THIS generation (`generation = ?`).
316
+ if (/generation\s*!=\s*\?/i.test(sql)) {
317
+ for (let i = rows.length - 1; i >= 0; i--) if (rows[i].generation !== g) rows.splice(i, 1)
318
+ } else {
319
+ for (let i = rows.length - 1; i >= 0; i--) if (rows[i].generation === g) rows.splice(i, 1)
315
320
  }
316
321
  return []
317
322
  }
318
323
  if (/SELECT COUNT/i.test(sql)) {
324
+ // The pre-stage orphan count (Task 1923) filters `generation = ?`; the
325
+ // verify count joins the pointer. Only the former binds a generation.
326
+ if (/generation\s*=\s*\?/i.test(sql)) {
327
+ const g = Number(params[1])
328
+ return [{ n: rows.filter((r) => r.generation === g).length }]
329
+ }
330
+ return [{ n: rows.filter((r) => r.generation === pointer).length }]
331
+ }
332
+ return []
333
+ },
334
+ }
335
+ }
336
+
337
+ // The wedge Task 1923 fixes: a stage that half-completed on a D1 500 leaves
338
+ // orphan rows at the NEXT generation while the pointer stays at current. This
339
+ // client models the one property that turns that into a permanent freeze — the
340
+ // D1 UNIQUE(accountId, relPath, generation) constraint — by throwing on a
341
+ // colliding INSERT, exactly as every later cycle's first re-stage did.
342
+ function wedgeableClient(seed: { pointer: number; current?: string[]; orphans: string[] }) {
343
+ const calls: { sql: string; params: unknown[] }[] = []
344
+ const rows: { accountId: string; relPath: string; generation: number }[] = []
345
+ let pointer: number | null = seed.pointer
346
+ const orphanGen = seed.pointer + 1
347
+ // The last good tree the pointer names, present in a real wedge alongside the
348
+ // orphans. Seeding it proves the pre-stage clear scopes to `next` and leaves
349
+ // the reader-visible current generation untouched.
350
+ for (const relPath of seed.current ?? []) rows.push({ accountId: 'acc-1', relPath, generation: seed.pointer })
351
+ for (const relPath of seed.orphans) rows.push({ accountId: 'acc-1', relPath, generation: orphanGen })
352
+ return {
353
+ calls,
354
+ rows,
355
+ pointerValue: () => pointer,
356
+ async query(sql: string, params: unknown[] = []) {
357
+ calls.push({ sql, params })
358
+ if (/SELECT\s+currentGeneration/i.test(sql)) {
359
+ return pointer === null ? [] : [{ currentGeneration: pointer }]
360
+ }
361
+ if (/INSERT INTO directory_state|UPDATE directory_state/i.test(sql)) {
362
+ pointer = Number(params[1] ?? params[0])
363
+ return []
364
+ }
365
+ if (/INSERT INTO directory \(/i.test(sql)) {
366
+ const accountId = String(params[0])
367
+ const relPath = String(params[1])
368
+ const generation = Number(params[6])
369
+ if (rows.some((r) => r.accountId === accountId && r.relPath === relPath && r.generation === generation)) {
370
+ throw new Error(
371
+ 'd1 query failed: HTTP 500 — UNIQUE constraint failed: directory.accountId, directory.relPath, directory.generation',
372
+ )
373
+ }
374
+ rows.push({ accountId, relPath, generation })
375
+ return []
376
+ }
377
+ if (/DELETE FROM directory\b/i.test(sql)) {
378
+ const g = Number(params[1])
379
+ if (/generation\s*!=\s*\?/i.test(sql)) {
380
+ for (let i = rows.length - 1; i >= 0; i--) if (rows[i].generation !== g) rows.splice(i, 1)
381
+ } else {
382
+ for (let i = rows.length - 1; i >= 0; i--) if (rows[i].generation === g) rows.splice(i, 1)
383
+ }
384
+ return []
385
+ }
386
+ if (/SELECT COUNT/i.test(sql)) {
387
+ if (/generation\s*=\s*\?/i.test(sql)) {
388
+ const g = Number(params[1])
389
+ return [{ n: rows.filter((r) => r.generation === g).length }]
390
+ }
319
391
  return [{ n: rows.filter((r) => r.generation === pointer).length }]
320
392
  }
321
393
  return []
@@ -323,6 +395,42 @@ function generationClient() {
323
395
  }
324
396
  }
325
397
 
398
+ describe('a partial stage self-heals (Task 1923)', () => {
399
+ it('clears orphans at the next generation, flips onto it, and logs stage-clear', async () => {
400
+ // Pointer at 5 naming the last good tree; three orphan rows already at
401
+ // generation 6 for relPaths the current walk re-stages ('output' is the
402
+ // walk's first row, so the current code's first INSERT is the one that
403
+ // collides). Without the pre-stage clear that INSERT throws on the UNIQUE
404
+ // constraint and the push fails, which is what froze gls-data for ~6 h on
405
+ // 2026-07-22.
406
+ const c = wedgeableClient({
407
+ pointer: 5,
408
+ current: ['output', 'output/report.pdf', 'quotes', 'quotes/CR2969'],
409
+ orphans: ['output', 'output/report.pdf', 'quotes'],
410
+ })
411
+ const lines: string[] = []
412
+ const r = await push(c, { log: (l: string) => lines.push(l) })
413
+
414
+ expect(r.rows).toBe(7)
415
+ expect(c.pointerValue()).toBe(6)
416
+ // Nothing survives at a generation the pointer does not name.
417
+ expect(c.rows.every((row) => row.generation === 6)).toBe(true)
418
+ const clear = lines.find((l) => l.includes('op=stage-clear'))!
419
+ expect(clear).toContain('account=acc-1')
420
+ expect(clear).toContain('generation=6')
421
+ // Exactly the 3 orphans at generation 6, not the 4 current-tree rows the
422
+ // pointer names: the clear scopes to `next`, never to a reader-visible row.
423
+ expect(clear).toContain('cleared=3')
424
+ })
425
+
426
+ it('emits no stage-clear line on a clean cycle with no orphans', async () => {
427
+ const c = wedgeableClient({ pointer: 5, orphans: [] })
428
+ const lines: string[] = []
429
+ await push(c, { log: (l: string) => lines.push(l) })
430
+ expect(lines.some((l) => l.includes('op=stage-clear'))).toBe(false)
431
+ })
432
+ })
433
+
326
434
  describe('the index replace is atomic', () => {
327
435
  it('never issues a delete before the pointer flip', async () => {
328
436
  // The delete-then-insert window this replaces was Task 1842: a client
@@ -60,6 +60,7 @@ const deadHead = async () => {
60
60
  throw new Error('ECONNREFUSED')
61
61
  }
62
62
  const errHead = async () => ({ ok: false, status: 502 })
63
+ const missingHead = async () => ({ ok: false, status: 404 })
63
64
 
64
65
  describe('processIndexedDownload', () => {
65
66
  it('returns a signed url for an indexed file when the device answers', async () => {
@@ -87,6 +88,18 @@ describe('processIndexedDownload', () => {
87
88
  expect(r.payload.error).toBe('device-offline')
88
89
  })
89
90
 
91
+ // A live device that answers 404 means the indexed row is stale: the file was
92
+ // renamed or removed while the index was frozen. That is "refresh the list",
93
+ // not "the device is down", so it must not collapse into device-offline.
94
+ it('reports file-missing when a reachable device 404s the path', async () => {
95
+ const lines: string[] = []
96
+ const r = await processIndexedDownload('sess-a', PATH, makeEnv(), (l) => lines.push(l), 1000, missingHead as never)
97
+ expect(r.status).toBe(404)
98
+ expect(r.payload.error).toBe('file-missing')
99
+ expect(r.url).toBeUndefined()
100
+ expect(lines.some((l) => l.includes('result=file-missing'))).toBe(true)
101
+ })
102
+
90
103
  it('404s a path that is not in the index, without contacting the device', async () => {
91
104
  let touched = false
92
105
  const spy = async () => {
@@ -55,10 +55,14 @@ describe('schema.sql', () => {
55
55
  })
56
56
  })
57
57
 
58
- // The broker runs `wrangler pages deploy <dir>` with <dir> = the site root, so
59
- // the positional directory IS the asset root and pages_build_output_dir is
60
- // overridden. A nested public/ therefore publishes the site one level too deep
61
- // and `/` returns 404 the live failure Task 1774 closes.
58
+ // The template's flat shape and its `pages_build_output_dir = "."` have to agree,
59
+ // because the two together are what puts index.html at the served root. Until
60
+ // Task 1917 the broker handed wrangler the site root as the positional asset
61
+ // directory, which overrode the declared output dir, so a nested public/ served
62
+ // the site one level too deep and `/` returned 404 — the live failure Task 1774
63
+ // closed by flattening. Since 1917 the broker publishes the declared output dir,
64
+ // so a mismatch here no longer 404s; these assertions keep the shipped template
65
+ // on the one shape that is deployed and measured.
62
66
  describe('template layout matches what the deploy publishes', () => {
63
67
  it('has no public/ level', () => {
64
68
  expect(existsSync(join(TEMPLATE, 'public'))).toBe(false)
@@ -33,7 +33,7 @@ If the site captures form data, add the `[[d1_databases]]` binding from `d1-data
33
33
 
34
34
  ## 3. Load the reused deploy token
35
35
 
36
- > **On a multi-tenant install, a client account does not run this step at all.** After the 1631 storage isolation, a client's `cloudflare.env` carries no master, and `cf-token.sh` denies it the `pages` scope (`reason=not-house`) — correctly, since that master is account-wide. A client account publishes with the `storage-pages-deploy` tool, which runs the deploy house-side; the project must already be recorded to the account by the house (`storage-pages-adopt`), because the project-to-folder mapping is not derivable and is never guessed. The rest of this section applies to the **house account and single-tenant installs**, where the master still lives in the account secrets file. If you are a client account and you reached a `not-house` denial here, that is this branch, not a broken token: use the tool.
36
+ > **On a multi-tenant install, a client account does not run this step at all.** After the 1631 storage isolation, a client's `cloudflare.env` carries no master, and `cf-token.sh` denies it the `pages` scope (`reason=not-house`) — correctly, since that master is account-wide. A client account publishes with the `storage-pages-deploy` tool, which runs the deploy house-side; the project must already be recorded to the account by the house (`storage-pages-adopt`), because the project-to-folder mapping is not derivable and is never guessed. The rest of this section applies to the **house account and single-tenant installs**, where the master still lives in the account secrets file. If you are a client account and you reached a `not-house` denial here, that is this branch, not a broken token: use the tool. The tool reads `pages_build_output_dir` from the site root's `wrangler.toml` (or `wrangler.json`/`.jsonc`) and publishes that directory as the served root, which is the same asset root step 4 below passes positionally. A declared output dir that is missing fails the deploy naming the path rather than publishing its parent.
37
37
 
38
38
  A Pages deploy needs **Account · Cloudflare Pages · Edit**. If the site has Pages Functions hitting D1, the token also needs **Account · D1 · Edit** (the single most common breakage — see `d1-data-capture.md`). The `pages` scope resolves the stable `<brand>-pages-d1` token, which carries both. Resolve it with the deterministic helper, which mints it once only if absent and reuses it thereafter. This is the one command; do not source the secrets file and hand the resulting `CLOUDFLARE_API_TOKEN` to `wrangler` — that variable holds the **master**, not the per-scope token, and `wrangler pages` then returns `Authentication error [code: 10000]`:
39
39
 
@@ -98,11 +98,11 @@ Copy `template/` to `<accountDir>/sites/<project>/` and fill `wrangler.toml`'s `
98
98
  placeholder in the assembled tree.**
99
99
 
100
100
  `sites/<project>/` is the only location the broker will publish: `resolveSiteDir` resolves
101
- `<accountDir>/sites/<name>` and refuses anything else with `invalid-site`. The tree is flat because
102
- the deploy publishes the directory it is handed — `index.html`, `portal.css`, `portal.js`,
101
+ `<accountDir>/sites/<name>` and refuses anything else with `invalid-site`. The tree is flat: `index.html`, `portal.css`, `portal.js`,
103
102
  `functions/` and `wrangler.toml` all sit at its root, with no nested asset level, and
104
- `pages_build_output_dir` reads `"."` to match. A nested asset level publishes the site one level too
105
- deep, which serves 404 at the domain root.
103
+ `pages_build_output_dir` reads `"."` to match. The deploy publishes whatever
104
+ directory `pages_build_output_dir` declares, so the two must agree; a nested asset level that the
105
+ config does not declare publishes the site one level too deep, which serves 404 at the domain root.
106
106
 
107
107
  The deploy serves everything in that tree except a fixed ignore list — `functions/`, `_worker.js`,
108
108
  `_redirects`, `_headers`, `_routes.json`, `node_modules`, `.git`, `.DS_Store`, `.wrangler` — and
@@ -171,6 +171,18 @@ export async function processIndexedDownload(
171
171
  try {
172
172
  const head = await fetchFn(url, { method: 'HEAD' })
173
173
  if (!head.ok) {
174
+ if (head.status === 404) {
175
+ // The device answered, so it is not down: this row is stale. The file
176
+ // was renamed or removed while the index was frozen, and the device
177
+ // returns the same 404 for a path that is missing and one that is not
178
+ // exposed — so this cannot and must not try to tell those apart. The
179
+ // client's remedy is "refresh the list", not "wait for the device".
180
+ log(
181
+ `[data-portal] op=fetch-through owner=${q(session.ownerId)} path=${q(relPath)} ` +
182
+ `status=404 ms=${Date.now() - started} result=file-missing`,
183
+ )
184
+ return { status: 404, payload: { ok: false, error: 'file-missing' } }
185
+ }
174
186
  log(
175
187
  `[data-portal] op=fetch-through owner=${q(session.ownerId)} path=${q(relPath)} ` +
176
188
  `status=${head.status} ms=${Date.now() - started} result=device-error`,
@@ -292,6 +292,14 @@
292
292
  say('That file is on the device, which is not reachable right now. Try again shortly.');
293
293
  return;
294
294
  }
295
+ /* A reachable device that 404s a stale row sends error:'file-missing'.
296
+ The message says refresh, not wait. Keyed on the string, so the
297
+ edge's own not-found 404s (not-granted, absent, is-directory) carry
298
+ error:'not found' and keep the generic message below. */
299
+ if (res.d && res.d.error === 'file-missing') {
300
+ say('That file is no longer available under this name. Refresh the list and try again.');
301
+ return;
302
+ }
295
303
  if (!res.d.ok || !res.d.url) {
296
304
  say('That file could not be opened.');
297
305
  return;
@@ -128,6 +128,12 @@ provision_account_dir() {
128
128
  { "type": "command", "command": "bash $HOOKS_PATH/post-tool-use-agent.sh" }
129
129
  ]
130
130
  },
131
+ {
132
+ "matcher": "mcp__plugin_browser_browser__browser-pdf-save",
133
+ "hooks": [
134
+ { "type": "command", "command": "bash $HOOKS_PATH/quote-render-pdf-conformance.sh" }
135
+ ]
136
+ },
131
137
  {
132
138
  "matcher": "mcp__.*__.*-(export|import)-parse$",
133
139
  "hooks": [
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
@@ -2071,6 +2071,10 @@ export function buildHttpApp(deps) {
2071
2071
  }
2072
2072
  const byte = body.byte;
2073
2073
  const trigger = typeof body.trigger === 'string' ? body.trigger : 'manual';
2074
+ // Task 1920 — the stop button sets escalate:true so that when ESC proves
2075
+ // ineffective (turn blocked in a tool/subagent call) the manager escalates
2076
+ // once to Ctrl-C. Absent/false keeps the ESC-only re-send path.
2077
+ const escalate = body.escalate === true;
2074
2078
  const intId = `${sessionId.slice(0, 8)}-${start}`;
2075
2079
  deps.logger(`[webchat-interrupt] op=interrupt-requested intId=${intId} sessionId=${sessionId.slice(0, 8)} trigger=${trigger}`);
2076
2080
  const result = writeInterruptToPty(sessionId, byte, deps.logger);
@@ -2115,6 +2119,11 @@ export function buildHttpApp(deps) {
2115
2119
  let lastSendAt = sentAt;
2116
2120
  let escSends = 1; // the initial ESC already written above
2117
2121
  let canResend = true;
2122
+ // Task 1920 — the one-shot escalation guard. Seed it from the initial byte:
2123
+ // if the pre-loop write above already sent Ctrl-C (byte='ctrl-c'), the loop
2124
+ // must not send a second one. The invariant is "exactly one Ctrl-C ever per
2125
+ // call" because a second SIGINT can exit the CLI and kill the session.
2126
+ let ctrlCSent = byte === 'ctrl-c';
2118
2127
  while (Date.now() - sentAt < stopMs) {
2119
2128
  const st = deps.watcher.getBySessionId(sessionId)?.status;
2120
2129
  if (st !== 'busy') {
@@ -2122,14 +2131,34 @@ export function buildHttpApp(deps) {
2122
2131
  break;
2123
2132
  }
2124
2133
  if (canResend && Date.now() - lastSendAt >= reescalateMs) {
2125
- const re = writeInterruptToPty(sessionId, byte, deps.logger);
2126
- lastSendAt = Date.now();
2127
- if (re.ok) {
2128
- escSends += 1;
2129
- deps.logger(`[webchat-interrupt] op=interrupt-reescalated intId=${intId} sends=${escSends}`);
2134
+ if (escalate) {
2135
+ // Task 1920 — ESC ran for a full sub-window while still busy, so it is
2136
+ // ineffective (turn blocked in a tool/subagent call). Escalate once to
2137
+ // Ctrl-C (SIGINT reaches the harness mid-tool-call); never a second
2138
+ // time a second SIGINT can exit the CLI and kill the session. After
2139
+ // the one Ctrl-C, stop sending and just poll for busy-clear.
2140
+ if (!ctrlCSent) {
2141
+ const esc = writeInterruptToPty(sessionId, 'ctrl-c', deps.logger);
2142
+ lastSendAt = Date.now();
2143
+ if (esc.ok) {
2144
+ ctrlCSent = true;
2145
+ deps.logger(`[webchat-interrupt] op=interrupt-escalated intId=${intId} mechanism=ctrl-c sends=${escSends + 1}`);
2146
+ }
2147
+ else {
2148
+ canResend = false; // child vanished mid-interrupt; the next status read exits the loop
2149
+ }
2150
+ }
2130
2151
  }
2131
2152
  else {
2132
- canResend = false; // child vanished mid-interrupt; the next status read exits the loop
2153
+ const re = writeInterruptToPty(sessionId, byte, deps.logger);
2154
+ lastSendAt = Date.now();
2155
+ if (re.ok) {
2156
+ escSends += 1;
2157
+ deps.logger(`[webchat-interrupt] op=interrupt-reescalated intId=${intId} sends=${escSends}`);
2158
+ }
2159
+ else {
2160
+ canResend = false; // child vanished mid-interrupt; the next status read exits the loop
2161
+ }
2133
2162
  }
2134
2163
  }
2135
2164
  await new Promise((r) => setTimeout(r, 120));