@agentex/agent 0.0.27 → 0.0.29

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 (199) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +98 -8
  3. package/dist/derived.d.ts.map +1 -1
  4. package/dist/derived.js +99 -2
  5. package/dist/derived.js.map +1 -1
  6. package/dist/goals/controller.d.ts.map +1 -1
  7. package/dist/goals/controller.js +21 -0
  8. package/dist/goals/controller.js.map +1 -1
  9. package/dist/goals/normalize.d.ts +1 -0
  10. package/dist/goals/normalize.d.ts.map +1 -1
  11. package/dist/goals/normalize.js +5 -0
  12. package/dist/goals/normalize.js.map +1 -1
  13. package/dist/history/fs.d.ts +36 -0
  14. package/dist/history/fs.d.ts.map +1 -0
  15. package/dist/history/fs.js +266 -0
  16. package/dist/history/fs.js.map +1 -0
  17. package/dist/history/index.d.ts +3 -0
  18. package/dist/history/index.d.ts.map +1 -0
  19. package/dist/history/index.js +2 -0
  20. package/dist/history/index.js.map +1 -0
  21. package/dist/history/types.d.ts +75 -0
  22. package/dist/history/types.d.ts.map +1 -0
  23. package/dist/history/types.js +9 -0
  24. package/dist/history/types.js.map +1 -0
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/providers/claude/attach.d.ts.map +1 -1
  29. package/dist/providers/claude/attach.js +50 -9
  30. package/dist/providers/claude/attach.js.map +1 -1
  31. package/dist/providers/claude/history.d.ts +3 -0
  32. package/dist/providers/claude/history.d.ts.map +1 -0
  33. package/dist/providers/claude/history.js +248 -0
  34. package/dist/providers/claude/history.js.map +1 -0
  35. package/dist/providers/claude/index.d.ts.map +1 -1
  36. package/dist/providers/claude/index.js +26 -0
  37. package/dist/providers/claude/index.js.map +1 -1
  38. package/dist/providers/claude/session.d.ts +2 -0
  39. package/dist/providers/claude/session.d.ts.map +1 -1
  40. package/dist/providers/claude/session.js +17 -10
  41. package/dist/providers/claude/session.js.map +1 -1
  42. package/dist/providers/codex/attach.d.ts +2 -2
  43. package/dist/providers/codex/attach.d.ts.map +1 -1
  44. package/dist/providers/codex/attach.js +44 -7
  45. package/dist/providers/codex/attach.js.map +1 -1
  46. package/dist/providers/codex/history.d.ts +3 -0
  47. package/dist/providers/codex/history.d.ts.map +1 -0
  48. package/dist/providers/codex/history.js +350 -0
  49. package/dist/providers/codex/history.js.map +1 -0
  50. package/dist/providers/codex/index.d.ts.map +1 -1
  51. package/dist/providers/codex/index.js +26 -0
  52. package/dist/providers/codex/index.js.map +1 -1
  53. package/dist/providers/codex/transcript-normalize.d.ts +3 -2
  54. package/dist/providers/codex/transcript-normalize.d.ts.map +1 -1
  55. package/dist/providers/codex/transcript-normalize.js +4 -3
  56. package/dist/providers/codex/transcript-normalize.js.map +1 -1
  57. package/dist/providers/codex/transcript.d.ts +3 -1
  58. package/dist/providers/codex/transcript.d.ts.map +1 -1
  59. package/dist/providers/codex/transcript.js +3 -1
  60. package/dist/providers/codex/transcript.js.map +1 -1
  61. package/dist/providers/cursor/discovery.d.ts +6 -0
  62. package/dist/providers/cursor/discovery.d.ts.map +1 -0
  63. package/dist/providers/cursor/discovery.js +118 -0
  64. package/dist/providers/cursor/discovery.js.map +1 -0
  65. package/dist/providers/cursor/execute.d.ts.map +1 -1
  66. package/dist/providers/cursor/execute.js +83 -45
  67. package/dist/providers/cursor/execute.js.map +1 -1
  68. package/dist/providers/cursor/index.d.ts.map +1 -1
  69. package/dist/providers/cursor/index.js +17 -4
  70. package/dist/providers/cursor/index.js.map +1 -1
  71. package/dist/providers/cursor/parse.js +1 -1
  72. package/dist/providers/cursor/parse.js.map +1 -1
  73. package/dist/providers/cursor/probe.d.ts +3 -0
  74. package/dist/providers/cursor/probe.d.ts.map +1 -0
  75. package/dist/providers/cursor/probe.js +89 -0
  76. package/dist/providers/cursor/probe.js.map +1 -0
  77. package/dist/providers/cursor/runtime.d.ts +4 -0
  78. package/dist/providers/cursor/runtime.d.ts.map +1 -0
  79. package/dist/providers/cursor/runtime.js +12 -0
  80. package/dist/providers/cursor/runtime.js.map +1 -0
  81. package/dist/providers/cursor/session.d.ts +3 -0
  82. package/dist/providers/cursor/session.d.ts.map +1 -0
  83. package/dist/providers/cursor/session.js +14 -0
  84. package/dist/providers/cursor/session.js.map +1 -0
  85. package/dist/providers/opencode/client.d.ts +14 -0
  86. package/dist/providers/opencode/client.d.ts.map +1 -0
  87. package/dist/providers/opencode/client.js +40 -0
  88. package/dist/providers/opencode/client.js.map +1 -0
  89. package/dist/providers/opencode/discovery.d.ts +4 -0
  90. package/dist/providers/opencode/discovery.d.ts.map +1 -0
  91. package/dist/providers/opencode/discovery.js +68 -0
  92. package/dist/providers/opencode/discovery.js.map +1 -0
  93. package/dist/providers/opencode/execute.d.ts.map +1 -1
  94. package/dist/providers/opencode/execute.js +219 -164
  95. package/dist/providers/opencode/execute.js.map +1 -1
  96. package/dist/providers/opencode/history.d.ts +24 -0
  97. package/dist/providers/opencode/history.d.ts.map +1 -0
  98. package/dist/providers/opencode/history.js +242 -0
  99. package/dist/providers/opencode/history.js.map +1 -0
  100. package/dist/providers/opencode/http-session.d.ts.map +1 -1
  101. package/dist/providers/opencode/http-session.js +333 -49
  102. package/dist/providers/opencode/http-session.js.map +1 -1
  103. package/dist/providers/opencode/index.d.ts.map +1 -1
  104. package/dist/providers/opencode/index.js +26 -3
  105. package/dist/providers/opencode/index.js.map +1 -1
  106. package/dist/providers/opencode/manager.d.ts +11 -0
  107. package/dist/providers/opencode/manager.d.ts.map +1 -0
  108. package/dist/providers/opencode/manager.js +240 -0
  109. package/dist/providers/opencode/manager.js.map +1 -0
  110. package/dist/providers/opencode/modes.d.ts +3 -0
  111. package/dist/providers/opencode/modes.d.ts.map +1 -0
  112. package/dist/providers/opencode/modes.js +30 -0
  113. package/dist/providers/opencode/modes.js.map +1 -0
  114. package/dist/providers/opencode/parse.d.ts.map +1 -1
  115. package/dist/providers/opencode/parse.js +2 -22
  116. package/dist/providers/opencode/parse.js.map +1 -1
  117. package/dist/providers/opencode/probe.d.ts +3 -0
  118. package/dist/providers/opencode/probe.d.ts.map +1 -0
  119. package/dist/providers/opencode/probe.js +107 -0
  120. package/dist/providers/opencode/probe.js.map +1 -0
  121. package/dist/providers/opencode/runtime.d.ts +8 -0
  122. package/dist/providers/opencode/runtime.d.ts.map +1 -0
  123. package/dist/providers/opencode/runtime.js +12 -0
  124. package/dist/providers/opencode/runtime.js.map +1 -0
  125. package/dist/providers/opencode/server.d.ts +18 -3
  126. package/dist/providers/opencode/server.d.ts.map +1 -1
  127. package/dist/providers/opencode/server.js +0 -0
  128. package/dist/providers/opencode/server.js.map +1 -1
  129. package/dist/providers/opencode/skill-config.d.ts +11 -0
  130. package/dist/providers/opencode/skill-config.d.ts.map +1 -0
  131. package/dist/providers/opencode/skill-config.js +57 -0
  132. package/dist/providers/opencode/skill-config.js.map +1 -0
  133. package/dist/sessions/exec-backed.d.ts +10 -0
  134. package/dist/sessions/exec-backed.d.ts.map +1 -0
  135. package/dist/sessions/exec-backed.js +141 -0
  136. package/dist/sessions/exec-backed.js.map +1 -0
  137. package/dist/sessions/history.d.ts +4 -0
  138. package/dist/sessions/history.d.ts.map +1 -0
  139. package/dist/sessions/history.js +59 -0
  140. package/dist/sessions/history.js.map +1 -0
  141. package/dist/sessions/index.d.ts +3 -0
  142. package/dist/sessions/index.d.ts.map +1 -1
  143. package/dist/sessions/index.js +2 -0
  144. package/dist/sessions/index.js.map +1 -1
  145. package/dist/types.d.ts +152 -6
  146. package/dist/types.d.ts.map +1 -1
  147. package/dist/types.js.map +1 -1
  148. package/dist/utils/auth.d.ts.map +1 -1
  149. package/dist/utils/auth.js +36 -6
  150. package/dist/utils/auth.js.map +1 -1
  151. package/dist/utils/binary.d.ts.map +1 -1
  152. package/dist/utils/binary.js +3 -0
  153. package/dist/utils/binary.js.map +1 -1
  154. package/dist/utils/endpoint.js +3 -3
  155. package/dist/utils/endpoint.js.map +1 -1
  156. package/package.json +10 -2
  157. package/src/derived.ts +126 -3
  158. package/src/goals/controller.ts +17 -0
  159. package/src/goals/normalize.ts +5 -0
  160. package/src/history/fs.ts +307 -0
  161. package/src/history/index.ts +18 -0
  162. package/src/history/types.ts +104 -0
  163. package/src/index.ts +31 -0
  164. package/src/providers/claude/attach.ts +48 -6
  165. package/src/providers/claude/history.ts +290 -0
  166. package/src/providers/claude/index.ts +26 -0
  167. package/src/providers/claude/session.ts +15 -10
  168. package/src/providers/codex/attach.ts +52 -7
  169. package/src/providers/codex/history.ts +399 -0
  170. package/src/providers/codex/index.ts +26 -0
  171. package/src/providers/codex/transcript-normalize.ts +4 -3
  172. package/src/providers/codex/transcript.ts +5 -1
  173. package/src/providers/cursor/discovery.ts +114 -0
  174. package/src/providers/cursor/execute.ts +67 -30
  175. package/src/providers/cursor/index.ts +19 -5
  176. package/src/providers/cursor/parse.ts +1 -1
  177. package/src/providers/cursor/probe.ts +90 -0
  178. package/src/providers/cursor/runtime.ts +11 -0
  179. package/src/providers/cursor/session.ts +15 -0
  180. package/src/providers/opencode/client.ts +44 -0
  181. package/src/providers/opencode/discovery.ts +69 -0
  182. package/src/providers/opencode/execute.ts +64 -12
  183. package/src/providers/opencode/history.ts +270 -0
  184. package/src/providers/opencode/http-session.ts +319 -50
  185. package/src/providers/opencode/index.ts +26 -3
  186. package/src/providers/opencode/manager.ts +269 -0
  187. package/src/providers/opencode/modes.ts +31 -0
  188. package/src/providers/opencode/parse.ts +2 -22
  189. package/src/providers/opencode/probe.ts +113 -0
  190. package/src/providers/opencode/runtime.ts +13 -0
  191. package/src/providers/opencode/server.ts +0 -0
  192. package/src/providers/opencode/skill-config.ts +64 -0
  193. package/src/sessions/exec-backed.ts +169 -0
  194. package/src/sessions/history.ts +76 -0
  195. package/src/sessions/index.ts +3 -0
  196. package/src/types.ts +163 -4
  197. package/src/utils/auth.ts +35 -6
  198. package/src/utils/binary.ts +3 -0
  199. package/src/utils/endpoint.ts +3 -3
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentex/agent",
3
- "version": "0.0.27",
4
- "description": "Programmatic execution of AI coding agents (Claude Code, Codex, OpenClaw)",
3
+ "version": "0.0.29",
4
+ "description": "Programmatic execution of AI coding agents including Claude Code, Codex, Cursor, and OpenCode",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/dynamismlabs/agentex.git",
@@ -38,6 +38,11 @@
38
38
  "import": "./dist/goals/index.js",
