@intentic/sandbox-contract 1.310.0 → 1.311.0

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 (118) hide show
  1. package/README.md +38 -159
  2. package/dist/contracts/accounts.contract.d.ts +3 -0
  3. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  4. package/dist/contracts/agent.contract.d.ts +15 -0
  5. package/dist/contracts/agent.contract.d.ts.map +1 -1
  6. package/dist/contracts/agents.contract.d.ts +596 -1
  7. package/dist/contracts/agents.contract.d.ts.map +1 -1
  8. package/dist/contracts/agents.contract.js +13 -2
  9. package/dist/contracts/agents.contract.js.map +1 -1
  10. package/dist/contracts/capabilities.contract.d.ts +2 -0
  11. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  12. package/dist/contracts/device.contract.d.ts +1 -0
  13. package/dist/contracts/device.contract.d.ts.map +1 -1
  14. package/dist/contracts/runner.contract.d.ts +45 -0
  15. package/dist/contracts/runner.contract.d.ts.map +1 -1
  16. package/dist/contracts/settings.contract.d.ts +8 -0
  17. package/dist/contracts/settings.contract.d.ts.map +1 -1
  18. package/dist/contracts/system.contract.d.ts +59 -0
  19. package/dist/contracts/system.contract.d.ts.map +1 -1
  20. package/dist/events/agent-events.d.ts +20 -0
  21. package/dist/events/agent-events.d.ts.map +1 -1
  22. package/dist/events/agent-events.js +6 -1
  23. package/dist/events/agent-events.js.map +1 -1
  24. package/dist/events/system-events.d.ts +48 -0
  25. package/dist/events/system-events.d.ts.map +1 -1
  26. package/dist/events/transcript.d.ts +5 -0
  27. package/dist/events/transcript.d.ts.map +1 -1
  28. package/dist/events/transcript.js +9 -1
  29. package/dist/events/transcript.js.map +1 -1
  30. package/dist/front/generated/wire.d.ts +74 -0
  31. package/dist/front/generated/wire.d.ts.map +1 -0
  32. package/dist/front/generated/wire.js +2 -0
  33. package/dist/front/generated/wire.js.map +1 -0
  34. package/dist/index.d.ts +684 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/policy/command-run.d.ts +1 -1
  39. package/dist/policy/command-run.d.ts.map +1 -1
  40. package/dist/policy/command-run.js +7 -4
  41. package/dist/policy/command-run.js.map +1 -1
  42. package/dist/protocol/ingress-protocol.d.ts.map +1 -1
  43. package/dist/protocol/ingress-protocol.js +15 -0
  44. package/dist/protocol/ingress-protocol.js.map +1 -1
  45. package/dist/schemas/agents.d.ts +72 -3
  46. package/dist/schemas/agents.d.ts.map +1 -1
  47. package/dist/schemas/agents.js +10 -7
  48. package/dist/schemas/agents.js.map +1 -1
  49. package/dist/schemas/automations.d.ts +24 -0
  50. package/dist/schemas/automations.d.ts.map +1 -1
  51. package/dist/schemas/capabilities.d.ts +3 -0
  52. package/dist/schemas/capabilities.d.ts.map +1 -1
  53. package/dist/schemas/capabilities.js +4 -0
  54. package/dist/schemas/capabilities.js.map +1 -1
  55. package/dist/schemas/devices.d.ts +32 -6
  56. package/dist/schemas/devices.d.ts.map +1 -1
  57. package/dist/schemas/devices.js +8 -6
  58. package/dist/schemas/devices.js.map +1 -1
  59. package/dist/schemas/keep-warm.d.ts +68 -0
  60. package/dist/schemas/keep-warm.d.ts.map +1 -0
  61. package/dist/schemas/keep-warm.js +74 -0
  62. package/dist/schemas/keep-warm.js.map +1 -0
  63. package/dist/schemas/providers/provider-oauth.d.ts +3 -0
  64. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  65. package/dist/schemas/providers/provider-oauth.js +4 -0
  66. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  67. package/dist/schemas/providers/usage.d.ts +6 -0
  68. package/dist/schemas/providers/usage.d.ts.map +1 -1
  69. package/dist/schemas/providers/usage.js +4 -0
  70. package/dist/schemas/providers/usage.js.map +1 -1
  71. package/dist/schemas/settings.d.ts +4 -0
  72. package/dist/schemas/settings.d.ts.map +1 -1
  73. package/dist/schemas/settings.js +23 -0
  74. package/dist/schemas/settings.js.map +1 -1
  75. package/dist/state/definition.d.ts +16 -0
  76. package/dist/state/definition.d.ts.map +1 -1
  77. package/dist/state/fix-stance.d.ts.map +1 -1
  78. package/dist/state/fix-stance.js +1 -0
  79. package/dist/state/fix-stance.js.map +1 -1
  80. package/dist/state/runtime-state.d.ts +3 -0
  81. package/dist/state/runtime-state.d.ts.map +1 -1
  82. package/dist/state/runtime-state.js +1 -0
  83. package/dist/state/runtime-state.js.map +1 -1
  84. package/dist/text/transcript-fold.d.ts +2 -0
  85. package/dist/text/transcript-fold.d.ts.map +1 -1
  86. package/dist/text/transcript-fold.js +20 -7
  87. package/dist/text/transcript-fold.js.map +1 -1
  88. package/dist/time/zone.d.ts +1 -0
  89. package/dist/time/zone.d.ts.map +1 -1
  90. package/dist/time/zone.js +5 -0
  91. package/dist/time/zone.js.map +1 -1
  92. package/package.json +16 -5
  93. package/src/contracts/agents.contract.ts +14 -2
  94. package/src/events/agent-events.ts +9 -1
  95. package/src/events/transcript.ts +14 -3
  96. package/src/front/generated/wire.ts +73 -0
  97. package/src/ids/hostnames.fixture.json +20 -0
  98. package/src/ids/hostnames.test.ts +20 -0
  99. package/src/index.ts +1 -0
  100. package/src/policy/command-run.test.ts +35 -0
  101. package/src/policy/command-run.ts +10 -5
  102. package/src/protocol/ingress-protocol.test.ts +45 -1
  103. package/src/protocol/ingress-protocol.ts +25 -4
  104. package/src/schemas/agents.ts +14 -7
  105. package/src/schemas/capabilities.ts +6 -0
  106. package/src/schemas/devices.ts +16 -9
  107. package/src/schemas/keep-warm.test.ts +68 -0
  108. package/src/schemas/keep-warm.ts +124 -0
  109. package/src/schemas/providers/provider-oauth.ts +6 -0
  110. package/src/schemas/providers/usage.ts +7 -0
  111. package/src/schemas/settings.ts +30 -0
  112. package/src/state/fix-stance.test.ts +14 -0
  113. package/src/state/fix-stance.ts +2 -0
  114. package/src/state/runtime-state.ts +4 -0
  115. package/src/text/transcript-fold.test.ts +20 -1
  116. package/src/text/transcript-fold.ts +39 -9
  117. package/src/time/zone.ts +7 -0
  118. package/src/webext/index.ts +1 -1
