@namzu/sdk 20.4.0 → 21.1.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 (98) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/dist/bridge/sse/mapper.d.ts +19 -0
  3. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  4. package/dist/bridge/sse/mapper.js +9 -1
  5. package/dist/bridge/sse/mapper.js.map +1 -1
  6. package/dist/contracts/schemas.d.ts +10 -10
  7. package/dist/manager/run/persistence.d.ts +26 -0
  8. package/dist/manager/run/persistence.d.ts.map +1 -1
  9. package/dist/manager/run/persistence.js +40 -0
  10. package/dist/manager/run/persistence.js.map +1 -1
  11. package/dist/public-runtime.d.ts +8 -0
  12. package/dist/public-runtime.d.ts.map +1 -1
  13. package/dist/public-runtime.js +31 -0
  14. package/dist/public-runtime.js.map +1 -1
  15. package/dist/run/command-gate.d.ts +107 -0
  16. package/dist/run/command-gate.d.ts.map +1 -0
  17. package/dist/run/command-gate.js +157 -0
  18. package/dist/run/command-gate.js.map +1 -0
  19. package/dist/run/drain.d.ts +194 -0
  20. package/dist/run/drain.d.ts.map +1 -0
  21. package/dist/run/drain.js +228 -0
  22. package/dist/run/drain.js.map +1 -0
  23. package/dist/run/index.d.ts +8 -0
  24. package/dist/run/index.d.ts.map +1 -1
  25. package/dist/run/index.js +4 -0
  26. package/dist/run/index.js.map +1 -1
  27. package/dist/run/memory-promoter.d.ts +70 -0
  28. package/dist/run/memory-promoter.d.ts.map +1 -0
  29. package/dist/run/memory-promoter.js +117 -0
  30. package/dist/run/memory-promoter.js.map +1 -0
  31. package/dist/run/workspace-fingerprint.d.ts +105 -0
  32. package/dist/run/workspace-fingerprint.d.ts.map +1 -0
  33. package/dist/run/workspace-fingerprint.js +147 -0
  34. package/dist/run/workspace-fingerprint.js.map +1 -0
  35. package/dist/runtime/query/events.d.ts +12 -0
  36. package/dist/runtime/query/events.d.ts.map +1 -1
  37. package/dist/runtime/query/events.js +61 -3
  38. package/dist/runtime/query/events.js.map +1 -1
  39. package/dist/runtime/query/index.d.ts +36 -0
  40. package/dist/runtime/query/index.d.ts.map +1 -1
  41. package/dist/runtime/query/index.js +32 -0
  42. package/dist/runtime/query/index.js.map +1 -1
  43. package/dist/runtime/query/resume-run.d.ts +25 -0
  44. package/dist/runtime/query/resume-run.d.ts.map +1 -1
  45. package/dist/runtime/query/resume-run.js +15 -3
  46. package/dist/runtime/query/resume-run.js.map +1 -1
  47. package/dist/store/index.d.ts +1 -1
  48. package/dist/store/index.d.ts.map +1 -1
  49. package/dist/store/index.js +1 -1
  50. package/dist/store/index.js.map +1 -1
  51. package/dist/store/run/conformance.d.ts +159 -0
  52. package/dist/store/run/conformance.d.ts.map +1 -0
  53. package/dist/store/run/conformance.js +451 -0
  54. package/dist/store/run/conformance.js.map +1 -0
  55. package/dist/store/run/disk.d.ts +30 -2
  56. package/dist/store/run/disk.d.ts.map +1 -1
  57. package/dist/store/run/disk.js +101 -0
  58. package/dist/store/run/disk.js.map +1 -1
  59. package/dist/store/run/memory.d.ts +5 -4
  60. package/dist/store/run/memory.d.ts.map +1 -1
  61. package/dist/store/run/memory.js +30 -1
  62. package/dist/store/run/memory.js.map +1 -1
  63. package/dist/types/doctor/check.d.ts +28 -2
  64. package/dist/types/doctor/check.d.ts.map +1 -1
  65. package/dist/types/run/event-cursor.d.ts +90 -0
  66. package/dist/types/run/event-cursor.d.ts.map +1 -0
  67. package/dist/types/run/event-cursor.js +47 -0
  68. package/dist/types/run/event-cursor.js.map +1 -0
  69. package/dist/types/run/events.d.ts +71 -1
  70. package/dist/types/run/events.d.ts.map +1 -1
  71. package/dist/types/run/events.js.map +1 -1
  72. package/dist/types/run/index.d.ts +1 -0
  73. package/dist/types/run/index.d.ts.map +1 -1
  74. package/dist/types/run/index.js +1 -0
  75. package/dist/types/run/index.js.map +1 -1
  76. package/dist/types/run/store.d.ts +41 -1
  77. package/dist/types/run/store.d.ts.map +1 -1
  78. package/package.json +6 -1
  79. package/src/bridge/sse/mapper.ts +28 -1
  80. package/src/manager/run/persistence.ts +43 -0
  81. package/src/public-runtime.ts +53 -0
  82. package/src/run/command-gate.ts +234 -0
  83. package/src/run/drain.ts +393 -0
  84. package/src/run/index.ts +20 -0
  85. package/src/run/memory-promoter.ts +155 -0
  86. package/src/run/workspace-fingerprint.ts +193 -0
  87. package/src/runtime/query/events.ts +68 -4
  88. package/src/runtime/query/index.ts +87 -0
  89. package/src/runtime/query/resume-run.ts +67 -12
  90. package/src/store/index.ts +1 -1
  91. package/src/store/run/conformance.ts +705 -0
  92. package/src/store/run/disk.ts +106 -2
  93. package/src/store/run/memory.ts +35 -5
  94. package/src/types/doctor/check.ts +28 -2
  95. package/src/types/run/event-cursor.ts +118 -0
  96. package/src/types/run/events.ts +80 -3
  97. package/src/types/run/index.ts +1 -0
  98. package/src/types/run/store.ts +43 -1