39
39
  "default": "./dist/goals/index.js"
40
40
  },
41
+ "./history": {
42
+ "types": "./dist/history/index.d.ts",
43
+ "import": "./dist/history/index.js",
44
+ "default": "./dist/history/index.js"
45
+ },
41
46
  "./sessions": {
42
47
  "types": "./dist/sessions/index.d.ts",
43
48
  "import": "./dist/sessions/index.js",
@@ -84,6 +89,8 @@
84
89
  "agent",
85
90
  "claude",
86
91
  "codex",
92
+ "cursor",
93
+ "opencode",
87
94
  "openclaw",
88
95
  "provider",
89
96
  "execution"
@@ -114,6 +121,7 @@
114
121
  "smoke:mock": "tsx scripts/smoke.ts --mock",
115
122
  "smoke:concurrent": "tsx scripts/concurrent-smoke.ts",
116
123
  "demo:durable": "tsx scripts/durable-session-demo.ts",
124
+ "diagnose:history": "tsx scripts/local-history-diagnostic.ts",
117
125
  "list-models": "tsx scripts/list-models.ts"
118
126
  }
119
127
  }
package/src/derived.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  import type {
2
+ AgentSession,
3
+ AttachOptions,
4
+ HistoryAttachment,
2
5
  ProviderCapabilities,
3
6
  ProviderConfig,
4
7
  ProviderModel,
5
8
  ProviderModule,
9
+ ProviderRuntimeContext,
10
+ SessionAttachment,
11
+ SessionRecord,
6
12
  } from "./types.js";