@@ -22,6 +22,7 @@ export interface WallRule {
22
22
  export declare const cronOptions: (tz: Zone) => {
23
23
  timezone: string;
24
24
  };
25
+ export declare const nextDayStartIn: (at: number, zone: Zone) => number;
25
26
  export declare const sameClock: (a: Zone, b: Zone, at?: number) => boolean;
26
27
  export declare const zoneLabel: (rule: Zone, reader: Zone, at?: number) => string | undefined;
27
28
  //# sourceMappingURL=zone.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"zone.d.ts","sourceRoot":"","sources":["../../src/time/zone.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAGzD,eAAO,MAAM,GAAG,EAAY,IAAI,CAAC;AASjC,eAAO,MAAM,MAAM,UAAW,MAAM,KAAG,KAAK,IAAI,IAS/C,CAAC;AAGF,eAAO,MAAM,MAAM,UAAW,MAAM,GAAG,SAAS,KAAG,IAAI,GAAG,SAAuE,CAAC;AAIlI,eAAO,MAAM,UAAU,+EAAkG,CAAC;AAM1H,eAAO,MAAM,SAAS,QAAO,IAAgE,CAAC;AAO9F,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAC;AAEjE,eAAO,MAAM,cAAc,aAA2E,CAAC;AAGvG,eAAO,MAAM,UAAU,OAAQ,MAAM,QAAQ,IAAI,KAAG,QAC8E,CAAC;AAGnI,eAAO,MAAM,QAAQ,OAAQ,MAAM,KAAG,QAA+D,CAAC;AAStG,eAAO,MAAM,WAAW,OAAQ,MAAM,QAAQ,IAAI,KAAG,MAG5B,CAAC;AAG1B,eAAO,MAAM,UAAU,UAAW,MAAM,GAAG,SAAS,KAAG,QAAQ,GAAG,SAAyG,CAAC;AAO5K,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;CACrB;AAGD,eAAO,MAAM,WAAW,OAAQ,IAAI,KAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAwB,CAAC;AAclF,eAAO,MAAM,SAAS,MAAO,IAAI,KAAK,IAAI,OAAM,MAAM,KAAgB,OAAmE,CAAC;AAM1I,eAAO,MAAM,SAAS,SAAU,IAAI,UAAU,IAAI,OAAM,MAAM,KAAgB,MAAM,GAAG,SAAgF,CAAC"}
1
+ {"version":3,"file":"zone.d.ts","sourceRoot":"","sources":["../../src/time/zone.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAGzD,eAAO,MAAM,GAAG,EAAY,IAAI,CAAC;AASjC,eAAO,MAAM,MAAM,UAAW,MAAM,KAAG,KAAK,IAAI,IAS/C,CAAC;AAGF,eAAO,MAAM,MAAM,UAAW,MAAM,GAAG,SAAS,KAAG,IAAI,GAAG,SAAuE,CAAC;AAIlI,eAAO,MAAM,UAAU,+EAAkG,CAAC;AAM1H,eAAO,MAAM,SAAS,QAAO,IAAgE,CAAC;AAO9F,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAC;AAEjE,eAAO,MAAM,cAAc,aAA2E,CAAC;AAGvG,eAAO,MAAM,UAAU,OAAQ,MAAM,QAAQ,IAAI,KAAG,QAC8E,CAAC;AAGnI,eAAO,MAAM,QAAQ,OAAQ,MAAM,KAAG,QAA+D,CAAC;AAStG,eAAO,MAAM,WAAW,OAAQ,MAAM,QAAQ,IAAI,KAAG,MAG5B,CAAC;AAG1B,eAAO,MAAM,UAAU,UAAW,MAAM,GAAG,SAAS,KAAG,QAAQ,GAAG,SAAyG,CAAC;AAO5K,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;CACrB;AAGD,eAAO,MAAM,WAAW,OAAQ,IAAI,KAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAwB,CAAC;AAclF,eAAO,MAAM,cAAc,OAAQ,MAAM,QAAQ,IAAI,KAAG,MAIvD,CAAC;AAGF,eAAO,MAAM,SAAS,MAAO,IAAI,KAAK,IAAI,OAAM,MAAM,KAAgB,OAAmE,CAAC;AAM1I,eAAO,MAAM,SAAS,SAAU,IAAI,UAAU,IAAI,OAAM,MAAM,KAAgB,MAAM,GAAG,SAAgF,CAAC"}
package/dist/time/zone.js CHANGED
@@ -30,6 +30,11 @@ const offsetMinutes = (at, zone) => {
30
30
  }
31
31
  return (match[1] === "-" ? -1 : 1) * (Number(match[2]) * 60 + Number(match[3]));
32
32
  };
33
+ export const nextDayStartIn = (at, zone) => {
34
+ const [year = 0, month = 1, day = 1] = civilDayIn(at, zone).split("-").map(Number);
35
+ const midnightUtc = Date.UTC(year, month - 1, day + 1);
36
+ return midnightUtc - offsetMinutes(midnightUtc, zone) * 60_000;
37
+ };
33
38
  export const sameClock = (a, b, at = Date.now()) => a === b || offsetMinutes(at, a) === offsetMinutes(at, b);
34
39
  export const zoneLabel = (rule, reader, at = Date.now()) => (sameClock(rule, reader, at) ? undefined : rule.replace(/_/g, " "));
35
40
  //# sourceMappingURL=zone.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"zone.js","sourceRoot":"","sources":["../../src/time/zone.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB,MAAM,CAAC,MAAM,GAAG,GAAG,KAAa,CAAC;AASjC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,KAAa,EAAiB,EAAE;IACnD,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,IAAI,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,eAAe,EAAE,CAAC,QAAQ,KAAK,EAAE,CAAC;IAC5F,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC,CAAC;AAGF,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,KAAyB,EAAoB,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAIlI,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,uDAAuD,EAAE,CAAC,CAAC;AAM1H,MAAM,CAAC,MAAM,SAAS,GAAG,GAAS,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAgB,CAAC;AAS9F,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,EAAE,+BAA+B,CAAC,CAAC;AAGvG,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EAAU,EAAE,IAAU,EAAY,EAAE,CAC3D,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAa,CAAC;AAGnI,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EAAU,EAAY,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAa,CAAC;AAStG,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAAU,EAAE,IAAU,EAAU,EAAE,CAC1D,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;KACrJ,MAAM,CAAC,EAAE,CAAC;KACV,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAG1B,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAyB,EAAwB,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAkB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAa5K,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAAQ,EAAwB,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;AAIlF,MAAM,aAAa,GAAG,CAAC,EAAU,EAAE,IAAU,EAAU,EAAE;IACrD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,cAAc,CAAC,EAAE,KAAK,CAAC;IAC3J,MAAM,KAAK,GAAG,0BAA0B,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC1D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACjB,OAAO,CAAC,CAAC;IACb,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC,CAAC;AAGF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAO,EAAE,CAAO,EAAE,EAAE,GAAW,IAAI,CAAC,GAAG,EAAE,EAAW,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAM1I,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAU,EAAE,MAAY,EAAE,EAAE,GAAW,IAAI,CAAC,GAAG,EAAE,EAAsB,EAAE,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"zone.js","sourceRoot":"","sources":["../../src/time/zone.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/sandbox-contract",
3
- "version": "1.310.0",
3
+ "version": "1.311.0",
4
4
  "description": "oRPC wire contract for the intentic sandbox daemon, shared by the daemon and its browser client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -150,6 +150,17 @@
150
150
  "default": "./dist/policy/owner-ticket.js"
151
151
  }