@@ -0,0 +1,228 @@
1
+ /**
2
+ * One pass over a queue of durable runs: list what nobody holds, take it,
3
+ * hand it to a worker, give it back.
4
+ *
5
+ * Every primitive this composes already shipped —
6
+ * {@link import('../store/run/listing.js').listDurableRuns} enumerates runs
7
+ * above a run id, `claimRun` arbitrates between processes, `releaseRun`
8
+ * returns a run to the queue, and `resumeRun` carries a fence into every
9
+ * durable write. Nothing composed them, so the two things the claim was
10
+ * built for — an approval inbox and a crash sweeper — still required a host
11
+ * to write the loop, and writing it correctly means getting the release
12
+ * into a `finally` and the `null` claim out of the error path. Both are the
13
+ * kind of thing a host gets wrong once, quietly.
14
+ *
15
+ * ## What this deliberately is NOT
16
+ *
17
+ * A supervisor, a daemon, or a scheduler. There is no timer here, no
18
+ * process spawn, no retry backoff and no `while (true)`. `drainRuns` makes
19
+ * ONE bounded pass and returns what happened; running it again is the
20
+ * caller's decision, made wherever that caller already has a scheduler. A
21
+ * per-platform supervisor is the same trade the deployment-adapter matrix
22
+ * was rejected for: one seam beats N adapters.
23
+ *
24
+ * The unit of work is a callback, so this module never needs a provider, a
25
+ * tool registry or a sandbox — the half of a run that cannot be serialized
26
+ * stays with the caller, exactly as `resumeRun` already splits it.
27
+ */
28
+ import { claimRun, listDurableRuns, releaseRun, summarizePark } from '../store/run/listing.js';
29
+ import { NamzuError } from '../types/errors/index.js';
30
+ /** Runs handled per pass when the caller names no page size. */
31
+ export const DEFAULT_DRAIN_PAGE_SIZE = 100;
32
+ function refuse(code, message, details) {
33
+ throw new NamzuError({ code, message, details });
34
+ }
35
+ function toMessage(err) {
36
+ return err instanceof Error ? err.message : String(err);
37
+ }
38
+ /**
39
+ * Refuse a store that cannot do the whole job, BEFORE anything is listed.
40
+ *
41
+ * Checked up front rather than at the first call that needs each method, so
42
+ * that a store missing only `releaseRun` cannot resume half a queue and then
43
+ * discover it has no way to give the runs back. The optional-capability rule
44
+ * on `CheckpointStore` says a caller refuses rather than degrades; a drainer
45
+ * that degraded would be the worst instance of it, because "claimed by
46
+ * default" here means every worker proceeds on every run.
47
+ */
48
+ function assertDrainable(store) {
49
+ const missing = ['listDurableRuns', 'claimRun', 'releaseRun'].filter((m) => typeof store[m] !== 'function');
50
+ if (missing.length === 0)
51
+ return;
52
+ refuse('capability_unavailable', `drainRuns: the injected checkpoint store does not implement ${missing.map((m) => `\`${m}\``).join(', ')}, so it cannot arbitrate a queue. Refusing before anything is claimed rather than draining what it can — a drainer that proceeded without a claim would let two workers restore one checkpoint, both execute its tools and both write under one run id. Supply a store that implements all three (the built-in disk and in-memory stores do), or run a single writer per run.`, { missing });
53
+ }
54
+ /**
55
+ * Take every unclaimed run under a scope, one bounded pass, and give each
56
+ * one back when its work returns.
57
+ *
58
+ * The shape is: list parked-and-unclaimed → claim → work → release in a
59
+ * `finally`. The `finally` is the part a host writes wrong: a worker that
60
+ * returns without releasing leaves the run stuck until the lease lapses,
61
+ * and a worker that releases only on success leaves a FAILED run stuck for
62
+ * the same duration — so a queue quietly loses its throughput to the runs
63
+ * that need retrying most.
64
+ *
65
+ * `claimed: false` is not a parameter. A drainer never wants work somebody
66
+ * else holds; that is what makes it a drainer rather than a listing. An
67
+ * expired claim counts as unheld, which is what makes a dead worker's runs
68
+ * recoverable at all.
69
+ *
70
+ * ## What "exactly once" does and does not mean here
71
+ *
72
+ * Two drainers never hold one run at the same time — that is the claim, and
73
+ * it is absolute. **Exactly-once over a whole pass is a weaker promise, and
74
+ * where it holds it comes from the FILTER, not from the claim.** A listing
75
+ * is a snapshot; between paging a row and claiming it, another drainer can
76
+ * finish that run and release it, and the claim then succeeds on work
77
+ * already done. So a claimed row is re-read against
78
+ * {@link DrainRunsParams.park} before any work starts, and one that no
79
+ * longer matches is given straight back as {@link DrainRunsResult.stale}.
80
+ * An inbox drain (`park: ['outstanding']`) whose work answers the park is
81
+ * therefore exactly-once, because doing the work is what removes the run
82
+ * from the queue.
83
+ *
84
+ * With NO park filter there is nothing to re-check, and two drainers can
85
+ * both process one run. That is not an omission: a checkpoint store holds no
86
+ * run STATUS by design — nothing in it distinguishes a run that finished
87
+ * from one that died — so "already done" is a fact only the host's own run
88
+ * records carry. A crash sweep intersects with those records inside
89
+ * `onRun`, which is the shape {@link DurableRunEntry} already prescribes.
90
+ *
91
+ * @throws NamzuError `capability_unavailable` when the store cannot list,
92
+ * claim or release — before any run is touched.
93
+ * @throws NamzuError `invalid_config` on a lease or concurrency that cannot
94
+ * mean what it says.
95
+ */
96
+ export async function drainRuns(params) {
97
+ const { store, scope, holder, ttlMs, onRun, park, signal, now } = params;
98
+ assertDrainable(store);
99
+ if (holder.trim().length === 0) {
100
+ refuse('invalid_config', 'drainRuns: `holder` is empty. It is the only thing that distinguishes a renewal from a theft, so two drainers sharing one string take live claims from each other instantly. Use something per-process — a worker id, a pod name plus a pid.', { holder });
101
+ }
102
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
103
+ refuse('invalid_config', `drainRuns: ttlMs must be a positive number of milliseconds, got ${String(ttlMs)}. A lease that expires immediately is a lease every worker can take at once, which is the condition a claim exists to prevent.`, { ttlMs });
104
+ }
105
+ const maxConcurrent = params.maxConcurrent ?? 1;
106
+ if (!Number.isInteger(maxConcurrent) || maxConcurrent < 1) {
107
+ refuse('invalid_config', `drainRuns: maxConcurrent must be a positive integer, got ${String(params.maxConcurrent)}. Zero would drain nothing while reporting a successful pass.`, { maxConcurrent: params.maxConcurrent });
108
+ }
109
+ const pageSize = params.pageSize ?? DEFAULT_DRAIN_PAGE_SIZE;
110
+ const drained = [];
111
+ const skipped = [];
112
+ const stale = [];
113
+ const failed = [];
114
+ const unreleased = [];
115
+ let listed = 0;
116
+ let stopped = false;
117
+ const giveBack = async (entry, fence) => {
118
+ try {
119
+ await releaseRun(store, entry, fence);
120
+ }
121
+ catch (err) {
122
+ // Never rethrown: on the work path this runs inside a `finally`
123
+ // unwinding the caller's error, and replacing it would send the
124
+ // operator to debug the disk instead of the run.
125
+ unreleased.push({ runId: entry.runId, error: toMessage(err) });
126
+ }
127
+ };
128
+ /**
129
+ * Is this row still the row the listing described?
130
+ *
131
+ * A listing is a SNAPSHOT, and a claim taken against a stale snapshot is
132
+ * a claim on work somebody already did. The window is real and small:
133
+ * drainer B pages the queue, drainer A takes a run, finishes it, answers
134
+ * its park and releases — and B's claim then succeeds on a run that is no
135
+ * longer outstanding. Mutual exclusion cannot close that; only re-reading
136
+ * after the claim can, which is why this is here and not in the store.
137
+ *
138
+ * Only the park is re-checked, because it is the only predicate this loop
139
+ * was given. **Two drainers with no park filter can both process one
140
+ * run**, and no amount of claiming prevents it: a checkpoint store holds
141
+ * no run STATUS by design — see the note on {@link DurableRunEntry} — so
142
+ * "already done" is a fact only the host's own run records carry. A crash
143
+ * sweep intersects with those records inside `onRun`.
144
+ */
145
+ const stillMatches = async (entry) => {
146
+ if (!park)
147
+ return true;
148
+ const fresh = summarizePark(await store.listCheckpoints(entry), now ?? Date.now());
149
+ return fresh !== undefined && park.includes(fresh.state);
150
+ };
151
+ /**
152
+ * One run: take it, work it, give it back.
153
+ *
154
+ * **No cancellation check here, and its absence is deliberate.** One was
155
+ * written, and a mutation test found nothing could kill it: the batch
156
+ * below dispatches with `.map(handle)`, which calls every handler
157
+ * synchronously before any of them awaits, so a signal that aborts during
158
+ * a batch cannot be observed at the top of a handler that has already
159
+ * been entered — and a signal that aborts BETWEEN batches is caught by
160
+ * the check in the loop, which runs first. A branch nothing can reach is
161
+ * a declaration nothing drives, so it is gone rather than covered by a
162
+ * test that would have proved nothing
163
+ * (`docs/conventions/declared-but-undriven.md`).
164
+ */
165
+ const handle = async (entry) => {
166
+ const claim = await claimRun(store, entry, {
167
+ holder,
168
+ ttlMs,
169
+ ...(now !== undefined ? { now } : {}),
170
+ });
171
+ // `null` is not an error. Another worker got there first, which is the
172
+ // ordinary outcome of two readers on one queue.
173
+ if (!claim) {
174
+ skipped.push(entry.runId);
175
+ return;
176
+ }
177
+ // Checked with the claim in hand rather than before taking it: only
178
+ // under the claim is the answer stable, because nobody else can change
179
+ // it while this drainer holds the run.
180
+ if (!(await stillMatches(entry))) {
181
+ stale.push(entry.runId);
182
+ await giveBack(entry, claim.fence);
183
+ return;
184
+ }
185
+ try {
186
+ await onRun(entry, claim);
187
+ drained.push(entry.runId);
188
+ }
189
+ catch (err) {
190
+ failed.push({ runId: entry.runId, error: toMessage(err) });
191
+ }
192
+ finally {
193
+ await giveBack(entry, claim.fence);
194
+ }
195
+ };
196
+ let cursor;
197
+ do {
198
+ if (signal?.aborted) {
199
+ stopped = true;
200
+ break;
201
+ }
202
+ const page = await listDurableRuns(store, scope, {
203
+ // Not a parameter. See the note above.
204
+ claimed: false,
205
+ ...(park ? { park } : {}),
206
+ limit: pageSize,
207
+ ...(cursor !== undefined ? { cursor } : {}),
208
+ ...(now !== undefined ? { now } : {}),
209
+ });
210
+ listed += page.entries.length;
211
+ // Windowed rather than `Promise.all` over the page: the leases are taken
212
+ // as the work starts, so a page bigger than the pass can finish inside
213
+ // one TTL does not hand the tail of it to somebody else mid-flight.
214
+ for (let i = 0; i < page.entries.length; i += maxConcurrent) {
215
+ if (signal?.aborted) {
216
+ stopped = true;
217
+ break;
218
+ }
219
+ await Promise.all(page.entries.slice(i, i + maxConcurrent).map(handle));
220
+ }
221
+ // A drained run is released, so it is unclaimed again — but the cursor
222
+ // is a position in a total order and has already passed it, so the pass
223
+ // cannot see it twice and cannot fail to terminate.
224
+ cursor = stopped ? undefined : page.cursor;
225
+ } while (cursor !== undefined);
226
+ return { listed, drained, skipped, stale, failed, unreleased, stopped };
227
+ }
228
+ //# sourceMappingURL=drain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drain.js","sourceRoot":"","sources":["../../src/run/drain.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,14 @@ export { RunPersistence } from '../manager/run/persistence.js';
2
2
  export { RunDiskStore } from '../store/run/disk.js';
3
3
  export { createRunReporter } from './reporter.js';
4
4
  export type { RunReporter } from './reporter.js';
5
+ export { DEFAULT_DRAIN_PAGE_SIZE, drainRuns } from './drain.js';
6
+ export type { DrainFailure, DrainRun, DrainRunsParams, DrainRunsResult } from './drain.js';
7
+ export { DEFAULT_GATE_MAX_RETRIES, DEFAULT_GATE_OUTPUT_CHARS, DEFAULT_GATE_TIMEOUT_MS, clipOutput, createCommandGate, } from './command-gate.js';
8
+ export type { CommandGateOptions, GateExec } from './command-gate.js';
9
+ export { FINGERPRINT_MAX_BYTES, FINGERPRINT_TIMEOUT_MS, fingerprintWorkspace, } from './workspace-fingerprint.js';
10
+ export type { FingerprintExec, WorkspaceFingerprintOptions } from './workspace-fingerprint.js';
5
11
  export { checkLimitsDetailed, buildLimitConfig } from './LimitChecker.js';
6
12
  export type { LimitCheckerState, LimitCheckResult } from './LimitChecker.js';
13
+ export { RUN_MEMORY_TAG, createMemoryPromoter } from './memory-promoter.js';
14
+ export type { MemoryPromoterOptions } from './memory-promoter.js';
7
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/run/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAE9D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjD,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/run/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAE9D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjD,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC1F,OAAO,EACN,wBAAwB,EACxB,yBAAyB,EACzB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,GACjB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,EACN,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,GACpB,MAAM,4BAA4B,CAAA;AACnC,YAAY,EAAE,eAAe,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAE9F,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAE5E,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3E,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA"}
package/dist/run/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  export { RunPersistence } from '../manager/run/persistence.js';
2
2
  export { RunDiskStore } from '../store/run/disk.js';
3
3
  export { createRunReporter } from './reporter.js';
4
+ export { DEFAULT_DRAIN_PAGE_SIZE, drainRuns } from './drain.js';
5
+ export { DEFAULT_GATE_MAX_RETRIES, DEFAULT_GATE_OUTPUT_CHARS, DEFAULT_GATE_TIMEOUT_MS, clipOutput, createCommandGate, } from './command-gate.js';
6
+ export { FINGERPRINT_MAX_BYTES, FINGERPRINT_TIMEOUT_MS, fingerprintWorkspace, } from './workspace-fingerprint.js';
4
7
  export { checkLimitsDetailed, buildLimitConfig } from './LimitChecker.js';
8
+ export { RUN_MEMORY_TAG, createMemoryPromoter } from './memory-promoter.js';
5
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/run/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAE9D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAGjD,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/run/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAE9D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAGjD,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE/D,OAAO,EACN,wBAAwB,EACxB,yBAAyB,EACzB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,GACjB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EACN,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,GACpB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAGzE,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The default {@link PromoteMemory}: write what a run learned into a
3
+ * {@link MemoryStore}, or write nothing at all.
4
+ *
5
+ * `promoteMemory` is called once at settle with the compaction extractor's
6
+ * already-structured output — decisions, discoveries, user requirements,
7
+ * failures, environment facts — and **nothing shipped supplied the hook**.
8
+ * So the structure the compaction pass had spent tokens producing was
9
+ * serialized into one system message and dropped on the floor when the run
10
+ * ended, exactly as its own module comment says. This is the supplier, and
11
+ * it is mostly a filter: the hard part — extracting facts from a transcript
12
+ * — already happened.
13
+ *
14
+ * ## The filter, which is the only decision here
15
+ *
16
+ * **A run that learned nothing must leave nothing.** Not an empty record,
17
+ * not a record whose body says "no decisions" — nothing. A promoter that
18
+ * wrote a row per run would fill the store with the runs least worth
19
+ * remembering, and `search_memory` would then return them: the model reads
20
+ * that store on later runs, so noise here is not merely wasted disk, it is
21
+ * context spent on a run that did nothing.
22
+ *
23
+ * What counts as having learned something is the five KNOWLEDGE categories —
24
+ * decisions, discoveries, user requirements, failures, environment. Not
25
+ * `task`, which every run has because it is the prompt restated. Not
26
+ * `files`, which every run that opened anything has, and which says what was
27
+ * touched rather than what was learned. A run whose only trace is "it read
28
+ * six files" is the exact record this filter exists to refuse.
29
+ *
30
+ * ## What it does NOT do
31
+ *
32
+ * Deduplicate against what is already stored, merge with a previous run's
33
+ * record, or expire anything. Each is a policy with real trade-offs and a
34
+ * host that wants one owns it — `promoteMemory` is a callback precisely so
35
+ * that the runtime does not decide this. This is the obvious default, not
36
+ * the only possible one.
37
+ */
38
+ import type { MemoryStore } from '../types/memory/index.js';
39
+ import type { PromoteMemory } from '../types/run/memory-promotion.js';
40
+ /** Tag every record this promoter writes, so a host can find or prune them. */
41
+ export declare const RUN_MEMORY_TAG = "run-memory";
42
+ export interface MemoryPromoterOptions {
43
+ /** Where records go. The same store `save_memory` writes through. */
44
+ readonly store: MemoryStore;
45
+ /**
46
+ * Extra tags on every record, beyond {@link RUN_MEMORY_TAG}.
47
+ *
48
+ * A host running several agents against one store uses this to tell whose
49
+ * memory is whose; without it a later search cannot.
50
+ */
51
+ readonly tags?: readonly string[];
52
+ /**
53
+ * Cap on entries rendered per category. Defaults to 20.
54
+ *
55
+ * The extractor already caps its lists, and this is the second cap for
56
+ * the same reason the first exists: a record nobody will read is a record
57
+ * that costs context every time it is retrieved.
58
+ */
59
+ readonly maxPerCategory?: number;
60
+ }
61
+ /**
62
+ * Build a promoter that writes one record per run that learned something.
63
+ *
64
+ * Never throws out to the runtime — but it does not swallow either: the
65
+ * runtime already catches and logs a promoter's failure at settle, and
66
+ * catching here as well would hide a broken store from the one place that
67
+ * reports it.
68
+ */
69
+ export declare function createMemoryPromoter(options: MemoryPromoterOptions): PromoteMemory;
70
+ //# sourceMappingURL=memory-promoter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-promoter.d.ts","sourceRoot":"","sources":["../../src/run/memory-promoter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,kCAAkC,CAAA;AAkBzF,+EAA+E;AAC/E,eAAO,MAAM,cAAc,eAAe,CAAA;AAE1C,MAAM,WAAW,qBAAqB;IACrC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAChC;AA6CD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,aAAa,CAuBlF"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The default {@link PromoteMemory}: write what a run learned into a
3
+ * {@link MemoryStore}, or write nothing at all.
4
+ *
5
+ * `promoteMemory` is called once at settle with the compaction extractor's
6
+ * already-structured output — decisions, discoveries, user requirements,
7
+ * failures, environment facts — and **nothing shipped supplied the hook**.
8
+ * So the structure the compaction pass had spent tokens producing was
9
+ * serialized into one system message and dropped on the floor when the run
10
+ * ended, exactly as its own module comment says. This is the supplier, and
11
+ * it is mostly a filter: the hard part — extracting facts from a transcript
12
+ * — already happened.
13
+ *
14
+ * ## The filter, which is the only decision here
15
+ *
16
+ * **A run that learned nothing must leave nothing.** Not an empty record,
17
+ * not a record whose body says "no decisions" — nothing. A promoter that
18
+ * wrote a row per run would fill the store with the runs least worth
19
+ * remembering, and `search_memory` would then return them: the model reads
20
+ * that store on later runs, so noise here is not merely wasted disk, it is
21
+ * context spent on a run that did nothing.
22
+ *
23
+ * What counts as having learned something is the five KNOWLEDGE categories —
24
+ * decisions, discoveries, user requirements, failures, environment. Not
25
+ * `task`, which every run has because it is the prompt restated. Not
26
+ * `files`, which every run that opened anything has, and which says what was
27
+ * touched rather than what was learned. A run whose only trace is "it read
28
+ * six files" is the exact record this filter exists to refuse.
29
+ *
30
+ * ## What it does NOT do
31
+ *
32
+ * Deduplicate against what is already stored, merge with a previous run's
33
+ * record, or expire anything. Each is a policy with real trade-offs and a
34
+ * host that wants one owns it — `promoteMemory` is a callback precisely so
35
+ * that the runtime does not decide this. This is the obvious default, not
36
+ * the only possible one.
37
+ */
38
+ /**
39
+ * The categories that make a run worth remembering.
40
+ *
41
+ * Ordered as they are rendered. `userRequirements` first because it is the
42
+ * most durable of the five — a constraint the user stated outlives the run
43
+ * that heard it, whereas a discovery about a codebase expires when the
44
+ * codebase moves.
45
+ */
46
+ const KNOWLEDGE = [
47
+ ['userRequirements', 'What the user requires'],
48
+ ['decisions', 'Decisions'],
49
+ ['discoveries', 'Discoveries'],
50
+ ['failures', 'What did not work'],
51
+ ['environment', 'Environment'],
52
+ ];
53
+ /** Tag every record this promoter writes, so a host can find or prune them. */
54
+ export const RUN_MEMORY_TAG = 'run-memory';
55
+ /** Everything the candidate knows, as `[heading, items]`, empties dropped. */
56
+ function knowledge(candidate, cap) {
57
+ const out = [];
58
+ for (const [key, heading] of KNOWLEDGE) {
59
+ const items = candidate[key];
60
+ if (items.length > 0)
61
+ out.push([heading, items.slice(0, cap)]);
62
+ }
63
+ return out;
64
+ }
65
+ /** A one-line summary naming what kind of knowledge the record holds. */
66
+ function summarize(sections) {
67
+ return sections.map(([heading, items]) => `${heading.toLowerCase()} (${items.length})`).join(', ');
68
+ }
69
+ function render(candidate, sections) {
70
+ const body = sections.map(([heading, items]) => `## ${heading}\n\n${items.map((i) => `- ${i}`).join('\n')}`);
71
+ // The eviction counts, when there are any. Carried rather than hidden for
72
+ // the reason the candidate carries them: somebody reading this record
73
+ // should know they are reading a truncated account of the run, not a
74
+ // complete one.
75
+ const evicted = Object.entries(candidate.evicted).filter(([, n]) => n > 0);
76
+ if (evicted.length > 0) {
77
+ body.push(`## Dropped during the run\n\n${evicted
78
+ .map(([category, n]) => `- ${category}: ${n} entr${n === 1 ? 'y' : 'ies'} evicted`)
79
+ .join('\n')}`);
80
+ }
81
+ if (candidate.files.length > 0) {
82
+ body.push(`## Files touched\n\n${candidate.files.map((f) => `- ${f}`).join('\n')}`);
83
+ }
84
+ return `# ${candidate.task}\n\n${body.join('\n\n')}\n`;
85
+ }
86
+ /**
87
+ * Build a promoter that writes one record per run that learned something.
88
+ *
89
+ * Never throws out to the runtime — but it does not swallow either: the
90
+ * runtime already catches and logs a promoter's failure at settle, and
91
+ * catching here as well would hide a broken store from the one place that
92
+ * reports it.
93
+ */
94
+ export function createMemoryPromoter(options) {
95
+ const cap = options.maxPerCategory ?? 20;
96
+ const tags = [RUN_MEMORY_TAG, ...(options.tags ?? [])];
97
+ return async (candidate) => {
98
+ const sections = knowledge(candidate, cap);
99
+ // Nothing learned, nothing written. Not an empty record: a store full
100
+ // of rows describing runs that discovered nothing is a store whose
101
+ // search results are mostly noise, and the model reads that store.
102
+ if (sections.length === 0)
103
+ return;
104
+ await options.store.create({
105
+ title: candidate.task.trim() || `Run ${candidate.runId}`,
106
+ summary: summarize(sections),
107
+ content: render(candidate, sections),
108
+ tags,
109
+ format: 'markdown',
110
+ // The run id, so a record can be traced back to the run that formed
111
+ // it. Evidence rather than decoration: without it a surprising
112
+ // memory cannot be checked against what actually happened.
113
+ metadata: { runId: candidate.runId, source: RUN_MEMORY_TAG },
114
+ });
115
+ };
116
+ }
117
+ //# sourceMappingURL=memory-promoter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-promoter.js","sourceRoot":"","sources":["../../src/run/memory-promoter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAKH;;;;;;;GAOG;AACH,MAAM,SAAS,GAAG;IACjB,CAAC,kBAAkB,EAAE,wBAAwB,CAAC;IAC9C,CAAC,WAAW,EAAE,WAAW,CAAC;IAC1B,CAAC,aAAa,EAAE,aAAa,CAAC;IAC9B,CAAC,UAAU,EAAE,mBAAmB,CAAC;IACjC,CAAC,aAAa,EAAE,aAAa,CAAC;CAC8C,CAAA;AAE7E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,cAAc,GAAG,YAAY,CAAA;AAsB1C,8EAA8E;AAC9E,SAAS,SAAS,CACjB,SAA6B,EAC7B,GAAW;IAEX,MAAM,GAAG,GAA6C,EAAE,CAAA;IACxD,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,SAAS,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAsB,CAAA;QACjD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IAC/D,CAAC;IACD,OAAO,GAAG,CAAA;AACX,CAAC;AAED,yEAAyE;AACzE,SAAS,SAAS,CAAC,QAA2D;IAC7E,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACnG,CAAC;AAED,SAAS,MAAM,CACd,SAA6B,EAC7B,QAA2D;IAE3D,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CACxB,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,MAAM,OAAO,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACjF,CAAA;IACD,0EAA0E;IAC1E,sEAAsE;IACtE,qEAAqE;IACrE,gBAAgB;IAChB,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC1E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI,CACR,gCAAgC,OAAO;aACrC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,QAAQ,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC;aAClF,IAAI,CAAC,IAAI,CAAC,EAAE,CACd,CAAA;IACF,CAAC;IACD,IAAI,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,uBAAuB,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACpF,CAAC;IACD,OAAO,KAAK,SAAS,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAA;AACvD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA8B;IAClE,MAAM,GAAG,GAAG,OAAO,CAAC,cAAc,IAAI,EAAE,CAAA;IACxC,MAAM,IAAI,GAAG,CAAC,cAAc,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAA;IAEtD,OAAO,KAAK,EAAE,SAA6B,EAAiB,EAAE;QAC7D,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;QAC1C,sEAAsE;QACtE,mEAAmE;QACnE,mEAAmE;QACnE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAEjC,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;YAC1B,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,OAAO,SAAS,CAAC,KAAK,EAAE;YACxD,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC;YAC5B,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC;YACpC,IAAI;YACJ,MAAM,EAAE,UAAU;YAClB,oEAAoE;YACpE,+DAA+D;YAC/D,2DAA2D;YAC3D,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE;SAC5D,CAAC,CAAA;IACH,CAAC,CAAA;AACF,CAAC"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * A hash of everything a run could have changed in its working tree.
3
+ *
4
+ * It exists to answer one question, asked between two attempts at the same
5
+ * verification: **did anything happen since it last failed?** A verify-then-fix
6
+ * loop that re-runs the build after a turn which edited nothing spends a full
7
+ * command execution to learn what a comparison already knew, and does it once
8
+ * per remaining attempt — so a model that has stopped making progress burns
9
+ * the entire budget confirming the same failure.
10
+ *
11
+ * ## What is hashed, and why each part
12
+ *
13
+ * Three sources, because no one of them is complete:
14
+ *
15
+ * 1. **`git status --porcelain`** — which paths differ from the index at all.
16
+ * Cheap, and it catches additions, deletions and mode changes. On its own
17
+ * it is not enough: editing a tracked file that was ALREADY modified
18
+ * leaves the status output byte-identical.
19
+ * 2. **`git diff --binary HEAD`** — the content of every tracked change.
20
+ * `--binary` so an edit to a file git treats as binary is a real diff
21
+ * rather than the constant line `Binary files … differ`, which would make
22
+ * every edit to such a file invisible.
23
+ * 3. **Untracked file contents**, which no `git diff` covers. A new file is
24
+ * named by `status` but its CONTENT is not, so successive edits to a
25
+ * brand-new file would otherwise look like no change at all.
26
+ *
27
+ * ### Symlinks are recorded as their target, not read through
28
+ *
29
+ * Reading a link follows it, so a link repointed from one file to another
30
+ * with identical contents hashes the same — while the thing the workspace
31
+ * actually resolves has changed. The link's target path is the fact that
32
+ * moved, so that is what goes in.
33
+ *
34
+ * ## Failing open, on the cheap side
35
+ *
36
+ * Every uncertainty returns `null`, meaning *no fingerprint*, and a caller
37
+ * that cannot fingerprint re-runs its command. That is the correct direction:
38
+ * the cost of a wrong `null` is one command execution, and the cost of a
39
+ * wrong MATCH is a verification silently skipped — the loop would report
40
+ * "nothing changed" about a workspace that did change, and the model would be
41
+ * told to edit something it had already edited.
42
+ *
43
+ * So: a non-zero exit from any git invocation, a repository with no commits,
44
+ * a timeout, or output past the size cap all produce `null` rather than a
45
+ * partial hash. A truncated diff that hashed successfully would be the worst
46
+ * outcome available here, because two different workspaces truncated at the
47
+ * same point collide.
48
+ */
49
+ import type { CommandOptions, CommandResult } from '../types/execution/index.js';
50
+ /** How a fingerprint runs git. Injected so a test needs no repository. */
51
+ export type FingerprintExec = (command: string, args: string[], options?: CommandOptions) => Promise<CommandResult>;
52
+ /**
53
+ * The three filesystem reads an untracked entry needs.
54
+ *
55
+ * Injectable for one specific reason, written down because a seam that
56
+ * exists only for tests is usually a smell: **creating a symlink requires a
57
+ * privilege that is not granted by default on Windows**, so the symlink rule
58
+ * below — the one that says a repointed link changes the fingerprint even
59
+ * when the bytes behind it do not — cannot be exercised on a developer
60
+ * machine without it. A rule that can only be checked on some machines is a
61
+ * rule nobody checks.
62
+ *
63
+ * The default is `node:fs/promises` and every other test uses it against a
64
+ * real repository, so this is not a fixture standing in for production; it is
65
+ * one branch of one function reached without a privilege.
66
+ */
67
+ export interface FingerprintFs {
68
+ lstat(path: string): Promise<{
69
+ isSymbolicLink(): boolean;
70
+ isFile(): boolean;
71
+ }>;
72
+ readlink(path: string): Promise<string>;
73
+ readFile(path: string): Promise<Buffer>;
74
+ }
75
+ /**
76
+ * Cap on the bytes any single git invocation may produce.
77
+ *
78
+ * Past it the fingerprint is abandoned rather than hashed. A diff big enough
79
+ * to hit this is a diff nobody is going to iterate on anyway, and hashing a
80
+ * clipped one would let two different trees agree.
81
+ */
82
+ export declare const FINGERPRINT_MAX_BYTES: number;
83
+ /** Default deadline per git invocation. */
84
+ export declare const FINGERPRINT_TIMEOUT_MS = 20000;
85
+ export interface WorkspaceFingerprintOptions {
86
+ /** Repository root, or any directory inside it. */
87
+ readonly cwd: string;
88
+ /** How to run git. */
89
+ readonly exec: FingerprintExec;
90
+ /** Per-invocation deadline. See {@link FINGERPRINT_TIMEOUT_MS}. */
91
+ readonly timeoutMs?: number;
92
+ /** See {@link FINGERPRINT_MAX_BYTES}. */
93
+ readonly maxBytes?: number;
94
+ /** Filesystem reads. See {@link FingerprintFs}. */
95
+ readonly fs?: FingerprintFs;
96
+ }
97
+ /**
98
+ * A hash of the working tree's uncommitted state, or `null` when it cannot be
99
+ * established.
100
+ *
101
+ * **`null` is never "unchanged".** It means "I cannot tell", and the caller
102
+ * must treat it as a reason to do the work rather than to skip it.
103
+ */
104
+ export declare function fingerprintWorkspace(options: WorkspaceFingerprintOptions): Promise<string | null>;
105
+ //# sourceMappingURL=workspace-fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-fingerprint.d.ts","sourceRoot":"","sources":["../../src/run/workspace-fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAEhF,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE,cAAc,KACpB,OAAO,CAAC,aAAa,CAAC,CAAA;AAE3B;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,aAAa;IAC7B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,cAAc,IAAI,OAAO,CAAC;QAAC,MAAM,IAAI,OAAO,CAAA;KAAE,CAAC,CAAA;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACvC;AAID;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,QAAkB,CAAA;AAEpD,2CAA2C;AAC3C,eAAO,MAAM,sBAAsB,QAAS,CAAA;AAE5C,MAAM,WAAW,2BAA2B;IAC3C,mDAAmD;IACnD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,sBAAsB;IACtB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,EAAE,CAAC,EAAE,aAAa,CAAA;CAC3B;AAwBD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmDxB"}