7
13
  import { getProvider, registerProvider } from "./registry.js";
8
14
  import { acpProvider } from "./providers/acp/index.js";
15
+ import { MalformedSessionRecordError } from "./sessions/record.js";
9
16
 
10
17
  /**
11
18
  * Declarative configuration for a *derived* provider — a new provider id that
@@ -76,6 +83,38 @@ export function getAcpFactory(): AcpFactory | null {
76
83
  return acpFactory;
77
84
  }
78
85
 
86
+ function withProviderType(record: SessionRecord | null, providerType: string): SessionRecord | null {
87
+ return record ? { ...record, providerType } : null;
88
+ }
89
+
90
+ function wrapDerivedSession(session: AgentSession, providerType: string): AgentSession {
91
+ return new Proxy(session, {
92
+ get(target, property) {
93
+ if (property === "describe") {
94
+ return target.describe
95
+ ? () => withProviderType(target.describe!(), providerType)
96
+ : undefined;
97
+ }
98
+ if (property === "describeHistory") {
99
+ return target.describeHistory
100
+ ? () => withProviderType(target.describeHistory!(), providerType)
101
+ : undefined;
102
+ }
103
+ const value = Reflect.get(target, property, target) as unknown;
104
+ return typeof value === "function" ? value.bind(target) : value;
105
+ },
106
+ });
107
+ }
108
+
109
+ function assertDerivedRecord(record: SessionRecord, providerType: string): void {
110
+ if (record.providerType !== providerType) {
111
+ throw new MalformedSessionRecordError(
112
+ `${providerType} attach requires providerType ${JSON.stringify(providerType)}; got ${JSON.stringify(record.providerType)}`,
113
+ "providerType",
114
+ );
115
+ }
116
+ }
117
+
79
118
  /**
80
119
  * Build a derived `ProviderModule` from a base provider plus an overlay. The
81
120
  * returned module is NOT auto-registered — pass it to `registerProvider`, or
@@ -124,15 +163,21 @@ export function defineDerivedProvider(cfg: DerivedProviderConfig): ProviderModul
124
163
 
125
164
  if (base.createSession) {
126
165
  const baseCreate = base.createSession.bind(base);
127
- derived.createSession = (ctx) =>
128
- baseCreate({ ...ctx, env: overlayEnv(ctx.env), config: overlayConfig(ctx.config) });
166
+ derived.createSession = async (ctx) => wrapDerivedSession(
167
+ await baseCreate({ ...ctx, env: overlayEnv(ctx.env), config: overlayConfig(ctx.config) }),
168
+ cfg.id,
169
+ );
129
170
  }
130
171
  if (cfg.models) {
131
172
  const models = cfg.models;
132
173
  derived.listModels = async () => models;
133
174
  } else if (base.listModels) {
134
175
  const baseList = base.listModels.bind(base);
135
- derived.listModels = (opts) => baseList(opts);
176
+ derived.listModels = (opts) => baseList({
177
+ ...opts,
178
+ env: overlayEnv(opts?.env),
179
+ config: overlayConfig(opts?.config),
180
+ });
136
181
  }
137
182
  if (base.listModes) {
138
183
  const baseModes = base.listModes.bind(base);
@@ -144,10 +189,88 @@ export function defineDerivedProvider(cfg: DerivedProviderConfig): ProviderModul
144
189
  derived.checkQuota = (qctx) =>
145
190
  baseQuota({ ...qctx, env: overlayEnv(qctx?.env), config: overlayConfig(qctx?.config) });
146
191
  }
192
+ if (base.probeCapabilities) {
193
+ const baseProbe = base.probeCapabilities.bind(base);
194
+ derived.probeCapabilities = (ctx) => baseProbe({
195
+ ...ctx,
196
+ env: overlayEnv(ctx?.env),
197
+ config: overlayConfig(ctx?.config),
198
+ });
199
+ }
200
+ if (base.upstreamProviders) {
201
+ const manager = base.upstreamProviders;
202
+ const runtime = (ctx?: ProviderRuntimeContext): ProviderRuntimeContext => ({
203
+ ...ctx,
204
+ env: overlayEnv(ctx?.env),
205
+ config: overlayConfig(ctx?.config),
206
+ });
207
+ derived.upstreamProviders = {
208
+ list: (ctx) => manager.list(runtime(ctx)),
209
+ authMethods: (providerId, ctx) => manager.authMethods(providerId, runtime(ctx)),
210
+ setApiKey: (providerId, key, ctx) => manager.setApiKey(providerId, key, runtime(ctx)),
211
+ beginOAuth: (providerId, methodId, inputs, ctx) =>
212
+ manager.beginOAuth(providerId, methodId, inputs, runtime(ctx)),
213
+ completeOAuth: (flowId, code, ctx) => manager.completeOAuth(flowId, code, runtime(ctx)),
214
+ canDisconnect: (providerId, ctx) => manager.canDisconnect(providerId, runtime(ctx)),
215
+ disconnect: (providerId, ctx) => manager.disconnect(providerId, runtime(ctx)),
216
+ };
217
+ }
218
+ if (base.attachSession && derived.createSession) {
219
+ const baseAttach = base.attachSession.bind(base);
220
+ derived.attachSession = async (record, opts): Promise<SessionAttachment> => {
221
+ assertDerivedRecord(record, cfg.id);
222
+ const attachment = await baseAttach(
223
+ { ...record, providerType: base.type },
224
+ overlayAttachOptions(opts, overlayEnv, overlayConfig),
225
+ );
226
+ const derivedRecord = withProviderType(attachment.record, cfg.id)!;
227
+ return {
228
+ ...attachment,
229
+ record: derivedRecord,
230
+ resume: (ctx) => derived.createSession!({
231
+ ...ctx,
232
+ cwd: ctx?.cwd ?? derivedRecord.cwd ?? undefined,
233
+ sessionParams: derivedRecord.params,
234
+ }),
235
+ };
236
+ };
237
+ }
238
+ if (base.attachHistory && derived.createSession) {
239
+ const baseAttachHistory = base.attachHistory.bind(base);
240
+ derived.attachHistory = async (record, opts): Promise<HistoryAttachment> => {
241
+ assertDerivedRecord(record, cfg.id);
242
+ const attachment = await baseAttachHistory(
243
+ { ...record, providerType: base.type },
244
+ overlayAttachOptions(opts, overlayEnv, overlayConfig),
245
+ );
246
+ const derivedRecord = withProviderType(attachment.record, cfg.id)!;
247
+ return {
248
+ ...attachment,
249
+ record: derivedRecord,
250
+ resume: (ctx) => derived.createSession!({
251
+ ...ctx,
252
+ cwd: ctx?.cwd ?? derivedRecord.cwd ?? undefined,
253
+ sessionParams: derivedRecord.params,
254
+ }),
255
+ };
256
+ };
257
+ }
147
258
 
148
259
  return derived;
149
260
  }
150
261
 
262
+ function overlayAttachOptions(
263
+ opts: AttachOptions | undefined,
264
+ overlayEnv: (env?: Record<string, string>) => Record<string, string>,
265
+ overlayConfig: (config?: ProviderConfig) => ProviderConfig,
266
+ ): AttachOptions {
267
+ return {
268
+ ...opts,
269
+ env: overlayEnv(opts?.env),
270
+ config: overlayConfig(opts?.config),
271
+ };
272
+ }
273
+
151
274
  // ---------------------------------------------------------------------------
152
275
  // Config-file loader
153
276
  // ---------------------------------------------------------------------------
@@ -139,6 +139,9 @@ export class GoalController {
139
139
  * sentinel/loop), call `setGoal` again; hydrate does not restart the loop.
140
140
  */
