wicked-crew 0.7.13 → 0.7.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/delivery-cache.d.ts +135 -0
- package/dist/api/delivery-cache.d.ts.map +1 -0
- package/dist/api/delivery-cache.js +245 -0
- package/dist/api/delivery-cache.js.map +1 -0
- package/dist/api/delivery-index.d.ts +42 -11
- package/dist/api/delivery-index.d.ts.map +1 -1
- package/dist/api/delivery-index.js +104 -52
- package/dist/api/delivery-index.js.map +1 -1
- package/dist/api/endpoint-manifest-live.d.ts.map +1 -1
- package/dist/api/endpoint-manifest-live.js +0 -1
- package/dist/api/endpoint-manifest-live.js.map +1 -1
- package/dist/api/gate-cache.d.ts +24 -0
- package/dist/api/gate-cache.d.ts.map +1 -1
- package/dist/api/gate-cache.js +35 -0
- package/dist/api/gate-cache.js.map +1 -1
- package/dist/api/routes.d.ts +14 -7
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +60 -13
- package/dist/api/routes.js.map +1 -1
- package/dist/api/seat-health.d.ts +0 -25
- package/dist/api/seat-health.d.ts.map +1 -1
- package/dist/api/seat-health.js +6 -81
- package/dist/api/seat-health.js.map +1 -1
- package/dist/api/server.d.ts +18 -15
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +87 -36
- package/dist/api/server.js.map +1 -1
- package/dist/api/stall-watchdog.d.ts +40 -3
- package/dist/api/stall-watchdog.d.ts.map +1 -1
- package/dist/api/stall-watchdog.js +94 -15
- package/dist/api/stall-watchdog.js.map +1 -1
- package/dist/campaigns/rollup.d.ts +3 -0
- package/dist/campaigns/rollup.d.ts.map +1 -1
- package/dist/campaigns/rollup.js +17 -2
- package/dist/campaigns/rollup.js.map +1 -1
- package/dist/campaigns/routes.d.ts +4 -4
- package/dist/core/types.d.ts +20 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +20 -1
- package/dist/core/types.js.map +1 -1
- package/endpoint-manifest.json +3 -2
- package/package.json +2 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The background delivery-derivation cache — `GET /runs` p99 (perf finding #2).
|
|
3
|
+
*
|
|
4
|
+
* The vacuity split (crew#311) put two git probes per would-be-stranded run on the run DTOs'
|
|
5
|
+
* assembly path. On the LIST route that is a fan-out: ~20 terminal worktrees × (status + log)
|
|
6
|
+
* with a 10s timeout each, contending with the active worker's own git — measured p99 18.7s /
|
|
7
|
+
* max 18.9s while p50 stayed 182ms and `GET /runs/:id` (the same actor read) stayed 10ms in the
|
|
8
|
+
* exact same windows. The TTL memo bounded the cost per window but not the worst case: the
|
|
9
|
+
* unlucky poll that lands after expiry pays every probe, serially contended.
|
|
10
|
+
*
|
|
11
|
+
* This cache inverts the ownership: derivation runs in the BACKGROUND and the request path only
|
|
12
|
+
* ever reads a map. Concretely —
|
|
13
|
+
*
|
|
14
|
+
* - a sweep every {@link DELIVERY_SWEEP_INTERVAL_MS} (= `WORKTREE_CLEAN_TTL_MS`, so labels
|
|
15
|
+
* stay exactly as fresh as the memo already promised) re-derives every candidate run:
|
|
16
|
+
* COMPLETED + repo-scoped + not already delivered — the only shape whose full derivation
|
|
17
|
+
* can differ from the stat-only one;
|
|
18
|
+
* - a terminal-frame warm ({@link warm}) derives a run ONCE when it reaches terminal state,
|
|
19
|
+
* so a finished run's label heals in seconds, not at the next tick;
|
|
20
|
+
* - the request path calls {@link read}: a hit answers the derived state; a miss degrades to
|
|
21
|
+
* the stat-only tri-state (`deliveryStateOf` — the pre-crew#311 stranded/none label) for at
|
|
22
|
+
* most one tick and NEVER blocks on (or triggers) a derivation. The list path spawns no git,
|
|
23
|
+
* ever.
|
|
24
|
+
*
|
|
25
|
+
* The sweep pool is capped at {@link MAX_CONCURRENT_DERIVATIONS} with an in-flight guard per
|
|
26
|
+
* run, so the background layer can never storm git the way the request fan-out did. Probes are
|
|
27
|
+
* injected (the shared TTL-memoized git probes in production), so the campaigns rollup — which
|
|
28
|
+
* still derives on-request over the SAME probe memo — rides warm entries the sweeper refreshed.
|
|
29
|
+
*
|
|
30
|
+
* FAILURE RULE (PR #435 review): a derivation whose probes could not answer
|
|
31
|
+
* (`VacuityProbeUnavailable` — git unspawnable, timeout, a worktree racing the engine's
|
|
32
|
+
* terminal reap) is NEVER recorded — absence of an answer is not a verdict. The entry stays
|
|
33
|
+
* untouched (miss ⇒ the honest stat-only degrade) and the run re-derives on a short backoff,
|
|
34
|
+
* so a raced terminal-frame warm heals in seconds even where the sweep is off.
|
|
35
|
+
*/
|
|
36
|
+
import type { AgentSession, SessionView } from '../core/types.js';
|
|
37
|
+
import { type DeliveryState, type VacuityProbes } from './delivery-index.js';
|
|
38
|
+
/** The session facts the derivation reads — the same pick `deliveryStateWithVacuity` takes. */
|
|
39
|
+
type SessionFacts = Pick<AgentSession, 'id' | 'status' | 'repo_ref' | 'workdir'>;
|
|
40
|
+
/** One tick = the trust window the per-probe memo already grants, so moving derivation off the
|
|
41
|
+
* request path changes WHO pays, never how stale a label may read. */
|
|
42
|
+
export declare const DELIVERY_SWEEP_INTERVAL_MS = 30000;
|
|
43
|
+
/** The background pool cap: at most this many derivations (each up to two git subprocesses) run
|
|
44
|
+
* at once, so a 50-run backlog drains as a trickle instead of a spike that contends with the
|
|
45
|
+
* active worker's git — the exact contention the request-path fan-out exhibited. */
|
|
46
|
+
export declare const MAX_CONCURRENT_DERIVATIONS = 3;
|
|
47
|
+
/** First retry delay after a derivation whose probes could not answer; doubles per consecutive
|
|
48
|
+
* failure up to the sweep cadence. The terminal-frame warm fires at the most turbulent instant
|
|
49
|
+
* of a run's life (the engine's terminal reap runs concurrently), so a raced first derivation
|
|
50
|
+
* is EXPECTED — the retry is what heals it in seconds even where no sweep ticks (test-built
|
|
51
|
+
* daemons keep the interval off). */
|
|
52
|
+
export declare const DERIVATION_RETRY_BASE_MS = 1000;
|
|
53
|
+
export interface DeliveryDerivationCacheDeps {
|
|
54
|
+
/** The candidate set per sweep — the same ~10ms actor read the routes use. */
|
|
55
|
+
listViews: () => Promise<SessionView[]>;
|
|
56
|
+
/** The probes behind 'stranded'/'vacuous'. In the daemon these are the SAME TTL-memoized git
|
|
57
|
+
* probes the campaigns rollup uses; only `worktreeExists` (one stat, no git) is ever invoked
|
|
58
|
+
* on the request path — the git pair runs exclusively under this cache's pool. */
|
|
59
|
+
probes: VacuityProbes;
|
|
60
|
+
/** A run the `DeliveryIndex` already answers needs no derivation — the read path
|
|
61
|
+
* short-circuits on the recorded URL before it ever consults this cache. */
|
|
62
|
+
isDelivered: (runId: string) => boolean;
|
|
63
|
+
/** Retry-backoff floor after a failed derivation (tests shorten it). */
|
|
64
|
+
retryBaseMs?: number;
|
|
65
|
+
log?: (msg: string) => void;
|
|
66
|
+
/** ERROR-level channel for a NON-probe derivation throw — a defect, not weather (the daemon
|
|
67
|
+
* wires `app.log.error`, which the diagnostics ring also captures). Falls back to `log`. */
|
|
68
|
+
logError?: (msg: string) => void;
|
|
69
|
+
}
|
|
70
|
+
export declare class DeliveryDerivationCache {
|
|
71
|
+
private readonly deps;
|
|
72
|
+
/** runId → the last full derivation. Entries exist only for candidate runs; the sweep prunes
|
|
73
|
+
* ids the store no longer serves and runs the index has since recorded as delivered. */
|
|
74
|
+
private readonly derived;
|
|
75
|
+
/** The per-run in-flight guard: a run being derived is never enqueued a second time — a
|
|
76
|
+
* sweep overlapping a terminal-frame warm folds into ONE derivation, not two git pairs. */
|
|
77
|
+
private readonly inFlight;
|
|
78
|
+
/** The pool: `active` counts held permits, `waiting` holds resolvers that each RECEIVE a
|
|
79
|
+
* permit on release (the count transfers — it never dips below the true concurrency). */
|
|
80
|
+
private active;
|
|
81
|
+
private readonly waiting;
|
|
82
|
+
private sweeping;
|
|
83
|
+
private timer;
|
|
84
|
+
/** Per-run backoff after a failed derivation (reset on the next honest verdict). */
|
|
85
|
+
private readonly retryDelayMs;
|
|
86
|
+
/** Pending retry timers — unref'd, and cleared on {@link stop} so a closed daemon (or a
|
|
87
|
+
* test's torn-down server) never re-derives into the void. */
|
|
88
|
+
private readonly retryTimers;
|
|
89
|
+
/** Set by {@link stop}: a derivation still in flight when the daemon closes must not arm a
|
|
90
|
+
* NEW retry timer after the stop already swept the set (Copilot on #435). */
|
|
91
|
+
private stopped;
|
|
92
|
+
constructor(deps: DeliveryDerivationCacheDeps);
|
|
93
|
+
/**
|
|
94
|
+
* The request path. Synchronous by design — a cache hit answers the background-derived state;
|
|
95
|
+
* a miss answers the stat-only tri-state (one `existsSync`, no git) and does NOT enqueue a
|
|
96
|
+
* derivation: scheduling belongs to the sweep and the terminal warm alone, so a request can
|
|
97
|
+
* never fan a poll out into subprocess spawns, however cold the cache.
|
|
98
|
+
*
|
|
99
|
+
* Non-candidate shapes skip the cache entirely: their stat answer IS the full derivation, and
|
|
100
|
+
* skipping keeps a (theoretical) stale entry from ever outliving its run's candidacy.
|
|
101
|
+
*/
|
|
102
|
+
read(session: SessionFacts): DeliveryState;
|
|
103
|
+
/**
|
|
104
|
+
* The terminal-frame warm: derive this run once, now, so a just-finished run's label heals in
|
|
105
|
+
* probe-time instead of waiting for the next tick. Fired from the daemon's CoreEvent
|
|
106
|
+
* subscription AFTER `resolveRunDelivery` settles — a just-recorded PR URL then skips the
|
|
107
|
+
* derivation via `isDelivered`. Best-effort like everything on that hook: never throws.
|
|
108
|
+
*/
|
|
109
|
+
warm(runId: string): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* One background pass: prune entries the store no longer serves (or the index has since
|
|
112
|
+
* recorded as delivered), then re-derive every candidate through the capped pool. Re-entrant
|
|
113
|
+
* calls fold — a sweep still draining when the next tick fires is not stacked, so a slow git
|
|
114
|
+
* day degrades to a slower refresh, never to a growing backlog.
|
|
115
|
+
*/
|
|
116
|
+
sweep(): Promise<void>;
|
|
117
|
+
/** Arm the tick (and sweep once immediately, so a restarted daemon's labels heal in seconds,
|
|
118
|
+
* not at +30s). Unref'd — the interval must never hold the process open. */
|
|
119
|
+
start(intervalMs?: number): void;
|
|
120
|
+
stop(): void;
|
|
121
|
+
/** Enqueue one run's full derivation (in-flight-guarded, pool-capped).
|
|
122
|
+
*
|
|
123
|
+
* THE LOAD-BEARING RULE (PR #435 review): a derivation whose probes FAILED is never cached
|
|
124
|
+
* as if it were an honest answer. The production probes throw `VacuityProbeUnavailable` when
|
|
125
|
+
* git could not answer (a raced teardown, a timeout, a load spike) — recording that as
|
|
126
|
+
* 'stranded' pinned a WRONG label on vacuous runs, forever where no sweep ticks. On failure
|
|
127
|
+
* the entry is left untouched (a miss keeps the honest stat-only degrade; a previous honest
|
|
128
|
+
* verdict stands) and the run re-derives after a short backoff, doubling up to the sweep
|
|
129
|
+
* cadence — the in-flight guard folds overlapping schedules, so retries can never storm. */
|
|
130
|
+
private schedule;
|
|
131
|
+
private acquire;
|
|
132
|
+
private release;
|
|
133
|
+
}
|
|
134
|
+
export {};
|
|
135
|
+
//# sourceMappingURL=delivery-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-cache.d.ts","sourceRoot":"","sources":["../../src/api/delivery-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAE7B,+FAA+F;AAC/F,KAAK,YAAY,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC,CAAC;AAEjF;uEACuE;AACvE,eAAO,MAAM,0BAA0B,QAAwB,CAAC;AAEhE;;qFAEqF;AACrF,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;;;sCAIsC;AACtC,eAAO,MAAM,wBAAwB,OAAQ,CAAC;AAE9C,MAAM,WAAW,2BAA2B;IAC1C,8EAA8E;IAC9E,SAAS,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACxC;;uFAEmF;IACnF,MAAM,EAAE,aAAa,CAAC;IACtB;iFAC6E;IAC7E,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B;iGAC6F;IAC7F,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AASD,qBAAa,uBAAuB;IAsBtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IArBjC;6FACyF;IACzF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D;gGAC4F;IAC5F,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D;8FAC0F;IAC1F,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAA+C;IAC5D,oFAAoF;IACpF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;IAC1D;mEAC+D;IAC/D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4C;IACxE;kFAC8E;IAC9E,OAAO,CAAC,OAAO,CAAS;gBAEK,IAAI,EAAE,2BAA2B;IAE9D;;;;;;;;OAQG;IACH,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa;IAQ1C;;;;;OAKG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBxC;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB5B;iFAC6E;IAC7E,KAAK,CAAC,UAAU,GAAE,MAAmC,GAAG,IAAI;IAU5D,IAAI,IAAI,IAAI;IAUZ;;;;;;;;iGAQ6F;IAC7F,OAAO,CAAC,QAAQ;YA+DF,OAAO;IAQrB,OAAO,CAAC,OAAO;CAKhB"}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The background delivery-derivation cache — `GET /runs` p99 (perf finding #2).
|
|
3
|
+
*
|
|
4
|
+
* The vacuity split (crew#311) put two git probes per would-be-stranded run on the run DTOs'
|
|
5
|
+
* assembly path. On the LIST route that is a fan-out: ~20 terminal worktrees × (status + log)
|
|
6
|
+
* with a 10s timeout each, contending with the active worker's own git — measured p99 18.7s /
|
|
7
|
+
* max 18.9s while p50 stayed 182ms and `GET /runs/:id` (the same actor read) stayed 10ms in the
|
|
8
|
+
* exact same windows. The TTL memo bounded the cost per window but not the worst case: the
|
|
9
|
+
* unlucky poll that lands after expiry pays every probe, serially contended.
|
|
10
|
+
*
|
|
11
|
+
* This cache inverts the ownership: derivation runs in the BACKGROUND and the request path only
|
|
12
|
+
* ever reads a map. Concretely —
|
|
13
|
+
*
|
|
14
|
+
* - a sweep every {@link DELIVERY_SWEEP_INTERVAL_MS} (= `WORKTREE_CLEAN_TTL_MS`, so labels
|
|
15
|
+
* stay exactly as fresh as the memo already promised) re-derives every candidate run:
|
|
16
|
+
* COMPLETED + repo-scoped + not already delivered — the only shape whose full derivation
|
|
17
|
+
* can differ from the stat-only one;
|
|
18
|
+
* - a terminal-frame warm ({@link warm}) derives a run ONCE when it reaches terminal state,
|
|
19
|
+
* so a finished run's label heals in seconds, not at the next tick;
|
|
20
|
+
* - the request path calls {@link read}: a hit answers the derived state; a miss degrades to
|
|
21
|
+
* the stat-only tri-state (`deliveryStateOf` — the pre-crew#311 stranded/none label) for at
|
|
22
|
+
* most one tick and NEVER blocks on (or triggers) a derivation. The list path spawns no git,
|
|
23
|
+
* ever.
|
|
24
|
+
*
|
|
25
|
+
* The sweep pool is capped at {@link MAX_CONCURRENT_DERIVATIONS} with an in-flight guard per
|
|
26
|
+
* run, so the background layer can never storm git the way the request fan-out did. Probes are
|
|
27
|
+
* injected (the shared TTL-memoized git probes in production), so the campaigns rollup — which
|
|
28
|
+
* still derives on-request over the SAME probe memo — rides warm entries the sweeper refreshed.
|
|
29
|
+
*
|
|
30
|
+
* FAILURE RULE (PR #435 review): a derivation whose probes could not answer
|
|
31
|
+
* (`VacuityProbeUnavailable` — git unspawnable, timeout, a worktree racing the engine's
|
|
32
|
+
* terminal reap) is NEVER recorded — absence of an answer is not a verdict. The entry stays
|
|
33
|
+
* untouched (miss ⇒ the honest stat-only degrade) and the run re-derives on a short backoff,
|
|
34
|
+
* so a raced terminal-frame warm heals in seconds even where the sweep is off.
|
|
35
|
+
*/
|
|
36
|
+
import { deliveryStateOf, deliveryStateWithVacuity, VacuityProbeUnavailable, WORKTREE_CLEAN_TTL_MS, } from './delivery-index.js';
|
|
37
|
+
/** One tick = the trust window the per-probe memo already grants, so moving derivation off the
|
|
38
|
+
* request path changes WHO pays, never how stale a label may read. */
|
|
39
|
+
export const DELIVERY_SWEEP_INTERVAL_MS = WORKTREE_CLEAN_TTL_MS;
|
|
40
|
+
/** The background pool cap: at most this many derivations (each up to two git subprocesses) run
|
|
41
|
+
* at once, so a 50-run backlog drains as a trickle instead of a spike that contends with the
|
|
42
|
+
* active worker's git — the exact contention the request-path fan-out exhibited. */
|
|
43
|
+
export const MAX_CONCURRENT_DERIVATIONS = 3;
|
|
44
|
+
/** First retry delay after a derivation whose probes could not answer; doubles per consecutive
|
|
45
|
+
* failure up to the sweep cadence. The terminal-frame warm fires at the most turbulent instant
|
|
46
|
+
* of a run's life (the engine's terminal reap runs concurrently), so a raced first derivation
|
|
47
|
+
* is EXPECTED — the retry is what heals it in seconds even where no sweep ticks (test-built
|
|
48
|
+
* daemons keep the interval off). */
|
|
49
|
+
export const DERIVATION_RETRY_BASE_MS = 1_000;
|
|
50
|
+
/** Only a COMPLETED repo-scoped run's full derivation can differ from the stat-only tri-state
|
|
51
|
+
* (`deliveryStateWithVacuity` answers `deliveryStateOf` verbatim, git-free, for every other
|
|
52
|
+
* shape) — so only these runs are ever derived or cached. */
|
|
53
|
+
function isCandidate(session) {
|
|
54
|
+
return session.status === 'completed' && session.repo_ref != null;
|
|
55
|
+
}
|
|
56
|
+
export class DeliveryDerivationCache {
|
|
57
|
+
deps;
|
|
58
|
+
/** runId → the last full derivation. Entries exist only for candidate runs; the sweep prunes
|
|
59
|
+
* ids the store no longer serves and runs the index has since recorded as delivered. */
|
|
60
|
+
derived = new Map();
|
|
61
|
+
/** The per-run in-flight guard: a run being derived is never enqueued a second time — a
|
|
62
|
+
* sweep overlapping a terminal-frame warm folds into ONE derivation, not two git pairs. */
|
|
63
|
+
inFlight = new Map();
|
|
64
|
+
/** The pool: `active` counts held permits, `waiting` holds resolvers that each RECEIVE a
|
|
65
|
+
* permit on release (the count transfers — it never dips below the true concurrency). */
|
|
66
|
+
active = 0;
|
|
67
|
+
waiting = [];
|
|
68
|
+
sweeping = false;
|
|
69
|
+
timer = null;
|
|
70
|
+
/** Per-run backoff after a failed derivation (reset on the next honest verdict). */
|
|
71
|
+
retryDelayMs = new Map();
|
|
72
|
+
/** Pending retry timers — unref'd, and cleared on {@link stop} so a closed daemon (or a
|
|
73
|
+
* test's torn-down server) never re-derives into the void. */
|
|
74
|
+
retryTimers = new Set();
|
|
75
|
+
/** Set by {@link stop}: a derivation still in flight when the daemon closes must not arm a
|
|
76
|
+
* NEW retry timer after the stop already swept the set (Copilot on #435). */
|
|
77
|
+
stopped = false;
|
|
78
|
+
constructor(deps) {
|
|
79
|
+
this.deps = deps;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The request path. Synchronous by design — a cache hit answers the background-derived state;
|
|
83
|
+
* a miss answers the stat-only tri-state (one `existsSync`, no git) and does NOT enqueue a
|
|
84
|
+
* derivation: scheduling belongs to the sweep and the terminal warm alone, so a request can
|
|
85
|
+
* never fan a poll out into subprocess spawns, however cold the cache.
|
|
86
|
+
*
|
|
87
|
+
* Non-candidate shapes skip the cache entirely: their stat answer IS the full derivation, and
|
|
88
|
+
* skipping keeps a (theoretical) stale entry from ever outliving its run's candidacy.
|
|
89
|
+
*/
|
|
90
|
+
read(session) {
|
|
91
|
+
if (isCandidate(session)) {
|
|
92
|
+
const hit = this.derived.get(session.id);
|
|
93
|
+
if (hit !== undefined)
|
|
94
|
+
return hit;
|
|
95
|
+
}
|
|
96
|
+
return deliveryStateOf(session, undefined, this.deps.probes.worktreeExists);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The terminal-frame warm: derive this run once, now, so a just-finished run's label heals in
|
|
100
|
+
* probe-time instead of waiting for the next tick. Fired from the daemon's CoreEvent
|
|
101
|
+
* subscription AFTER `resolveRunDelivery` settles — a just-recorded PR URL then skips the
|
|
102
|
+
* derivation via `isDelivered`. Best-effort like everything on that hook: never throws.
|
|
103
|
+
*/
|
|
104
|
+
async warm(runId) {
|
|
105
|
+
try {
|
|
106
|
+
if (this.deps.isDelivered(runId))
|
|
107
|
+
return;
|
|
108
|
+
const views = await this.deps.listViews();
|
|
109
|
+
const view = views.find((v) => v.session.id === runId);
|
|
110
|
+
if (view === undefined || !isCandidate(view.session))
|
|
111
|
+
return;
|
|
112
|
+
await this.schedule(view.session);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
this.deps.log?.(`[runs] delivery-derivation warm for ${runId} failed (label reads stat-only until the next sweep): ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* One background pass: prune entries the store no longer serves (or the index has since
|
|
120
|
+
* recorded as delivered), then re-derive every candidate through the capped pool. Re-entrant
|
|
121
|
+
* calls fold — a sweep still draining when the next tick fires is not stacked, so a slow git
|
|
122
|
+
* day degrades to a slower refresh, never to a growing backlog.
|
|
123
|
+
*/
|
|
124
|
+
async sweep() {
|
|
125
|
+
if (this.sweeping)
|
|
126
|
+
return;
|
|
127
|
+
this.sweeping = true;
|
|
128
|
+
try {
|
|
129
|
+
const views = await this.deps.listViews();
|
|
130
|
+
const alive = new Set(views.map((v) => v.session.id));
|
|
131
|
+
for (const id of this.derived.keys()) {
|
|
132
|
+
if (!alive.has(id) || this.deps.isDelivered(id))
|
|
133
|
+
this.derived.delete(id);
|
|
134
|
+
}
|
|
135
|
+
await Promise.all(views
|
|
136
|
+
.filter((v) => isCandidate(v.session) && !this.deps.isDelivered(v.session.id))
|
|
137
|
+
.map((v) => this.schedule(v.session)));
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
this.deps.log?.(`[runs] delivery-derivation sweep failed (labels degrade to the stat-only read until the next tick): ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
this.sweeping = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Arm the tick (and sweep once immediately, so a restarted daemon's labels heal in seconds,
|
|
147
|
+
* not at +30s). Unref'd — the interval must never hold the process open. */
|
|
148
|
+
start(intervalMs = DELIVERY_SWEEP_INTERVAL_MS) {
|
|
149
|
+
if (this.timer !== null)
|
|
150
|
+
return;
|
|
151
|
+
this.stopped = false;
|
|
152
|
+
void this.sweep();
|
|
153
|
+
this.timer = setInterval(() => {
|
|
154
|
+
void this.sweep();
|
|
155
|
+
}, intervalMs);
|
|
156
|
+
this.timer.unref?.();
|
|
157
|
+
}
|
|
158
|
+
stop() {
|
|
159
|
+
this.stopped = true; // in-flight derivations must not arm retries past this point
|
|
160
|
+
if (this.timer !== null) {
|
|
161
|
+
clearInterval(this.timer);
|
|
162
|
+
this.timer = null;
|
|
163
|
+
}
|
|
164
|
+
for (const t of this.retryTimers)
|
|
165
|
+
clearTimeout(t);
|
|
166
|
+
this.retryTimers.clear();
|
|
167
|
+
}
|
|
168
|
+
/** Enqueue one run's full derivation (in-flight-guarded, pool-capped).
|
|
169
|
+
*
|
|
170
|
+
* THE LOAD-BEARING RULE (PR #435 review): a derivation whose probes FAILED is never cached
|
|
171
|
+
* as if it were an honest answer. The production probes throw `VacuityProbeUnavailable` when
|
|
172
|
+
* git could not answer (a raced teardown, a timeout, a load spike) — recording that as
|
|
173
|
+
* 'stranded' pinned a WRONG label on vacuous runs, forever where no sweep ticks. On failure
|
|
174
|
+
* the entry is left untouched (a miss keeps the honest stat-only degrade; a previous honest
|
|
175
|
+
* verdict stands) and the run re-derives after a short backoff, doubling up to the sweep
|
|
176
|
+
* cadence — the in-flight guard folds overlapping schedules, so retries can never storm. */
|
|
177
|
+
schedule(session) {
|
|
178
|
+
const pending = this.inFlight.get(session.id);
|
|
179
|
+
if (pending !== undefined)
|
|
180
|
+
return pending;
|
|
181
|
+
const job = (async () => {
|
|
182
|
+
await this.acquire();
|
|
183
|
+
try {
|
|
184
|
+
const state = await deliveryStateWithVacuity(session, undefined, this.deps.probes);
|
|
185
|
+
// A 'stranded' verdict over a worktree that VANISHED mid-derivation is not an answer
|
|
186
|
+
// either: the engine's terminal reap races the terminal-frame warm, and a half-removed
|
|
187
|
+
// tree can read "dirty" (deleted paths in `git status`) the instant before its .git
|
|
188
|
+
// link disappears. One stat re-check turns that race into a retry, not a pinned label.
|
|
189
|
+
if (state.delivery === 'stranded' &&
|
|
190
|
+
typeof session.workdir === 'string' &&
|
|
191
|
+
session.workdir !== '' &&
|
|
192
|
+
!this.deps.probes.worktreeExists(session.workdir)) {
|
|
193
|
+
throw new VacuityProbeUnavailable(`worktree ${session.workdir} vanished mid-derivation`, 'raced the terminal reap');
|
|
194
|
+
}
|
|
195
|
+
this.derived.set(session.id, state);
|
|
196
|
+
this.retryDelayMs.delete(session.id);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
if (!(err instanceof VacuityProbeUnavailable)) {
|
|
200
|
+
// A NON-probe throw is a DEFECT (a bug in the derivation, a miswired dependency) —
|
|
201
|
+
// not weather. Hiding it inside the quiet backoff loop would spray retry noise over
|
|
202
|
+
// a logic error (Copilot on #435), so: record nothing (the honest stat-only degrade
|
|
203
|
+
// stands), say it at ERROR level, and do NOT retry — in the daemon the next sweep
|
|
204
|
+
// hits it again and keeps it loud, never silently green.
|
|
205
|
+
(this.deps.logError ?? this.deps.log)?.(`[runs] DEFECT: delivery derivation for ${session.id} threw a non-probe error (not retried; label reads stat-only): ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const delay = this.retryDelayMs.get(session.id) ?? this.deps.retryBaseMs ?? DERIVATION_RETRY_BASE_MS;
|
|
209
|
+
this.retryDelayMs.set(session.id, Math.min(delay * 2, DELIVERY_SWEEP_INTERVAL_MS));
|
|
210
|
+
this.deps.log?.(`[runs] delivery derivation for ${session.id} could not answer (label reads stat-only; ${this.stopped ? 'cache stopped — not retried' : `retrying in ${delay}ms`}): ${err.message}`);
|
|
211
|
+
// A derivation still in flight when stop() ran must not arm a timer the sweep-away
|
|
212
|
+
// already missed — a closed daemon never re-derives into the void.
|
|
213
|
+
if (this.stopped)
|
|
214
|
+
return;
|
|
215
|
+
const timer = setTimeout(() => {
|
|
216
|
+
this.retryTimers.delete(timer);
|
|
217
|
+
void this.warm(session.id);
|
|
218
|
+
}, delay);
|
|
219
|
+
timer.unref?.();
|
|
220
|
+
this.retryTimers.add(timer);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
this.release();
|
|
224
|
+
this.inFlight.delete(session.id);
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
this.inFlight.set(session.id, job);
|
|
228
|
+
return job;
|
|
229
|
+
}
|
|
230
|
+
async acquire() {
|
|
231
|
+
if (this.active < MAX_CONCURRENT_DERIVATIONS) {
|
|
232
|
+
this.active += 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
await new Promise((resolve) => this.waiting.push(resolve));
|
|
236
|
+
}
|
|
237
|
+
release() {
|
|
238
|
+
const next = this.waiting.shift();
|
|
239
|
+
if (next !== undefined)
|
|
240
|
+
next(); // hand the permit over — `active` is unchanged
|
|
241
|
+
else
|
|
242
|
+
this.active -= 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=delivery-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-cache.js","sourceRoot":"","sources":["../../src/api/delivery-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -46,7 +46,14 @@ export interface DeliveryState {
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function deliveryStateOf(session: Pick<AgentSession, 'status' | 'repo_ref' | 'workdir'>, url: string | undefined, worktreeExists: (path: string) => boolean): DeliveryState;
|
|
48
48
|
/** The probes behind the `'vacuous'` derivation (crew#311) — injected so route tests can pin the
|
|
49
|
-
* derivation without staging real repos; production wires the memoized git probes below.
|
|
49
|
+
* derivation without staging real repos; production wires the memoized git probes below.
|
|
50
|
+
*
|
|
51
|
+
* FAILURE CONTRACT (PR #435 review): a production git probe THROWS
|
|
52
|
+
* {@link VacuityProbeUnavailable} when git could not answer — spawn failure, timeout, a
|
|
53
|
+
* worktree or repo racing teardown, a non-git directory. That is the ABSENCE of an answer, not
|
|
54
|
+
* a verdict, and the caller owns the degrade: the delivery cache retries WITHOUT recording
|
|
55
|
+
* (never caching a failed probe as an honest label), and the campaigns rollup serves that one
|
|
56
|
+
* request the stat-only tri-state. Injected test probes return plain booleans. */
|
|
50
57
|
export interface VacuityProbes {
|
|
51
58
|
/** One stat — the `'stranded'` gate (crew#393; `fs.existsSync` in production). */
|
|
52
59
|
worktreeExists: (path: string) => boolean;
|
|
@@ -56,6 +63,17 @@ export interface VacuityProbes {
|
|
|
56
63
|
* commits of its own (or no longer exists) — the reaped-worktree half of vacuity. */
|
|
57
64
|
runBranchIsEmpty: (repoRef: string, runId: string) => Promise<boolean>;
|
|
58
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Thrown by the production probes when git COULD NOT ANSWER — infrastructure absence, not a
|
|
68
|
+
* verdict. The old behavior folded this into `false` ("not clean" / "not empty"), which was
|
|
69
|
+
* safe while every consumer re-probed within a TTL, but the delivery cache records derivations:
|
|
70
|
+
* caching a raced probe's `false` pinned a WRONG 'stranded' on a vacuous run (forever, where no
|
|
71
|
+
* sweep ticks). Distinguishing absence from verdict at the probe layer is what lets the cache
|
|
72
|
+
* refuse to record it.
|
|
73
|
+
*/
|
|
74
|
+
export declare class VacuityProbeUnavailable extends Error {
|
|
75
|
+
constructor(what: string, cause: unknown);
|
|
76
|
+
}
|
|
59
77
|
/**
|
|
60
78
|
* The delivery derivation WITH the vacuity split (crew#311) — the async refinement over
|
|
61
79
|
* {@link deliveryStateOf} the run DTOs actually use.
|
|
@@ -73,26 +91,36 @@ export interface VacuityProbes {
|
|
|
73
91
|
* work anywhere, while "reaped tree + commits on the branch" is landed work and stays
|
|
74
92
|
* `'none'` (nothing left in a worktree to lift).
|
|
75
93
|
*
|
|
76
|
-
* Both shapes now read `'vacuous'`, and only on POSITIVE reads
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
94
|
+
* Both shapes now read `'vacuous'`, and only on POSITIVE reads. A missing repo RECORD still
|
|
95
|
+
* reads `false` from the branch probe (a permanent state — the label honestly falls back to
|
|
96
|
+
* `'none'`), but a git failure now PROPAGATES as {@link VacuityProbeUnavailable} instead of
|
|
97
|
+
* silently keeping the pre-existing label: the caller owns the degrade (see the failure
|
|
98
|
+
* contract on {@link VacuityProbes}). (One honest edge: an operator who hand-deleted a
|
|
99
|
+
* completed run's branch has made its work unreachable — it reads vacuous, which is what the
|
|
100
|
+
* run now amounts to.)
|
|
81
101
|
*/
|
|
82
102
|
export declare function deliveryStateWithVacuity(session: Pick<AgentSession, 'id' | 'status' | 'repo_ref' | 'workdir'>, url: string | undefined, probes: VacuityProbes): Promise<DeliveryState>;
|
|
83
103
|
/** How long a per-worktree cleanliness read is trusted before the next request re-probes. A
|
|
84
104
|
* terminal run's worktree only changes under operator hands, and `GET /runs` is polled — an
|
|
85
105
|
* unmemoized probe would be two git subprocesses per stranded run per poll. */
|
|
86
106
|
export declare const WORKTREE_CLEAN_TTL_MS = 30000;
|
|
107
|
+
/** How long a FAILED probe ({@link VacuityProbeUnavailable}) is memoized before a real
|
|
108
|
+
* re-probe. Deliberately short: a failure is usually a raced teardown or a load spike, and
|
|
109
|
+
* the delivery cache's retry needs a genuine re-probe in ~a second — but a request-path
|
|
110
|
+
* consumer (the campaigns rollup) polling over a persistently broken repo must not spawn two
|
|
111
|
+
* 10s-timeout gits per poll tick either. */
|
|
112
|
+
export declare const PROBE_FAILURE_TTL_MS = 1000;
|
|
87
113
|
/**
|
|
88
114
|
* The production `worktreeIsClean` probe: the SAME two read-only instruments as the engine's
|
|
89
115
|
* `worktree_is_clean` — uncommitted paths (`git status --porcelain`, untracked included) and
|
|
90
116
|
* run-branch-only commits (`git log --oneline HEAD --not --exclude=wicked/* --branches`) —
|
|
91
117
|
* memoized per worktree path for {@link WORKTREE_CLEAN_TTL_MS}.
|
|
92
118
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
119
|
+
* A verdict comes only from git ANSWERING: exit 0 with empty/non-empty output. Any failure —
|
|
120
|
+
* git unspawnable, a timeout kill, a 128 "not a git repository", ENOENT from a worktree racing
|
|
121
|
+
* the terminal reap — throws {@link VacuityProbeUnavailable} (memoized for
|
|
122
|
+
* {@link PROBE_FAILURE_TTL_MS}) so no consumer can mistake absence for "not clean" and pin a
|
|
123
|
+
* wrong 'stranded' (PR #435 review). Callers degrade per the {@link VacuityProbes} contract.
|
|
96
124
|
*/
|
|
97
125
|
export declare function gitWorktreeIsClean(ttlMs?: number, now?: () => number): (path: string) => Promise<boolean>;
|
|
98
126
|
/**
|
|
@@ -102,8 +130,11 @@ export declare function gitWorktreeIsClean(ttlMs?: number, now?: () => number):
|
|
|
102
130
|
* `refs/heads/wicked/<runId>` no longer exists in the run's repo, or it exists and
|
|
103
131
|
* `git log <branch> --not --exclude=wicked/* --branches` is empty (the branch carries no commit
|
|
104
132
|
* of its own — the exact second instrument of the engine's `worktree_is_clean`, aimed at the
|
|
105
|
-
* branch instead of a checkout).
|
|
106
|
-
*
|
|
133
|
+
* branch instead of a checkout). A repo the registry no longer RESOLVES reads FALSE — a
|
|
134
|
+
* permanent state, the label honestly falls back to `'none'` — while a git failure or a
|
|
135
|
+
* registry read failure throws {@link VacuityProbeUnavailable} (absence of an answer, PR #435
|
|
136
|
+
* review), memoized for {@link PROBE_FAILURE_TTL_MS}; callers degrade per the
|
|
137
|
+
* {@link VacuityProbes} contract.
|
|
107
138
|
*
|
|
108
139
|
* `resolveRepoRoot` is injected (routes wire it to the adapter's repo registry) and its answer
|
|
109
140
|
* rides the same TTL memo as the verdict, so a `GET /runs` poll costs at most one registry read
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delivery-index.d.ts","sourceRoot":"","sources":["../../src/api/delivery-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAI5E,kGAAkG;AAClG,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,WAAW,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;IACxD,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC,EAC9D,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACxC,aAAa,CAYf;AAED;
|
|
1
|
+
{"version":3,"file":"delivery-index.d.ts","sourceRoot":"","sources":["../../src/api/delivery-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAI5E,kGAAkG;AAClG,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,WAAW,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;IACxD,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC,EAC9D,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACxC,aAAa,CAYf;AAED;;;;;;;;mFAQmF;AACnF,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,2FAA2F;IAC3F,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD;0FACsF;IACtF,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxE;AAED;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAIzC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC,EACrE,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,aAAa,CAAC,CAiBxB;AAED;;gFAEgF;AAChF,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C;;;;6CAI6C;AAC7C,eAAO,MAAM,oBAAoB,OAAQ,CAAC;AAiC1C;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,GAAE,MAA8B,EACrC,GAAG,GAAE,MAAM,MAAiB,GAC3B,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAyBpC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,EACjE,KAAK,GAAE,MAA8B,EACrC,GAAG,GAAE,MAAM,MAAiB,GAC3B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAgDtD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGrD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,IAAI,CAIhE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CASpE;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IAEtD;;;;;;;;OAQG;IACG,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB1E,8EAA8E;IAC9E,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAIrC;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAG1C"}
|