152
152
  },
153
+ "./front-wire": {
154
+ "types": "./dist/front/generated/wire.d.ts",
155
+ "import": {
156
+ "@intentic/src": "./src/front/generated/wire.ts",
157
+ "default": "./dist/front/generated/wire.js"
158
+ },
159
+ "require": {
160
+ "@intentic/src": "./src/front/generated/wire.ts",
161
+ "default": "./dist/front/generated/wire.js"
162
+ }
163
+ },
153
164
  "./ingress-protocol": {
154
165
  "types": "./dist/protocol/ingress-protocol.d.ts",
155
166
  "import": {
@@ -240,10 +251,10 @@
240
251
  }
241
252
  },
242
253
  "dependencies": {
243
- "@intentic/base": "1.310.0",
244
- "@intentic/constants": "1.310.0",
245
- "@intentic/extension-manifest": "1.310.0",
246
- "@intentic/registry": "1.310.0",
254
+ "@intentic/base": "1.311.0",
255
+ "@intentic/constants": "1.311.0",
256
+ "@intentic/extension-manifest": "1.311.0",
257
+ "@intentic/registry": "1.311.0",
247
258
  "@orpc/contract": "1.14.13",
248
259
  "tslib": "2.8.1",
249
260
  "zod": "4.5.4"
@@ -1,7 +1,6 @@
1
1
  import { procedure } from "../protocol/route-meta.js";
2
2
  import { AgentToolChildrenSchema, AgentTranscriptSchema } from "../events/transcript.js";
3
3
  import {
4
- AgentArchiveSchema,
5
4
  AgentAssignSchema,
6
5
  AgentAutoLandSchema,
7
6
  AgentBreakPolicySchema,
@@ -27,6 +26,7 @@ import { AgentsListSchema } from "../schemas/automations.js";
27
26
  import { AgentChangesSchema, AgentScratchSchema, AgentHistorySchema } from "../schemas/git/git.js";
28
27
  import { FileDiffSchema } from "../schemas/history.js";
29
28
  import { OkSchema } from "../schemas/shared.js";
29
+ import { AgentKeepWarmSchema } from "../schemas/keep-warm.js";
30
30
  import { ConversationPromptSchema } from "../schemas/system-prompt.js";
31
31
 
32
32
  // Every registered conversation-agent (AgentSummarySchema); an unknown {id} is NOT_FOUND across this whole family.
@@ -151,6 +151,18 @@ export const agentsContract = {
151
151
  .meta({ floor: "collaborator" })
152
152
  .input(AgentBreakPolicySchema)
153
153
  .output(AgentSummarySchema),
154
+ // Legal for a workspace conversation too, and only between turns in effect: a turn starting ends the hold.
155
+ keepWarm: procedure
156
+ .route({
157
+ method: "POST",
158
+ path: "/agents/{id}/keep-warm",
159
+ summary: "Keep this conversation's prompt cache warm while it sits idle",
160
+ description:
161
+ "Re-reads the conversation's cached context shortly before the provider would drop it, until the time asked for, so picking it back up costs a cache read instead of re-sending everything. Each refresh is a forked, unsaved request that adds nothing to the conversation. Stops by itself when the time runs out, when a turn starts, when the account nears its limit, or when the prompt the next turn would send has changed. Refused for a conversation whose cache is already cold or that this sandbox cannot replay. Null stops it.",
162
+ })
163
+ .meta({ floor: "collaborator" })
164
+ .input(AgentKeepWarmSchema)
165
+ .output(AgentSummarySchema),
154
166
  seen: procedure
155
167
  .route({
156
168
  method: "POST",
@@ -306,7 +318,7 @@ export const agentsContract = {
306
318
  "The gentle counterpart to discarding. Commits whatever the conversation still has in progress onto its own branch, releases its working copy, and keeps the entry and the record. Its scratch is not committed and goes with the copy. It leaves the live fleet and joins the archive. Refused for a conversation that is running.",
307
319
  })
308
320
  .meta({ floor: "collaborator", guest: true })
309
- .input(AgentArchiveSchema)
321
+ .input(AgentIdsSchema)
310
322
  .output(AgentsArchivedSchema),
311
323
  unarchive: procedure
312
324
  .route({
@@ -4,6 +4,7 @@ import { LandConflictSchema } from "../schemas/agents.js";
4
4
  import { ContextTrimSchema } from "../schemas/context-trim.js";
5
5
  import { RateLimitInfoSchema } from "../schemas/providers/claude-gate.js";
6
6
  import { FastModeStateSchema } from "../schemas/providers/fast-mode.js";
7
+ import { PromptCacheOpeningSchema, PromptFingerprintSchema } from "../schemas/keep-warm.js";
7
8
  import { AgentReplySchema, UsageWindowSchema } from "../schemas/providers/plan-limits.js";
8
9
  import { SubagentKindSchema, SubagentStatusSchema, SubagentVerificationSchema } from "../schemas/terminal.js";
9
10
  import { RetryLadderSchema } from "../schemas/turn-break.js";
@@ -50,7 +51,8 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
50
51
  // message to be drawn beside, so this is the only place the reader can learn the turn ran thin.
51
52
  ContextTrimSchema.extend({ kind: z.literal("context_trim") }),
52
53
  // The SDK's init handshake; carries the model it actually resolved for the turn.
53
- z.object({ kind: z.literal("init"), model: z.string() }),
54
+ // `prompt` names the parts the cache is keyed on, so a later turn can say which of them changed.
55
+ z.object({ kind: z.literal("init"), model: z.string(), prompt: PromptFingerprintSchema.optional() }),
54
56
  // The pre-turn snapshot id, emitted once before the provider stream so the client can offer
55
57
  // restore-to-before-this-message. Absent on an isolated turn or when the tree was already clean.
56
58
  // `index` is the message's position in the transcript, which the rewind route addresses by; absent on a turn with
@@ -139,7 +141,13 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
139
141
  cacheCreationTokens: z.number().optional(),
140
142
  durationMs: z.number().optional(),
141
143
  numTurns: z.number().optional(),
144
+ // The stream's first request only; a sum of frames keeps the first frame's, never adds them.
145
+ openingCacheReadTokens: z.number().optional(),
146
+ openingCacheCreationTokens: z.number().optional(),
147
+ promptFingerprint: z.string().optional(),
142
148
  }),
149
+ // What the turn's first request found in the prompt cache, sent as soon as it is known.
150
+ PromptCacheOpeningSchema.extend({ kind: z.literal("prompt_cache") }),
143
151
  // The provider's live answer to whether this turn may run, pushed mid-turn; drives the rate-limited notice, not the
144
152
  // headroom readouts.
145
153
  RateLimitInfoSchema.extend({ kind: z.literal("rate_limit_info"), account: z.string().optional() }),
@@ -311,8 +311,9 @@ export const holdsRequest = (row: TranscriptRequests): boolean => REQUEST_FIELDS
311
311
  // Whether a row is holding the turn open on a card nobody has answered.
312
312
  export const isAwaitingDecision = (row: TranscriptRequests): boolean => REQUEST_FIELDS.some((field) => row[field]?.status === "pending");
313
313
 
314
- // One change to a run's rows, so a client keeps rows rather than frames. Prose/thinking append to a row; a `tool` card
315
- // replaces whole by id; everything else replaces its row; `drop` removes a row that opened and never wrote.
314
+ // One change to a run's rows, so a client keeps rows rather than frames. Prose/thinking append to a row or a card; a
315
+ // `tool` card replaces its own fields by id, never its nested calls or thinking, which arrive by their own patches;
316
+ // everything else replaces its row; `drop` removes a row that opened and never wrote.
316
317
  export const TranscriptPatchSchema = z.discriminatedUnion("op", [
317
318
  z.object({ op: z.literal("append").describe("A new row at the end."), row: TranscriptRowSchema }),
318
319
  z.object({ op: z.literal("replace").describe("This row, whole, in place of the one at that index."), index: z.number().int().nonnegative(), row: TranscriptRowSchema }),
@@ -320,7 +321,17 @@ export const TranscriptPatchSchema = z.discriminatedUnion("op", [
320
321
  z.object({ op: z.literal("text").describe("More of the agent's prose, onto that row's text."), index: z.number().int().nonnegative(), text: z.string() }),
321
322
  z.object({ op: z.literal("thinking").describe("More of the agent's reasoning, onto that row's thinking."), index: z.number().int().nonnegative(), text: z.string() }),
322
323
  z.object({
323
- op: z.literal("tool").describe("A tool card, whole: new, or the latest state of one already there, matched by id wherever it nests."),
324
+ op: z.literal("toolThinking").describe("More of a delegated subagent's reasoning, onto the thinking of the card that started it."),
325
+ index: z.number().int().nonnegative(),
326
+ id: z.string().describe("The card's id, matched wherever it nests."),
327
+ text: z.string(),
328
+ }),
329
+ z.object({
330
+ op: z
331
+ .literal("tool")
332
+ .describe(
333
+ "A tool card's own fields: new, or the latest state of one already there, matched by id wherever it nests. Carries no `children` or `thinking`; a card already there keeps its own.",
334
+ ),
324
335
  index: z.number().int().nonnegative(),
325
336
  tool: TranscriptToolSchema,
326
337
  parent: z.string().optional().describe("The card this one nests under, when it is a delegated subagent's own call."),
@@ -0,0 +1,73 @@
1
+ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
2
+
3
+ export type Answer = { "answer": "preview", route: PreviewRoute, };
4
+
5
+ /**
6
+ * A PEM certificate chain and its PEM private key.
7
+ */
8
+ export type Certificate = { certificate: string, privateKey: string, };
9
+
10
+ export type Endpoint = { host: string, port: number, };
11
+
12
+ /**
13
+ * Everything Node sends the front.
14
+ */
15
+ export type FromNode = { "kind": "hello", build: string, pid: number, } | { "kind": "listen", config: ListenConfig, } | { "kind": "certificate", certificate?: Certificate, } | { "kind": "tunnel", tunnel?: TunnelConfig, } | { "kind": "answer", id: number, answer: Answer, } | { "kind": "refused", id: number, message: string, };
16
+
17
+ /**
18
+ * Request headers only the front sets on what it forwards to Node; it strips them from everything arriving outside.
19
+ * Exported as a literal type, so a Node constant naming one that drifts from this spelling fails to compile.
20
+ */
21
+ export type FrontHeader = "x-intentic-preview" | "x-intentic-preview-unreachable";
22
+
23
+ /**
24
+ * Every port the front binds, as Node's config names them; an absent one is not bound.
25
+ */
26
+ export type ListenConfig = { daemon: Endpoint, preview?: Endpoint, loopback?: Endpoint,
27
+ /**
28
+ * The 12-hex id every public hostname ends in; without one, a label's prefix alone classifies it.
29
+ */
30
+ sandboxId?: string,
31
+ /**
32
+ * Origins that may frame a preview besides the preview itself.
33
+ */
34
+ frameAncestors: Array<string>,
35
+ /**
36
+ * A preview path Node answers whatever its host resolves to.
37
+ */
38
+ previewProbePath: string, };
39
+
40
+ /**
41
+ * Where a preview host's requests go, as Node resolved it.
42
+ */
43
+ export type PreviewRoute = { "to": "upstream", upstream: Upstream, } | { "to": "node" };
44
+
45
+ /**
46
+ * A question the front asks Node; the answer carries the same id.
47
+ */
48
+ export type Question = { "question": "preview", host: string, };
49
+
50
+ export type Scheme = "http" | "https";
51
+
52
+ /**
53
+ * Everything the front sends Node.
54
+ */
55
+ export type ToNode = { "kind": "ask", id: number, question: Question, } | { "kind": "tunnel", connected: boolean, };
56
+
57
+ /**
58
+ * The ingress tunnel's door and the reachability grant it presents there.
59
+ */
60
+ export type TunnelConfig = { url: string, grant: string, };
61
+
62
+ /**
63
+ * A local upstream a preview host relays to.
64
+ */
65
+ export type Upstream = { host: string, port: number, scheme: Scheme,
66
+ /**
67
+ * Rewrites Host and Origin to `localhost:<port>`, for an app that never agreed to its preview name.
68
+ */
69
+ localhost: boolean,
70
+ /**
71
+ * Framed by the editor: `frame-ancestors` is set and `x-frame-options` dropped.
72
+ */
73
+ frameable: boolean, };
@@ -0,0 +1,20 @@
1
+ [
2
+ { "host": "preview-web-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345", "panel": "web" },
3
+ { "host": "preview-my-app-abcdef012345.sbx.example.test:443", "sandboxId": "abcdef012345", "panel": "my-app" },
4
+ { "host": "preview-shop--web-abcdef012345.example.com", "sandboxId": "abcdef012345", "panel": "shop--web" },
5
+ { "host": "preview-web-0123456789ab.sbx.example.test", "sandboxId": "abcdef012345" },
6
+ { "host": "preview--abcdef012345.sbx.example.test", "sandboxId": "abcdef012345" },
7
+ { "host": "preview-x-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345", "panel": "x" },
8
+ { "host": "port-1a2b3c4d5e6f-abcdef012345.sbx.example.test:8443", "sandboxId": "abcdef012345", "port": "1a2b3c4d5e6f" },
9
+ { "host": "public-9f8e7d6c5b4a-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345", "public": "9f8e7d6c5b4a" },
10
+ { "host": "sandbox-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345" },
11
+ { "host": "app.example.com", "sandboxId": "abcdef012345" },
12
+ { "host": "preview-web.localhost:8788", "panel": "web" },
13
+ { "host": "port-a.localhost", "port": "a" },
14
+ { "host": "public-z.example.com", "public": "z" },
15
+ { "host": "preview-.localhost" },
16
+ { "host": "127.0.0.1:8788" },
17
+ { "host": "[::1]:8788" },
18
+ { "host": "" },
19
+ { "host": "Preview-web-abcdef012345.sbx.example.test", "sandboxId": "abcdef012345" }
20
+ ]
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import {
2
3
  labelHostname,
3
4
  panelFromHost,
@@ -8,6 +9,7 @@ import {
8
9
  previewHostname,
9
10
  previewLabel,
10
11
  previewUrl,
12
+ publicSlotFromHost,
11
13
  sandboxHostname,
12
14
  sandboxIdFromUrl,
13
15
  syncFolder,
@@ -15,6 +17,24 @@ import {
15
17
 
16
18
  const ID = "abc123def456";
17
19
 
20
+ // The cases the front's Rust router is held to as well (_sandbox/front, route.rs), read from the one file both run.
21
+ interface HostCase {
22
+ readonly host: string;
23
+ readonly sandboxId?: string;
24
+ readonly panel?: string;
25
+ readonly port?: string;
26
+ readonly public?: string;
27
+ }
28
+ const HOST_CASES = JSON.parse(readFileSync(new URL("./hostnames.fixture.json", import.meta.url), "utf8")) as HostCase[];
29
+
30
+ test.each(HOST_CASES)("the shared host cases parse as the front parses them: $host", (hostCase) => {
31
+ expect({
32
+ panel: panelFromHost(hostCase.host, hostCase.sandboxId),
33
+ port: portSlotFromHost(hostCase.host, hostCase.sandboxId),
34
+ public: publicSlotFromHost(hostCase.host, hostCase.sandboxId),
35
+ }).toEqual({ panel: hostCase.panel, port: hostCase.port, public: hostCase.public });
36
+ });
37
+
18
38
  test("preview and port hostnames round-trip through their Host-header parsers", () => {
19
39
  expect(panelFromHost(`${previewHostname("shop--web", ID, "example.com")}:443`, ID)).toBe("shop--web");
20
40
  expect(portSlotFromHost(`${portHostname("a", ID, "example.com")}:443`, ID)).toBe("a");
package/src/index.ts CHANGED
@@ -216,6 +216,7 @@ export * from "./schemas/system-prompt.js";
216
216
  export * from "./schemas/system.js";
217
217
  export * from "./schemas/terminal.js";
218
218
  export * from "./schemas/turn-break.js";
219
+ export * from "./schemas/keep-warm.js";
219
220
  export * from "./schemas/providers/usage.js";
220
221
  export * from "./schemas/vpn.js";
221
222
  export * from "./schemas/webext.js";
@@ -0,0 +1,35 @@
1
+ import type { CommandRun } from "../schemas/ci.js";
2
+ import { followCommandRun } from "./command-run.js";
3
+
4
+ const RUNNING: CommandRun = { status: "running", command: "pnpm test", output: "" };
5
+ const PASSED: CommandRun = { status: "passed", command: "pnpm test", output: "ok" };
6
+
7
+ test("a poll that fails is reported and retried, and the run is still followed to its verdict", async () => {
8
+ const answers: Array<CommandRun | Error> = [new Error("daemon restarting"), RUNNING, PASSED];
9
+ const errors: unknown[] = [];
10
+ const settled = await followCommandRun(
11
+ async () => {
12
+ const next = answers.shift();
13
+ if (next instanceof Error) {
14
+ throw next;
15
+ }
16
+ return next as CommandRun;
17
+ },
18
+ { intervalMs: 1, onError: (cause) => errors.push(cause) },
19
+ );
20
+ expect(settled).toEqual(PASSED);
21
+ expect(errors).toEqual([new Error("daemon restarting")]);
22
+ });
23
+
24
+ test("a throw from onState is the caller's bug and reaches the caller, not a poll failure retried forever", async () => {
25
+ const errors: unknown[] = [];
26
+ const following = followCommandRun(async () => RUNNING, {
27
+ intervalMs: 1,
28
+ onState: () => {
29
+ throw new TypeError("state.session is undefined");
30
+ },
31
+ onError: (cause) => errors.push(cause),
32
+ });
33
+ await expect(following).rejects.toThrow("state.session is undefined");
34
+ expect(errors).toEqual([]);
35
+ });
@@ -10,8 +10,9 @@ export interface FollowRunOptions<R extends CommandRun> {
10
10
  readonly signal?: AbortSignal | undefined;
11
11
  // Every state read, settled or not, for a surface that draws progress.
12
12
  readonly onState?: ((run: R) => void) | undefined;
13
- // A poll that failed, not a run that failed: the run is still going, and following continues.
14
- readonly onError?: ((cause: unknown) => void) | undefined;
13
+ // A poll that failed, not a run that failed: the run is still going, and following continues. Required, since a
14
+ // loop that retries a failing read forever with nobody told looks exactly like a run that is still going.
15
+ readonly onError: (cause: unknown) => void;
15
16
  }
16
17
 
17
18
  // Follows a run to its verdict: reads state until it is no longer running. The one shared loop, so a dropped poll isn't
@@ -22,14 +23,18 @@ export const followCommandRun = async <R extends CommandRun>(read: () => Promise
22
23
  if (signal?.aborted === true) {
23
24
  return undefined;
24
25
  }
26
+ // Only the read is a poll that may fail and be retried; a throw from `onState` is the caller's own bug.
27
+ let run: R | undefined;
25
28
  try {
26
- const run = await read();
29
+ run = await read();
30
+ } catch (cause) {
31
+ onError(cause);
32
+ }
33
+ if (run !== undefined) {
27
34
  onState?.(run);
28
35
  if (run.status !== "running") {
29
36
  return run;
30
37
  }
31
- } catch (cause) {
32
- onError?.(cause);
33
38
  }
34
39
  await sleep(intervalMs, { signal });
35
40
  }
@@ -148,9 +148,25 @@ const target = createServer((request: IncomingMessage, response: ServerResponse)
148
148
 
149
149
  const cancelled = gate<string>();
150
150
 
151
+ // `/late` holds its handshake until released, so the session can die while the loopback upgrade is still pending.
152
+ const lateArrived = gate();
153
+ const lateReleased = gate();
154
+ let lateTornDown = false;
155
+
151
156
  // Hand-written HTTP/1.1 upgrade carrying the 101 head plus the raw bytes after it. `/refuse` answers instead of
152
157
  // upgrading.
153
158
  target.on("upgrade", (request: IncomingMessage, socket: Duplex, head: Buffer) => {
159
+ if ((request.url ?? "") === "/late") {
160
+ socket.on("error", () => {});
161
+ socket.on("close", () => (lateTornDown = true));
162
+ lateArrived.open();
163
+ void lateReleased.opened.then(() => {
164
+ if (!socket.destroyed) {
165
+ socket.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n");
166
+ }
167
+ });
168
+ return;
169
+ }
154
170
  if ((request.url ?? "") === "/refuse") {
155
171
  socket.end("HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 7\r\n\r\nno dice");
156
172
  return;
@@ -179,7 +195,7 @@ target.on("upgrade", (request: IncomingMessage, socket: Duplex, head: Buffer) =>
179
195
  // One session; every request is routed through it.
180
196
  const front = async (
181
197
  targetPort: number,
182
- ): Promise<{ readonly server: Server; readonly poison: (bytes: Buffer) => void; readonly close: () => void }> => {
198
+ ): Promise<{ readonly server: Server; readonly poison: (bytes: Buffer) => void; readonly sever: () => void; readonly close: () => void }> => {
183
199
  const [edgeSide, daemonSide] = duplexPair();
184
200
  const daemon = await serveIngressSession(daemonSide, { targetPort });
185
201
  const session = await openIngressSession(edgeSide);
@@ -202,6 +218,8 @@ const front = async (
202
218
  server,
203
219
  // Writes garbage onto the wire the edge reads, simulating a wedged or corrupted peer.
204
220
  poison: (bytes: Buffer) => void daemonSide.write(bytes),
221
+ // Cuts the tunnel under the daemon, which takes its h2 session and every stream on it down.
222
+ sever: () => void daemonSide.destroy(),
205
223
  close: () => {
206
224
  session.close();
207
225
  daemon.close();
@@ -439,6 +457,32 @@ test("a browser that gives up cancels the stream all the way to the target", asy
439
457
  await expect(cancelled.opened).resolves.toBe("aborted");
440
458
  });
441
459
 
460
+ // The handshake lands after its stream is gone: answering it throws ERR_HTTP2_INVALID_STREAM from an event callback.
461
+ test("a session that dies mid-handshake tears the loopback upgrade down instead of answering into a dead stream", async () => {
462
+ const own = await front((target.address() as AddressInfo).port);
463
+ const ownPort = await listen(own.server);
464
+ const request = h1Request({
465
+ host: "127.0.0.1",
466
+ port: ownPort,
467
+ path: "/late",
468
+ headers: { host: HOST, connection: "Upgrade", upgrade: "websocket", "sec-websocket-key": "x" },
469
+ agent: false,
470
+ });
471
+ request.on("error", () => {});
472
+ request.end();
473
+ await lateArrived.opened;
474
+
475
+ own.sever();
476
+ await new Promise((resolve) => setTimeout(resolve, 250));
477
+ lateReleased.open();
478
+
479
+ await waitFor(() => expect(lateTornDown).toBe(true));
480
+ await new Promise((resolve) => setTimeout(resolve, 50));
481
+ expect(uncaught).toStrictEqual([]);
482
+ request.destroy();
483
+ own.close();
484
+ });
485
+
442
486
  test("a tunnel whose target is not listening fails the exchange rather than answering for it", async () => {
443
487
  const dead = createServer();
444
488
  const deadPort = await listen(dead);
@@ -10,9 +10,9 @@ import {
10
10
  import { type AddressInfo, createServer as createNetServer, connect as netConnect, type Socket } from "node:net";
11
11
  import { Duplex } from "node:stream";
12
12
 
13
- // Both ends of the ingress tunnel's h2 session, over any node Duplex, in one file: a wire format with two owners
14
- // drifts. h2 gives multiplexing and backpressure for free; routing is per h2 stream, since one edge connection can
15
- // interleave several sandboxes' hosts.
13
+ // Both halves of the ingress tunnel's h2 session over any node Duplex, in one file so the wire has one owner: the edge's,
14
+ // and a daemon half its suites dial (a sandbox runs the Rust one, _sandbox/front tunnel.rs, held to this by its test).
15
+ // Routing is per h2 stream, since one edge connection can interleave several sandboxes' hosts.
16
16
 
17
17
  // Header hygiene
18
18
 
@@ -308,7 +308,7 @@ export const openIngressSession = async (duplex: Duplex): Promise<IngressSession
308
308
  };
309
309
  };
310
310
 
311
- // The daemon half: an h2 server over the tunnel, onto the loopback listener
311
+ // The daemon half the edge's suites dial: an h2 server over the tunnel, onto a local listener
312
312
 
313
313
  export interface ServeIngressSessionOptions {
314
314
  // The daemon's own listener; every stream lands there as a plain HTTP/1.1 request or upgrade.
@@ -340,6 +340,10 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
340
340
  server.on("error", () => duplex.destroy());
341
341
  server.on("session", (session) => session.setLocalWindowSize(CONNECTION_WINDOW_SIZE));
342
342
 
343
+ // The loopback's answer can outlive its stream when the session dies, and respond/write on a dead stream throws
344
+ // ERR_HTTP2_INVALID_STREAM from an event callback, where nothing catches it.
345
+ const gone = (stream: ServerHttp2Stream): boolean => stream.destroyed || stream.closed;
346
+
343
347
  const forwardToLoopback = (stream: ServerHttp2Stream, headers: IncomingHttpHeaders): void => {
344
348
  const local = h1Request({
345
349
  host: LOOPBACK,
@@ -353,6 +357,10 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
353
357
  });
354
358
  stream.pipe(local);
355
359
  local.on("response", (response) => {
360
+ if (gone(stream)) {
361
+ response.destroy();
362
+ return;
363
+ }
356
364
  stream.respond({ [constants.HTTP2_HEADER_STATUS]: response.statusCode ?? 502, ...endToEnd(response.headers) });
357
365
  response.pipe(stream);
358
366
  });
@@ -364,6 +372,9 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
364
372
  // means this — on a reset, node fires `aborted` before `finish` and `close`, so a
365
373
  // `writableEnded`/`writableFinished` guard there would miss it.
366
374
  stream.on("aborted", () => local.destroy());
375
+ // `aborted` fires only while the stream's writable side is open; `close` covers every other way it dies, and a
376
+ // request whose answer already completed ignores the destroy.
377
+ stream.on("close", () => local.destroy());
367
378
  };
368
379
 
369
380
  const spliceUpgrade = (stream: ServerHttp2Stream, headers: IncomingHttpHeaders): void => {
@@ -372,6 +383,10 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
372
383
  // pool the request path above uses.
373
384
  const local = h1Request({ host: LOOPBACK, port: options.targetPort, method: head.method, path: head.path, headers: head.headers, agent: false });
374
385
  local.on("upgrade", (response, socket, first) => {
386
+ if (gone(stream)) {
387
+ socket.destroy();
388
+ return;
389
+ }
375
390
  stream.respond({ [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_OK });
376
391
  // Response head written verbatim (see serializeHead), then any bytes already sent past it, then the pipe.
377
392
  stream.write(serializeHead(response, NOTHING_DROPPED, []));
@@ -383,11 +398,17 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
383
398
  // The local server declined to upgrade; its answer goes to the browser with framing that matches the already
384
399
  // de-chunked body.
385
400
  local.on("response", (response) => {
401
+ if (gone(stream)) {
402
+ response.destroy();
403
+ return;
404
+ }
386
405
  stream.respond({ [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_OK });
387
406
  stream.write(serializeHead(response, DECLINED_UPGRADE_DROP, DECLINED_UPGRADE_ADD));
388
407
  response.pipe(stream);
389
408
  });
390
409
  local.on("error", () => stream.close(constants.NGHTTP2_CONNECT_ERROR));
410
+ // Before the handshake answers, nothing else ties the loopback request to the stream; once spliced, a no-op.
411
+ stream.on("close", () => local.destroy());
391
412
  // An upgrade request carries no body; the client is waiting on the handshake before it says anything else.
392
413
  local.end();
393
414
  };
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { AgentHarnessSchema, AgentOriginSchema, AgentProviderSchema, ConversationQueueSchema, ForkedFromSchema } from "./agent.js";
4
4
  import { LoopStateSchema } from "./loops.js";
5
5
  import { LimitPolicySchema, RetryPolicySchema, TurnBreakPolicySchema, TurnBreakSchema } from "./turn-break.js";
6
+ import { KeepWarmSchema } from "./keep-warm.js";
6
7
  import { EMOJI_MAX_LENGTH, isSingleEmoji } from "../text/emoji.js";
7
8
  // A fleet agent is any conversation with a registry entry, keyed by conversationId. Isolated ones own a git worktree
8
9
  // (branch agent/<id>); workspace conversations have none, but both share one status/activity/cost lifecycle.
@@ -329,11 +330,24 @@ export const AgentSummarySchema = z.object({
329
330
  .object({
330
331
  at: z.number().describe("When its last request touched the provider's prompt cache, in milliseconds."),
331
332
  ttlMs: z.number().describe("How long that entry lives from `at`, in milliseconds."),
333
+ rollsAt: z
334
+ .number()
335
+ .optional()
336
+ .describe(
337
+ "When the date written into the agent's prompt next changes, in milliseconds (midnight where the agent runs). Past it the next turn sends a different prompt, so nothing kept before it is read again.",
338
+ ),
339
+ keepable: z
340
+ .boolean()
341
+ .optional()
342
+ .describe("Whether this sandbox can keep this cache warm: it holds the last turn's exact request to replay, on a Claude subscription account."),
332
343
  })
333
344
  .optional()
334
345
  .describe(
335
346
  "When this conversation's prompt cache was last kept alive and how long it lasts, which together say when picking the conversation up stops being cheap. Absent when the provider publishes nothing to ground it on.",
336
347
  ),
348
+ keepWarm: KeepWarmSchema.optional().describe(
349
+ "Whether the sandbox is keeping this conversation's prompt cache warm while it sits idle, how that is going, or why it stopped. Absent when nobody asked for it.",
350
+ ),
337
351
  activity: AgentActivitySchema.optional().describe("What it is doing at this moment."),
338
352
  // Beside `activity` because both answer "where is it", one in a sentence and one as a fraction. Live while a turn
339
353
  // runs, and left standing after it, so a settled card still says how far the work got.
@@ -517,13 +531,6 @@ export const AgentToolChildrenQuerySchema = AgentIdSchema.extend({
517
531
  toolId: z.string().min(1).describe("Which tool call, by the id its card carries."),
518
532
  });
519
533
  // Absent `ids` archives every archivable finished agent (the lane's "Clear"); unarchive always names its own ids.
520
- export const AgentArchiveSchema = z.object({
521
- ids: z
522
- .array(z.string().min(1))
523
- .max(500)
524
- .optional()
525
- .describe("Which conversations to put away. Leave it out for every finished one that can be archived right now."),
526
- });
527
534
  export const AgentIdsSchema = z.object({ ids: z.array(z.string().min(1)).min(1).max(500).describe("Which conversations.") });
528
535
  // What actually moved, not the roster afterward: two archives finishing at once would otherwise let a slower snapshot
529
536
  // resurrect what a faster one just filed away. Whole summaries, since the receiving surfaces (archive list, detail
@@ -354,6 +354,12 @@ export const CapabilityStatusSchema = z.object({
354
354
  state: CapabilityStateSchema.describe("Whether it is live, still coming up, broken, or switched off."),
355
355
  detail: z.string().optional().describe("What is wrong, in words a person can act on."),
356
356
  code: z.string().optional().describe("A short marker for that reason, for anything deciding what to do about it."),
357
+ settling: z
358
+ .boolean()
359
+ .optional()
360
+ .describe(
361
+ "True while something under way will move this on its own: a start, a download, a pairing code waiting to be typed. Absent when only a person can move it, or a pushed change will say when it moved.",
362
+ ),
357
363
  });
358
364
  export type CapabilityStatus = z.infer<typeof CapabilityStatusSchema>;
359
365
  // The list row: manifest entry plus live status, secrets never returned. `secrets` names which config keys hold a