141
141
  hydrate(state: GoalState): void {
142
+ // Hydration is historical initialization, never an overwrite. A live
143
+ // parser event or caller-set goal that won the race remains authoritative.
144
+ if (this.state !== null) return;
142
145
  this.state = state;
143
146
  this.mode = "idle";
144
147
  }
@@ -387,6 +390,19 @@ export class GoalController {
387
390
  // re-enters onTurnSettled and continues the loop.
388
391
  const nudge = verdict.nudge ?? defaultNudge(objective, this.iterations);
389
392
  void this.deps.send(nudge).catch(() => {});
393
+ } catch (error) {
394
+ if (gen !== this.generation) return;
395
+ this.transition({
396
+ objective,
397
+ status: "blocked",
398
+ met: false,
399
+ enforced: true,
400
+ source: "agentex",
401
+ blockedReason: "sentinel_error",
402
+ errorMessage: error instanceof Error ? error.message : String(error),
403
+ });
404
+ this.mode = "idle";
405
+ this.sentinel = null;
390
406
  } finally {
391
407
  this.evaluating = false;
392
408
  }
@@ -433,6 +449,7 @@ export class GoalController {
433
449
  raw: { synthetic: "goal", ...fields },
434
450
  };
435
451
  if (fields.blockedReason !== undefined) event.blockedReason = fields.blockedReason;
452
+ if (fields.errorMessage !== undefined) event.errorMessage = fields.errorMessage;
436
453
  if (fields.tokensUsed !== undefined) event.tokensUsed = fields.tokensUsed;
437
454
  if (fields.timeUsedSeconds !== undefined) event.timeUsedSeconds = fields.timeUsedSeconds;
438
455
  if (fields.tokenBudget !== undefined) event.tokenBudget = fields.tokenBudget;
@@ -33,6 +33,7 @@ export interface NormalizedGoalFields {
33
33
  enforced: boolean;
34
34
  source: GoalSource;
35
35
  blockedReason?: GoalBlockedReason;
36
+ errorMessage?: string;
36
37
  tokensUsed?: number;
37
38
  timeUsedSeconds?: number;
38
39
  tokenBudget?: number;
@@ -90,6 +91,9 @@ export function normalizeCodexGoalStatus(raw: string | null | undefined): {
90
91
  case "budget-limited":
91
92
  case "budget_limited":
92
93
  case "budgetlimited": // camelCase `budgetLimited`, lowercased
94
+ case "usage-limited":
95
+ case "usage_limited":
96
+ case "usagelimited": // camelCase `usageLimited`, lowercased
93
97
  return { status: "blocked", met: false, blockedReason: "budget" };
94
98
  case "blocked":
95
99
  return { status: "blocked", met: false, blockedReason: "needs_input" };
@@ -165,6 +169,7 @@ export function goalStateFromEvent(event: GoalStatusEvent): GoalState {
165
169
  updatedAt: event.timestamp,
166
170
  };
167
171
  if (event.blockedReason !== undefined) state.blockedReason = event.blockedReason;
172
+ if (event.errorMessage !== undefined) state.errorMessage = event.errorMessage;
168
173
  if (event.tokensUsed !== undefined) state.tokensUsed = event.tokensUsed;
169
174
  if (event.timeUsedSeconds !== undefined) state.timeUsedSeconds = event.timeUsedSeconds;
170
175
  if (event.tokenBudget !== undefined) state.tokenBudget = event.tokenBudget;
@@ -0,0 +1,307 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, stat, open as openFile, type FileHandle } from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import * as readline from "node:readline";
5
+
6
+ import {
7
+ LocalHistoryError,
8
+ type LocalHistorySourceFingerprint,
9
+ } from "./types.js";
10
+
11
+ export const UUID_JSONL_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i;
12
+ export const ROLLOUT_UUID_RE = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
13
+
14
+ export interface JsonlRecord {
15
+ raw: Record<string, unknown>;
16
+ text: string;
17
+ lineStartOffset: number;
18
+ nextOffset: number;
19
+ }
20
+
21
+ interface StableReadOptions {
22
+ /** Reject a completed read when the opened source changed while streaming. */
23
+ rejectChanges?: boolean;
24
+ /** Internal deterministic test hook. Not part of the public history API. */
25
+ afterInitialStat?: () => void | Promise<void>;
26
+ }
27
+
28
+ interface FileIdentity {
29
+ size: bigint;
30
+ modifiedAtNs: bigint;
31
+ device: bigint;
32
+ inode: bigint;
33
+ }
34
+
35
+ export function asRecord(value: unknown): Record<string, unknown> | null {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value)
37
+ ? value as Record<string, unknown>
38
+ : null;
39
+ }
40
+
41
+ export function asString(value: unknown): string | null {
42
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
43
+ }
44
+
45
+ export function isoDate(value: unknown, fallback: string | null = null): string | null {
46
+ if (typeof value !== "string") return fallback;
47
+ const parsed = new Date(value);
48
+ return Number.isNaN(parsed.getTime()) ? fallback : parsed.toISOString();
49
+ }
50
+
51
+ export function cleanTitle(value: string | null): string | null {
52
+ const cleaned = value
53
+ ?.replace(/<[^>]+>/g, " ")
54
+ .replace(/\[\[[^\]]+\]\]/g, " ")
55
+ .replace(/^[#>*_`\s-]+/g, "")
56
+ .replace(/\s+/g, " ")
57
+ .trim();
58
+ if (!cleaned) return null;
59
+ return cleaned.length > 120 ? `${cleaned.slice(0, 117).trimEnd()}...` : cleaned;
60
+ }
61
+
62
+ export async function isDirectory(filePath: string): Promise<boolean> {
63
+ try {
64
+ return (await stat(filePath)).isDirectory();
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ export async function listJsonlFiles(
71
+ root: string,
72
+ options: { maxDepth: number; limit?: number; accept?: (name: string) => boolean },
73
+ ): Promise<string[]> {
74
+ const files: string[] = [];
75
+ const limit = Math.max(1, options.limit ?? Number.MAX_SAFE_INTEGER);
76
+
77
+ async function visit(dir: string, depth: number): Promise<void> {
78
+ if (depth > options.maxDepth || files.length >= limit) return;
79
+ let entries;
80
+ try {
81
+ entries = await readdir(dir, { withFileTypes: true });
82
+ } catch {
83
+ return;
84
+ }
85
+ entries.sort((a, b) => a.name.localeCompare(b.name));
86
+ for (const entry of entries) {
87
+ if (files.length >= limit) break;
88
+ const child = path.join(dir, entry.name);
89
+ if (entry.isDirectory()) await visit(child, depth + 1);
90
+ else if (entry.isFile() && entry.name.endsWith(".jsonl") && (options.accept?.(entry.name) ?? true)) {
91
+ files.push(child);
92
+ }
93
+ }
94
+ }
95
+
96
+ await visit(root, 0);
97
+ return files;
98
+ }
99
+
100
+ export async function mapLimited<T, U>(
101
+ values: readonly T[],
102
+ concurrency: number,
103
+ mapper: (value: T) => Promise<U>,
104
+ ): Promise<U[]> {
105
+ const output = new Array<U>(values.length);
106
+ let cursor = 0;
107
+ async function worker(): Promise<void> {
108
+ while (cursor < values.length) {
109
+ const index = cursor++;
110
+ output[index] = await mapper(values[index]!);
111
+ }
112
+ }
113
+ await Promise.all(Array.from(
114
+ { length: Math.min(Math.max(1, concurrency), values.length) },
115
+ () => worker(),
116
+ ));
117
+ return output;
118
+ }
119
+
120
+ async function fileIdentity(handle: FileHandle): Promise<FileIdentity> {
121
+ const info = await handle.stat({ bigint: true });
122
+ if (!info.isFile()) throw new LocalHistoryError("invalid_session", "Local history source is not a file");
123
+ return {
124
+ size: info.size,
125
+ modifiedAtNs: info.mtimeNs,
126
+ device: info.dev,
127
+ inode: info.ino,
128
+ };
129
+ }
130
+
131
+ function sameFileIdentity(left: FileIdentity, right: FileIdentity): boolean {
132
+ return left.size === right.size
133
+ && left.modifiedAtNs === right.modifiedAtNs
134
+ && left.device === right.device
135
+ && left.inode === right.inode;
136
+ }
137
+
138
+ function sourceChangedError(): LocalHistoryError {
139
+ return new LocalHistoryError(
140
+ "source_changed_during_read",
141
+ "Local history source changed during the read",
142
+ );
143
+ }
144
+
145
+ function wrapFileError(error: unknown, action: "inspect" | "read" | "hash"): LocalHistoryError {
146
+ if (error instanceof LocalHistoryError) return error;
147
+ const code = (error as NodeJS.ErrnoException).code;
148
+ return new LocalHistoryError(
149
+ code === "ENOENT" ? "source_missing" : code === "EACCES" ? "permission_denied" : "io_error",
150
+ code === "ENOENT"
151
+ ? "Local history source is missing"
152
+ : `Unable to ${action} local history source`,
153
+ { cause: error },
154
+ );
155
+ }
156
+
157
+ export async function recentEligible<T>(
158
+ filePaths: readonly string[],
159
+ options: { limit?: number; concurrency?: number },
160
+ inspect: (filePath: string) => Promise<T | null>,
161
+ ): Promise<T[]> {
162
+ const limit = options.limit === undefined
163
+ ? Number.MAX_SAFE_INTEGER
164
+ : Math.max(0, options.limit);
165
+ if (limit === 0 || filePaths.length === 0) return [];
166
+
167
+ const metadata = await mapLimited(filePaths, 32, async (filePath) => {
168
+ try {
169
+ const info = await stat(filePath, { bigint: true });
170
+ return info.isFile() ? { filePath, modifiedAtNs: info.mtimeNs } : null;
171
+ } catch {
172
+ return null;
173
+ }
174
+ });
175
+ const ordered = metadata
176
+ .filter((value): value is { filePath: string; modifiedAtNs: bigint } => value !== null)
177
+ .sort((a, b) => a.modifiedAtNs === b.modifiedAtNs
178
+ ? a.filePath.localeCompare(b.filePath)
179
+ : a.modifiedAtNs > b.modifiedAtNs ? -1 : 1);
180
+
181
+ const output: T[] = [];
182
+ const concurrency = Math.max(1, options.concurrency ?? 16);
183
+ let cursor = 0;
184
+ while (cursor < ordered.length && output.length < limit) {
185
+ // Never inspect more files than the number of results still requested in
186
+ // one batch. Excluded files cause another bounded batch to be pulled.
187
+ const batchSize = Math.min(concurrency, limit - output.length, ordered.length - cursor);
188
+ const batch = ordered.slice(cursor, cursor + batchSize);
189
+ cursor += batch.length;
190
+ const inspected = await mapLimited(batch, concurrency, ({ filePath }) => inspect(filePath));
191
+ for (const value of inspected) {
192
+ if (value !== null) output.push(value);
193
+ if (output.length >= limit) break;
194
+ }
195
+ }
196
+ return output;
197
+ }
198
+
199
+ export async function sourceFingerprint(
200
+ filePath: string,
201
+ includeSha256 = false,
202
+ internalOptions: StableReadOptions = {},
203
+ ): Promise<LocalHistorySourceFingerprint> {
204
+ let handle: FileHandle | null = null;
205
+ try {
206
+ handle = await openFile(filePath, "r");
207
+ const before = await fileIdentity(handle);
208
+ const fingerprint: LocalHistorySourceFingerprint = {
209
+ size: Number(before.size),
210
+ modifiedAtNs: before.modifiedAtNs.toString(),
211
+ };
212
+ if (!includeSha256) return fingerprint;
213
+
214
+ await internalOptions.afterInitialStat?.();
215
+ const hash = createHash("sha256");
216
+ const stream = handle.createReadStream({ autoClose: false });
217
+ let after: FileIdentity;
218
+ try {
219
+ for await (const chunk of stream) hash.update(chunk as Buffer);
220
+ after = await fileIdentity(handle);
221
+ } finally {
222
+ stream.destroy();
223
+ }
224
+ if (!sameFileIdentity(before, after)) throw sourceChangedError();
225
+ fingerprint.sha256 = hash.digest("hex");
226
+ return fingerprint;
227
+ } catch (error) {
228
+ throw wrapFileError(error, includeSha256 ? "hash" : "inspect");
229
+ } finally {
230
+ await handle?.close();
231
+ }
232
+ }
233
+
234
+ export async function* readJsonlRecords(
235
+ filePath: string,
236
+ fromOffset = 0,
237
+ options: StableReadOptions = {},
238
+ ): AsyncIterable<JsonlRecord> {
239
+ let handle: FileHandle | null = null;
240
+ let stream: ReturnType<FileHandle["createReadStream"]> | null = null;
241
+ let rl: readline.Interface | null = null;
242
+ let offset = fromOffset;
243
+ try {
244
+ handle = await openFile(filePath, "r");
245
+ const before = await fileIdentity(handle);
246
+ await options.afterInitialStat?.();
247
+ stream = handle.createReadStream({ start: fromOffset, encoding: "utf8", autoClose: false });
248
+ stream.on("error", () => {});
249
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
250
+ for await (const line of rl) {
251
+ const lineStartOffset = offset;
252
+ offset += Buffer.byteLength(line, "utf8") + 1;
253
+ if (!line.trim()) continue;
254
+ try {
255
+ const raw = asRecord(JSON.parse(line));
256
+ if (raw) yield { raw, text: line, lineStartOffset, nextOffset: offset };
257
+ } catch {
258
+ // A damaged historical record must not hide later valid records.
259
+ }
260
+ }
261
+ if (options.rejectChanges) {
262
+ const after = await fileIdentity(handle);
263
+ if (!sameFileIdentity(before, after)) throw sourceChangedError();
264
+ let pathInfo;
265
+ try {
266
+ pathInfo = await stat(filePath, { bigint: true });
267
+ } catch (error) {
268
+ throw wrapFileError(error, "inspect");
269
+ }
270
+ if (pathInfo.dev !== after.device || pathInfo.ino !== after.inode) throw sourceChangedError();
271
+ }
272
+ } catch (error) {
273
+ throw wrapFileError(error, "read");
274
+ } finally {
275
+ rl?.close();
276
+ stream?.destroy();
277
+ await handle?.close();
278
+ }
279
+ }
280
+
281
+ export function textFromContent(content: unknown, acceptedTypes: ReadonlySet<string>): string | null {
282
+ if (typeof content === "string") return content.trim() || null;
283
+ if (!Array.isArray(content)) return null;
284
+ const parts: string[] = [];
285
+ for (const value of content) {
286
+ const block = asRecord(value);
287
+ if (!block) continue;
288
+ const type = asString(block["type"]);
289
+ if (type && !acceptedTypes.has(type)) continue;
290
+ const text = asString(block["text"]) ?? asString(block["content"]);
291
+ if (text) parts.push(text);
292
+ }
293
+ return parts.join("\n\n").trim() || null;
294
+ }
295
+
296
+ export function meaningfulHumanText(text: string | null): string | null {
297
+ if (!text) return null;
298
+ const trimmed = text.trim();
299
+ if (!trimmed) return null;
300
+ if (/^<(?:environment_context|user_instructions|developer_instructions|system-reminder)[\s>]/i.test(trimmed)) {
301
+ return null;
302
+ }
303
+ if (/^<(?:local-command-caveat|command-name|command-message|command-args)[\s>]/i.test(trimmed)) {
304
+ return null;
305
+ }
306
+ return trimmed;
307
+ }
@@ -0,0 +1,18 @@
1
+ export {
2
+ LocalHistoryError,
3
+ } from "./types.js";
4
+ export type {
5
+ LocalHistoryArchiveState,
6
+ LocalHistoryDiscoverOptions,
7
+ LocalHistoryErrorCode,
8
+ LocalHistoryEvent,
9
+ LocalHistoryFingerprintOptions,
10
+ LocalHistoryOps,
11
+ LocalHistoryProbeOptions,
12
+ LocalHistoryProbeResult,
13
+ LocalHistoryReadOptions,
14
+ LocalHistorySession,
15
+ LocalHistorySourceFingerprint,
16
+ LocalHistoryUserEvent,
17
+ LocalHistoryYield,
18
+ } from "./types.js";