@moxxy/core 0.6.3 → 0.21.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.
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/host-options.d.ts +2 -0
- package/dist/plugins/host-options.d.ts.map +1 -1
- package/dist/plugins/host.d.ts +7 -0
- package/dist/plugins/host.d.ts.map +1 -1
- package/dist/plugins/host.js +1 -0
- package/dist/plugins/host.js.map +1 -1
- package/dist/plugins/registry-kinds.d.ts +1 -0
- package/dist/plugins/registry-kinds.d.ts.map +1 -1
- package/dist/plugins/registry-kinds.js +12 -0
- package/dist/plugins/registry-kinds.js.map +1 -1
- package/dist/registries/active-def-registry.d.ts +46 -9
- package/dist/registries/active-def-registry.d.ts.map +1 -1
- package/dist/registries/active-def-registry.js +39 -10
- package/dist/registries/active-def-registry.js.map +1 -1
- package/dist/registries/event-stores.d.ts +13 -0
- package/dist/registries/event-stores.d.ts.map +1 -0
- package/dist/registries/event-stores.js +14 -0
- package/dist/registries/event-stores.js.map +1 -0
- package/dist/registries/modes.d.ts +6 -0
- package/dist/registries/modes.d.ts.map +1 -1
- package/dist/registries/modes.js +10 -0
- package/dist/registries/modes.js.map +1 -1
- package/dist/registries/services.d.ts +15 -0
- package/dist/registries/services.d.ts.map +1 -0
- package/dist/registries/services.js +26 -0
- package/dist/registries/services.js.map +1 -0
- package/dist/run-turn.d.ts.map +1 -1
- package/dist/run-turn.js +2 -0
- package/dist/run-turn.js.map +1 -1
- package/dist/session-runtime.d.ts +3 -1
- package/dist/session-runtime.d.ts.map +1 -1
- package/dist/session.d.ts +25 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +80 -10
- package/dist/session.js.map +1 -1
- package/dist/sessions/jsonl-event-store.d.ts +16 -0
- package/dist/sessions/jsonl-event-store.d.ts.map +1 -0
- package/dist/sessions/jsonl-event-store.js +30 -0
- package/dist/sessions/jsonl-event-store.js.map +1 -0
- package/dist/sessions/persistence.d.ts +3 -58
- package/dist/sessions/persistence.d.ts.map +1 -1
- package/dist/sessions/persistence.js +2 -0
- package/dist/sessions/persistence.js.map +1 -1
- package/dist/subagents/run-child.d.ts.map +1 -1
- package/dist/subagents/run-child.js +2 -0
- package/dist/subagents/run-child.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +11 -6
- package/src/plugins/host-options.ts +2 -0
- package/src/plugins/host.test.ts +7 -1
- package/src/plugins/host.ts +8 -0
- package/src/plugins/registry-kinds.test.ts +17 -0
- package/src/plugins/registry-kinds.ts +14 -0
- package/src/registries/active-def-registry.test.ts +42 -0
- package/src/registries/active-def-registry.ts +70 -13
- package/src/registries/event-stores.test.ts +77 -0
- package/src/registries/event-stores.ts +15 -0
- package/src/registries/modes.ts +12 -0
- package/src/registries/services.test.ts +29 -0
- package/src/registries/services.ts +33 -0
- package/src/run-turn.ts +2 -0
- package/src/session-runtime.ts +3 -0
- package/src/session.ts +91 -10
- package/src/sessions/jsonl-event-store.ts +31 -0
- package/src/sessions/persistence.ts +11 -58
- package/src/subagents/run-child.ts +2 -0
- package/dist/preferences.d.ts +0 -40
- package/dist/preferences.d.ts.map +0 -1
- package/dist/preferences.js +0 -59
- package/dist/preferences.js.map +0 -1
- package/src/preferences.test.ts +0 -156
- package/src/preferences.ts +0 -85
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import type { EventStoreDef } from '@moxxy/sdk';
|
|
6
|
+
import { Session, autoAllowResolver, silentLogger } from '../index.js';
|
|
7
|
+
import { jsonlEventStore } from '../sessions/jsonl-event-store.js';
|
|
8
|
+
|
|
9
|
+
function makeSession(): Session {
|
|
10
|
+
return new Session({ cwd: '/tmp', logger: silentLogger, permissionResolver: autoAllowResolver });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('EventStore registry + floor', () => {
|
|
14
|
+
it('seeds the JSONL store as the active, protected floor', () => {
|
|
15
|
+
const session = makeSession();
|
|
16
|
+
expect(session.eventStores.getActiveName()).toBe('jsonl');
|
|
17
|
+
expect(session.eventStores.getFloorName()).toBe('jsonl');
|
|
18
|
+
expect(session.eventStores.list().map((s) => s.name)).toEqual(['jsonl']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('a registered second store does NOT auto-activate (trust boundary)', () => {
|
|
22
|
+
const session = makeSession();
|
|
23
|
+
const fake: EventStoreDef = {
|
|
24
|
+
name: 'fake',
|
|
25
|
+
open: jsonlEventStore.open,
|
|
26
|
+
restore: jsonlEventStore.restore,
|
|
27
|
+
readPage: jsonlEventStore.readPage,
|
|
28
|
+
};
|
|
29
|
+
session.eventStores.register(fake);
|
|
30
|
+
// Still on the floor — a discovered store is inert until explicit setActive.
|
|
31
|
+
expect(session.eventStores.getActiveName()).toBe('jsonl');
|
|
32
|
+
session.eventStores.setActive('fake');
|
|
33
|
+
expect(session.eventStores.getActiveName()).toBe('fake');
|
|
34
|
+
// Removing the swap target reverts to the floor, never null.
|
|
35
|
+
session.eventStores.unregister('fake');
|
|
36
|
+
expect(session.eventStores.getActiveName()).toBe('jsonl');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('refuses to unregister the protected jsonl floor', () => {
|
|
40
|
+
const session = makeSession();
|
|
41
|
+
expect(() => session.eventStores.unregister('jsonl')).toThrow(/protected default/);
|
|
42
|
+
expect(session.eventStores.getActiveName()).toBe('jsonl');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('jsonlEventStore round-trip (behaviour-identical to SessionPersistence)', () => {
|
|
47
|
+
let dir: string;
|
|
48
|
+
beforeEach(async () => {
|
|
49
|
+
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mox-evstore-'));
|
|
50
|
+
});
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('open().attach() persists log events that restore() reads back', async () => {
|
|
56
|
+
const session = makeSession();
|
|
57
|
+
const store = session.eventStores.getActive()!;
|
|
58
|
+
const handle = store.open({ sessionId: session.id, cwd: '/tmp', dir });
|
|
59
|
+
const detach = handle.attach(session.log);
|
|
60
|
+
|
|
61
|
+
await session.log.append({
|
|
62
|
+
type: 'user_prompt',
|
|
63
|
+
text: 'hello store',
|
|
64
|
+
sessionId: session.id,
|
|
65
|
+
source: 'user',
|
|
66
|
+
});
|
|
67
|
+
await handle.settleWrites();
|
|
68
|
+
await handle.flush();
|
|
69
|
+
|
|
70
|
+
const restored = await store.restore(String(session.id), dir);
|
|
71
|
+
expect(restored.some((e) => e.type === 'user_prompt')).toBe(true);
|
|
72
|
+
|
|
73
|
+
const page = await store.readPage(String(session.id), { before: null, limit: 10 }, dir);
|
|
74
|
+
expect(page.events.length).toBeGreaterThanOrEqual(1);
|
|
75
|
+
detach();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EventStoreDef } from '@moxxy/sdk';
|
|
2
|
+
import { ActiveDefRegistry } from './active-def-registry.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The single-active EventStore registry (storage backend behind the event log).
|
|
6
|
+
* Core seeds the JSONL default as a protected floor; uses throw-on-duplicate
|
|
7
|
+
* `register` (NOT override) so a discovered plugin's store is added but never
|
|
8
|
+
* shadows the floor — the user must `setActive` it explicitly. A removed
|
|
9
|
+
* non-floor store reverts to the JSONL floor (never null).
|
|
10
|
+
*/
|
|
11
|
+
export class EventStoreRegistry extends ActiveDefRegistry<EventStoreDef> {
|
|
12
|
+
constructor() {
|
|
13
|
+
super({ noun: 'EventStore' });
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/registries/modes.ts
CHANGED
|
@@ -55,6 +55,18 @@ export class ModeRegistry {
|
|
|
55
55
|
return [...this.modes.values()];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
has(name: string): boolean {
|
|
59
|
+
return this.modes.has(name) || this.modes.has(migrateModeName(name));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Active mode name, or null when none is active. Mirrors ActiveDefRegistry
|
|
63
|
+
* so the manifest apply loop + `categories()` surface can treat modes
|
|
64
|
+
* uniformly. (`mode` has no protected-floor concept here — `mode-default`
|
|
65
|
+
* is a critical package, so the default is protected at the package level.) */
|
|
66
|
+
getActiveName(): string | null {
|
|
67
|
+
return this.active;
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
setActive(name: string): void {
|
|
59
71
|
// Prefer the literal name; only when it isn't registered fall back to the
|
|
60
72
|
// legacy-name map (e.g. a persisted "tool-use" → "default"). This never
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ServiceRegistryImpl } from './services.js';
|
|
3
|
+
|
|
4
|
+
describe('ServiceRegistryImpl', () => {
|
|
5
|
+
it('register / get / has round-trip', () => {
|
|
6
|
+
const r = new ServiceRegistryImpl();
|
|
7
|
+
expect(r.has('vault')).toBe(false);
|
|
8
|
+
expect(r.get('vault')).toBeUndefined();
|
|
9
|
+
const store = { secret: 1 };
|
|
10
|
+
r.register('vault', store);
|
|
11
|
+
expect(r.has('vault')).toBe(true);
|
|
12
|
+
expect(r.get('vault')).toBe(store);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('require returns the service or throws with a helpful message', () => {
|
|
16
|
+
const r = new ServiceRegistryImpl();
|
|
17
|
+
expect(() => r.require('vault')).toThrow(/Required service not registered: vault/);
|
|
18
|
+
const store = { secret: 2 };
|
|
19
|
+
r.register('vault', store);
|
|
20
|
+
expect(r.require<typeof store>('vault')).toBe(store);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('last write wins (a later plugin may replace a service)', () => {
|
|
24
|
+
const r = new ServiceRegistryImpl();
|
|
25
|
+
r.register('x', 1);
|
|
26
|
+
r.register('x', 2);
|
|
27
|
+
expect(r.get('x')).toBe(2);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ServiceRegistry } from '@moxxy/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-process inter-plugin service registry (one per Session). A plugin publishes
|
|
5
|
+
* a named service in `onInit`; a sibling plugin resolves it in its own `onInit`.
|
|
6
|
+
* Last-write-wins on a duplicate name (a later plugin may intentionally replace
|
|
7
|
+
* a service); `require` throws when the name was never published.
|
|
8
|
+
*/
|
|
9
|
+
export class ServiceRegistryImpl implements ServiceRegistry {
|
|
10
|
+
private readonly services = new Map<string, unknown>();
|
|
11
|
+
|
|
12
|
+
register<T>(name: string, impl: T): void {
|
|
13
|
+
this.services.set(name, impl);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get<T>(name: string): T | undefined {
|
|
17
|
+
return this.services.get(name) as T | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
require<T>(name: string): T {
|
|
21
|
+
if (!this.services.has(name)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Required service not registered: ${name}. The providing plugin's onInit ` +
|
|
24
|
+
'must run first — declare a moxxy.requirements entry on the consumer.',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return this.services.get(name) as T;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
has(name: string): boolean {
|
|
31
|
+
return this.services.has(name);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/run-turn.ts
CHANGED
|
@@ -110,6 +110,7 @@ export async function* runTurn(
|
|
|
110
110
|
turnId,
|
|
111
111
|
cwd: appCtx.cwd,
|
|
112
112
|
env: appCtx.env,
|
|
113
|
+
services: appCtx.services,
|
|
113
114
|
model,
|
|
114
115
|
systemPrompt: opts.systemPrompt,
|
|
115
116
|
provider,
|
|
@@ -123,6 +124,7 @@ export async function* runTurn(
|
|
|
123
124
|
// Reasoning preference (effort) — honored only by providers/models that
|
|
124
125
|
// advertise `supportsReasoning` (gated in collectProviderStream).
|
|
125
126
|
...(session.reasoning ? { reasoning: session.reasoning } : {}),
|
|
127
|
+
...(session.loopGuard ? { loopGuard: session.loopGuard } : {}),
|
|
126
128
|
permissions: session.resolver,
|
|
127
129
|
...(session.approvalResolver ? { approval: session.approvalResolver } : {}),
|
|
128
130
|
hooks: session.dispatcher,
|
package/src/session-runtime.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ElisionSettings,
|
|
7
7
|
HookDispatcher,
|
|
8
8
|
LLMProvider,
|
|
9
|
+
LoopGuardSettings,
|
|
9
10
|
ModeDef,
|
|
10
11
|
PermissionResolver,
|
|
11
12
|
PluginHostHandle,
|
|
@@ -59,6 +60,8 @@ export interface SessionRuntime {
|
|
|
59
60
|
readonly lazyTools: boolean;
|
|
60
61
|
/** Reasoning/thinking preference (effort), forwarded to each turn's ModeContext. */
|
|
61
62
|
readonly reasoning?: { readonly effort?: 'low' | 'medium' | 'high' } | boolean | undefined;
|
|
63
|
+
/** Stuck-loop guard tuning, forwarded to each turn's ModeContext. */
|
|
64
|
+
readonly loopGuard?: LoopGuardSettings;
|
|
62
65
|
readonly dispatcher: HookDispatcher;
|
|
63
66
|
readonly pluginHost: PluginHostHandle;
|
|
64
67
|
/**
|
package/src/session.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AppContext,
|
|
3
3
|
ClientSession,
|
|
4
|
+
EmittedEvent,
|
|
5
|
+
LoopGuardSettings,
|
|
4
6
|
MoxxyEvent,
|
|
5
7
|
RunTurnOptions,
|
|
6
8
|
SessionId,
|
|
@@ -32,10 +34,13 @@ import { SynthesizerRegistry } from './registries/synthesizers.js';
|
|
|
32
34
|
import { EmbedderRegistry } from './registries/embedders.js';
|
|
33
35
|
import { IsolatorRegistry } from './registries/isolators.js';
|
|
34
36
|
import { WorkflowExecutorRegistry } from './registries/workflow-executors.js';
|
|
37
|
+
import { EventStoreRegistry } from './registries/event-stores.js';
|
|
38
|
+
import { ServiceRegistryImpl } from './registries/services.js';
|
|
39
|
+
import { jsonlEventStore } from './sessions/jsonl-event-store.js';
|
|
35
40
|
import { RequirementRegistry } from './requirements.js';
|
|
36
41
|
import { PermissionEngine } from './permissions/engine.js';
|
|
37
42
|
import { autoAllowResolver } from './permissions/resolvers.js';
|
|
38
|
-
import { evaluateToolRule } from '@moxxy/sdk';
|
|
43
|
+
import { evaluateToolRule, isSelectableMode } from '@moxxy/sdk';
|
|
39
44
|
import type {
|
|
40
45
|
ApprovalResolver,
|
|
41
46
|
CredentialResolver,
|
|
@@ -129,6 +134,9 @@ export class Session implements ClientSession, SessionRuntime {
|
|
|
129
134
|
readonly embedders: EmbedderRegistry;
|
|
130
135
|
readonly isolators: IsolatorRegistry;
|
|
131
136
|
readonly workflowExecutors: WorkflowExecutorRegistry;
|
|
137
|
+
readonly eventStores: EventStoreRegistry;
|
|
138
|
+
/** Inter-plugin service registry — plugins publish/consume services in onInit. */
|
|
139
|
+
readonly services: ServiceRegistryImpl;
|
|
132
140
|
readonly requirements: RequirementRegistry;
|
|
133
141
|
readonly permissions: PermissionEngine;
|
|
134
142
|
/** Current PermissionResolver. Update via `setPermissionResolver(r)`. */
|
|
@@ -155,6 +163,12 @@ export class Session implements ClientSession, SessionRuntime {
|
|
|
155
163
|
* when the active model advertises `supportsReasoning`. Undefined → off.
|
|
156
164
|
*/
|
|
157
165
|
reasoning: { readonly effort?: 'low' | 'medium' | 'high' } | boolean | undefined = undefined;
|
|
166
|
+
/**
|
|
167
|
+
* Stuck-loop guard tuning, from `config.context.loopGuard`. Forwarded to each
|
|
168
|
+
* turn's ModeContext and on to the mode's stuck-loop detector. Undefined →
|
|
169
|
+
* the detector's defaults.
|
|
170
|
+
*/
|
|
171
|
+
loopGuard: LoopGuardSettings | undefined = undefined;
|
|
158
172
|
/**
|
|
159
173
|
* Model id resolved by the most recent `runTurn()` (see SessionRuntime).
|
|
160
174
|
* Last-writer-wins for concurrent turns; null until the first turn runs.
|
|
@@ -168,8 +182,6 @@ export class Session implements ClientSession, SessionRuntime {
|
|
|
168
182
|
*/
|
|
169
183
|
readyProviders?: Set<string>;
|
|
170
184
|
credentialResolver?: CredentialResolver;
|
|
171
|
-
mcpAdmin?: McpAdminView;
|
|
172
|
-
providerAdmin?: ProviderAdminView;
|
|
173
185
|
workflows?: WorkflowsView;
|
|
174
186
|
pluginsAdmin?: PluginsAdminView;
|
|
175
187
|
readonly dispatcher: HookDispatcherImpl;
|
|
@@ -191,13 +203,15 @@ export class Session implements ClientSession, SessionRuntime {
|
|
|
191
203
|
this.compactors = new CompactorRegistry();
|
|
192
204
|
this.cacheStrategies = new CacheStrategyRegistry();
|
|
193
205
|
this.viewRenderers = new ViewRendererRegistry();
|
|
194
|
-
// Seed the built-in renderer so `present_view`
|
|
195
|
-
// with; plugins can register/replace and
|
|
196
|
-
|
|
206
|
+
// Seed the built-in renderer as the protected floor so `present_view`
|
|
207
|
+
// always has one to parse with; plugins can register/replace and
|
|
208
|
+
// `setActive` an alternative, but it can never be removed (reverts here).
|
|
209
|
+
this.viewRenderers.register(defaultViewRenderer, { protected: true });
|
|
197
210
|
this.tunnelProviders = new TunnelProviderRegistry();
|
|
198
|
-
// Seed the no-op localhost provider
|
|
199
|
-
// URL; plugins (the proxy relay) register/
|
|
200
|
-
|
|
211
|
+
// Seed the no-op localhost provider as the protected floor so the web
|
|
212
|
+
// surface always resolves a URL; plugins (the proxy relay) register/
|
|
213
|
+
// setActive a real tunnel, reverting here if it's removed.
|
|
214
|
+
this.tunnelProviders.register(localhostTunnel, { protected: true });
|
|
201
215
|
this.channels = new ChannelRegistryImpl();
|
|
202
216
|
this.surfaceRegistry = new SurfaceRegistryImpl();
|
|
203
217
|
this.surfaces = new SurfaceHostImpl(this.surfaceRegistry, { cwd: this.cwd, logger: this.logger }, this.logger);
|
|
@@ -211,6 +225,49 @@ export class Session implements ClientSession, SessionRuntime {
|
|
|
211
225
|
this.embedders = new EmbedderRegistry();
|
|
212
226
|
this.isolators = new IsolatorRegistry();
|
|
213
227
|
this.workflowExecutors = new WorkflowExecutorRegistry();
|
|
228
|
+
this.services = new ServiceRegistryImpl();
|
|
229
|
+
// Publish the core registries on the inter-plugin service registry under
|
|
230
|
+
// well-known names, so a discovery-loaded plugin can resolve one in its
|
|
231
|
+
// onInit (typed as the SDK's minimal NamedRegistry) instead of being
|
|
232
|
+
// hand-built with a `() => session.<registry>` closure. The registry
|
|
233
|
+
// objects are stable; their contents grow as plugins register — consumers
|
|
234
|
+
// read them lazily at tool-call time, after all registration has run.
|
|
235
|
+
this.services.register('agents', this.agents);
|
|
236
|
+
this.services.register('tools', this.tools);
|
|
237
|
+
this.services.register('providers', this.providers);
|
|
238
|
+
this.services.register('viewRenderers', this.viewRenderers);
|
|
239
|
+
this.services.register('synthesizers', this.synthesizers);
|
|
240
|
+
this.services.register('skills', this.skills);
|
|
241
|
+
this.services.register('tunnelProviders', this.tunnelProviders);
|
|
242
|
+
// A stable accessor for the active provider's stored credentials. The
|
|
243
|
+
// resolver itself is installed late (by activateProvider, after plugins are
|
|
244
|
+
// built), so close over `this` and read it lazily at call time — lets
|
|
245
|
+
// provider-admin rebuild a reconfigured provider's instance without a
|
|
246
|
+
// host-injected `resolveActiveConfig` closure.
|
|
247
|
+
this.services.register(
|
|
248
|
+
'resolveCredentials',
|
|
249
|
+
(name: string): Promise<Record<string, unknown>> | Record<string, unknown> =>
|
|
250
|
+
this.credentialResolver ? this.credentialResolver(name) : {},
|
|
251
|
+
);
|
|
252
|
+
// A live registry-name snapshot (per kind) + a writable event-append fn, for
|
|
253
|
+
// plugins (self-update) that need them in onInit without a host closure.
|
|
254
|
+
// appendEvent is the writable counterpart to the read-only `ctx.log`.
|
|
255
|
+
this.services.register('registrySnapshot', () => ({
|
|
256
|
+
tools: this.tools.list().map((t) => t.name),
|
|
257
|
+
agents: this.agents.list().map((a) => a.name),
|
|
258
|
+
providers: this.providers.list().map((p) => p.name),
|
|
259
|
+
modes: this.modes.list().filter(isSelectableMode).map((m) => m.name),
|
|
260
|
+
compactors: this.compactors.list().map((c) => c.name),
|
|
261
|
+
channels: this.channels.list().map((c) => c.name),
|
|
262
|
+
}));
|
|
263
|
+
this.services.register(
|
|
264
|
+
'appendEvent',
|
|
265
|
+
(event: EmittedEvent): Promise<void> => this.log.append(event).then(() => undefined),
|
|
266
|
+
);
|
|
267
|
+
this.eventStores = new EventStoreRegistry();
|
|
268
|
+
// Seed the built-in JSONL store as the protected floor — the storage backend
|
|
269
|
+
// behind the event log always exists and can be swapped but never removed.
|
|
270
|
+
this.eventStores.register(jsonlEventStore, { protected: true });
|
|
214
271
|
this.requirements = new RequirementRegistry({
|
|
215
272
|
tools: this.tools,
|
|
216
273
|
providers: this.providers,
|
|
@@ -257,12 +314,16 @@ export class Session implements ClientSession, SessionRuntime {
|
|
|
257
314
|
embedders: this.embedders,
|
|
258
315
|
isolators: this.isolators,
|
|
259
316
|
workflowExecutors: this.workflowExecutors,
|
|
317
|
+
eventStores: this.eventStores,
|
|
260
318
|
requirements: this.requirements,
|
|
261
319
|
dispatcher: this.dispatcher,
|
|
262
320
|
loader: opts.pluginLoader,
|
|
263
321
|
...(opts.pluginDiscoveryPaths ? { userPaths: opts.pluginDiscoveryPaths } : {}),
|
|
264
322
|
...(opts.isPluginDisabled ? { isDisabled: opts.isPluginDisabled } : {}),
|
|
265
323
|
});
|
|
324
|
+
// Published after construction (the host is built late) so a discovery-loaded
|
|
325
|
+
// plugin (self-update) can reach reload/unload/listSkipped in its onInit.
|
|
326
|
+
this.services.register('pluginHost', this.pluginHost);
|
|
266
327
|
|
|
267
328
|
// Fan every appended event out to plugin `onEvent` hooks. Without this
|
|
268
329
|
// wiring the hook is dead code — declared on the SDK, dispatched by
|
|
@@ -367,6 +428,25 @@ export class Session implements ClientSession, SessionRuntime {
|
|
|
367
428
|
*/
|
|
368
429
|
private envSnapshot: Readonly<NodeJS.ProcessEnv> | null = null;
|
|
369
430
|
|
|
431
|
+
/**
|
|
432
|
+
* The provider-admin capability (configure/refresh stored providers), read by
|
|
433
|
+
* the runner's provider handlers + the desktop. The @moxxy/plugin-provider-admin
|
|
434
|
+
* plugin publishes it on the service registry in its onInit, so discovery-loading
|
|
435
|
+
* it needs no host stash. (RemoteSession sets its own field for thin clients.)
|
|
436
|
+
*/
|
|
437
|
+
get providerAdmin(): ProviderAdminView | undefined {
|
|
438
|
+
return this.services.get<ProviderAdminView>('providerAdmin');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The MCP-admin capability (enable/detach/list MCP servers), read by the
|
|
443
|
+
* runner's mcp handlers + the desktop. @moxxy/plugin-mcp publishes it on the
|
|
444
|
+
* service registry in its onInit. (RemoteSession sets its own field.)
|
|
445
|
+
*/
|
|
446
|
+
get mcpAdmin(): McpAdminView | undefined {
|
|
447
|
+
return this.services.get<McpAdminView>('mcpAdmin');
|
|
448
|
+
}
|
|
449
|
+
|
|
370
450
|
appContext(): AppContext {
|
|
371
451
|
if (!this.envSnapshot) {
|
|
372
452
|
this.envSnapshot = Object.freeze({ ...process.env });
|
|
@@ -376,6 +456,7 @@ export class Session implements ClientSession, SessionRuntime {
|
|
|
376
456
|
cwd: this.cwd,
|
|
377
457
|
log: this.log.asReader(),
|
|
378
458
|
env: this.envSnapshot,
|
|
459
|
+
services: this.services,
|
|
379
460
|
};
|
|
380
461
|
}
|
|
381
462
|
|
|
@@ -435,7 +516,7 @@ export class Session implements ClientSession, SessionRuntime {
|
|
|
435
516
|
})),
|
|
436
517
|
activeMode,
|
|
437
518
|
activeModeBadge,
|
|
438
|
-
modes: this.modes.list().map((m) => m.name),
|
|
519
|
+
modes: this.modes.list().filter(isSelectableMode).map((m) => m.name),
|
|
439
520
|
tools: this.tools.list().map((t) => ({
|
|
440
521
|
name: t.name,
|
|
441
522
|
description: t.description,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EventStoreDef, EventStoreScope, EventStoreSession } from '@moxxy/sdk';
|
|
2
|
+
import { SessionPersistence, readEventPage, restoreEvents } from './persistence.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The built-in EventStore: per-session JSONL (`~/.moxxy/sessions/<id>.jsonl`) +
|
|
6
|
+
* a `.json` meta sidecar. A thin adapter over the battle-tested
|
|
7
|
+
* {@link SessionPersistence} (write) and `restoreEvents`/`readEventPage` (read)
|
|
8
|
+
* — zero behaviour change, just expressed behind the {@link EventStoreDef}
|
|
9
|
+
* contract. Core seeds this as the protected floor: a plugin can register an
|
|
10
|
+
* alternative store, but it never auto-activates (the user opts in by name via
|
|
11
|
+
* `plugins.eventStore.default`).
|
|
12
|
+
*
|
|
13
|
+
* `SessionPersistence` already exposes the full {@link EventStoreSession}
|
|
14
|
+
* surface (attach/flush/settleWrites/updateHeader/degraded), so `open` returns
|
|
15
|
+
* one directly.
|
|
16
|
+
*/
|
|
17
|
+
export const jsonlEventStore: EventStoreDef = {
|
|
18
|
+
name: 'jsonl',
|
|
19
|
+
open(scope: EventStoreScope): EventStoreSession {
|
|
20
|
+
return new SessionPersistence({
|
|
21
|
+
sessionId: scope.sessionId,
|
|
22
|
+
cwd: scope.cwd,
|
|
23
|
+
...(scope.dir ? { dir: scope.dir } : {}),
|
|
24
|
+
...(scope.providerName ? { providerName: scope.providerName } : {}),
|
|
25
|
+
...(scope.modelId ? { modelId: scope.modelId } : {}),
|
|
26
|
+
...(scope.source ? { source: scope.source } : {}),
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
restore: (sessionId, dir) => restoreEvents(sessionId, dir),
|
|
30
|
+
readPage: (sessionId, opts, dir) => readEventPage(sessionId, opts, dir),
|
|
31
|
+
};
|
|
@@ -19,59 +19,22 @@
|
|
|
19
19
|
import { constants as fsConstants, promises as fs } from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { createMutex, type Mutex, type MoxxyEvent, type SessionId } from '@moxxy/sdk';
|
|
22
|
+
import type { EventLogLike, EventPage, SessionMeta, SessionSource } from '@moxxy/sdk';
|
|
22
23
|
import { moxxyPath, writeFileAtomic } from '@moxxy/sdk/server';
|
|
23
|
-
import type { EventLog } from '../events/log.js';
|
|
24
24
|
import { createLogger, type Logger } from '../logger.js';
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* prompt (a brand-new chat the user just opened); empty `cli`/`tui` sidecars are
|
|
32
|
-
* dropped as noise.
|
|
33
|
-
*/
|
|
34
|
-
export type SessionSource = 'cli' | 'tui' | 'desktop' | 'mobile';
|
|
26
|
+
// `SessionSource`, `SessionMeta` and `EventPage` are the EventStore contract's
|
|
27
|
+
// data shapes — defined in @moxxy/sdk so `EventStoreDef` can reference them.
|
|
28
|
+
// Re-exported here (and onward from @moxxy/core) so existing importers are
|
|
29
|
+
// unaffected by the move.
|
|
30
|
+
export type { EventLogLike, EventPage, SessionMeta, SessionSource } from '@moxxy/sdk';
|
|
35
31
|
|
|
36
32
|
/** Schema version of the per-session metadata file (`<id>.json`). Bump when the
|
|
37
33
|
* shape changes incompatibly; readers tolerate a missing/older version. */
|
|
38
34
|
export const SESSION_META_VERSION = 1;
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
* ONE file per session, the unit every surface (TUI/desktop/mobile) lists,
|
|
44
|
-
* searches and caches. The conversation itself lives in the append-only
|
|
45
|
-
* `<id>.jsonl`. Fields split by owner:
|
|
46
|
-
* - the RUNNER owns the content fields (`firstPrompt`, `eventCount`,
|
|
47
|
-
* `lastActivity`, `provider`, `model`, `startedAt`, `source`) and rewrites
|
|
48
|
-
* them on a debounce;
|
|
49
|
-
* - the UI owns `title` (a rename) and `groupId` (which desk it belongs to).
|
|
50
|
-
* Because both write the same file, the runner ADOPTS the UI fields on attach
|
|
51
|
-
* and re-merges them just before each write, so a live session never clobbers a
|
|
52
|
-
* rename or a move.
|
|
53
|
-
*/
|
|
54
|
-
export interface SessionMeta {
|
|
55
|
-
/** Schema version; absent on older files (treated as v0). */
|
|
56
|
-
readonly version?: number;
|
|
57
|
-
readonly id: string;
|
|
58
|
-
readonly cwd: string;
|
|
59
|
-
readonly startedAt: string;
|
|
60
|
-
readonly lastActivity: string;
|
|
61
|
-
readonly eventCount: number;
|
|
62
|
-
/** First 80 chars of the first user_prompt. The list/search label. */
|
|
63
|
-
readonly firstPrompt: string | null;
|
|
64
|
-
readonly provider: string | null;
|
|
65
|
-
readonly model: string | null;
|
|
66
|
-
/** Originating channel, when known. Written by the runner via `opts.source`. */
|
|
67
|
-
readonly source?: SessionSource;
|
|
68
|
-
/** Explicit workspace/desk membership (UI-owned). `null`/absent → grouped by
|
|
69
|
-
* cwd containment (CLI/TUI sessions that don't know about desks). */
|
|
70
|
-
readonly groupId?: string | null;
|
|
71
|
-
/** User-set display name, the rename (UI-owned). `null`/absent → the name is
|
|
72
|
-
* derived from `firstPrompt`. */
|
|
73
|
-
readonly title?: string | null;
|
|
74
|
-
}
|
|
36
|
+
// `SessionMeta` is defined in @moxxy/sdk (the EventStore contract's listing
|
|
37
|
+
// shape) and re-exported above. The JSONL impl below reads/writes it.
|
|
75
38
|
|
|
76
39
|
export interface SessionPersistenceOpts {
|
|
77
40
|
readonly sessionId: SessionId;
|
|
@@ -216,7 +179,7 @@ export class SessionPersistence {
|
|
|
216
179
|
* in the same (now empty) JSONL, matching how the in-memory log
|
|
217
180
|
* reuses the same Session object.
|
|
218
181
|
*/
|
|
219
|
-
attach(log:
|
|
182
|
+
attach(log: EventLogLike): () => void {
|
|
220
183
|
void this.ensureReady()
|
|
221
184
|
.then(() => this.scheduleIndexWrite())
|
|
222
185
|
.catch(() => undefined);
|
|
@@ -679,18 +642,8 @@ export async function restoreEvents(
|
|
|
679
642
|
return events;
|
|
680
643
|
}
|
|
681
644
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
export interface EventPage {
|
|
685
|
-
/** The events in this page, in ascending `seq` order. */
|
|
686
|
-
readonly events: MoxxyEvent[];
|
|
687
|
-
/**
|
|
688
|
-
* Cursor to pass as `before` for the NEXT (older) page — the `seq` of the
|
|
689
|
-
* OLDEST event in this page. `null` once the start of history (the first
|
|
690
|
-
* persisted event) is included, signalling there is no older page.
|
|
691
|
-
*/
|
|
692
|
-
readonly prevCursor: number | null;
|
|
693
|
-
}
|
|
645
|
+
// `EventPage` is defined in @moxxy/sdk (the EventStore read contract) and
|
|
646
|
+
// re-exported above; `readEventPage`/`pageEvents` below produce it.
|
|
694
647
|
|
|
695
648
|
/**
|
|
696
649
|
* Read ONE page of a persisted session's events without re-materializing the
|
|
@@ -447,6 +447,7 @@ function buildChildContext(
|
|
|
447
447
|
turnId: childTurnId,
|
|
448
448
|
cwd: parentAppCtx.cwd,
|
|
449
449
|
env: parentAppCtx.env,
|
|
450
|
+
services: parentAppCtx.services,
|
|
450
451
|
model,
|
|
451
452
|
...(spec.systemPrompt !== undefined ? { systemPrompt: spec.systemPrompt } : {}),
|
|
452
453
|
provider: parentSession.providers.getActive(),
|
|
@@ -457,6 +458,7 @@ function buildChildContext(
|
|
|
457
458
|
cacheStrategy: parentSession.cacheStrategies.getActive(),
|
|
458
459
|
...(parentSession.elisionSettings ? { elision: parentSession.elisionSettings } : {}),
|
|
459
460
|
...(parentSession.lazyTools ? { lazyTools: true } : {}),
|
|
461
|
+
...(parentSession.loopGuard ? { loopGuard: parentSession.loopGuard } : {}),
|
|
460
462
|
permissions: parentSession.resolver,
|
|
461
463
|
// Intentionally no `approval` — fanning approval gates out to N
|
|
462
464
|
// children in parallel would prompt the user N times. Strategies
|
package/dist/preferences.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* User-level runtime preferences persisted at ~/.moxxy/preferences.json.
|
|
3
|
-
* Distinct from `~/.moxxy/config.yaml` (user-edited, source of truth for
|
|
4
|
-
* provider/plugin wiring) — this file is mutated by the TUI as the user
|
|
5
|
-
* picks a model / loop strategy via slash commands, so it survives across
|
|
6
|
-
* CLI invocations. A missing file or unreadable contents is a no-op:
|
|
7
|
-
* preferences are best-effort, never load-blocking.
|
|
8
|
-
*/
|
|
9
|
-
export interface MoxxyPreferences {
|
|
10
|
-
/** Active provider name (e.g. "openai", "anthropic"). */
|
|
11
|
-
readonly providerName?: string;
|
|
12
|
-
/** Active model id under that provider (e.g. "gpt-5.4-mini"). */
|
|
13
|
-
readonly model?: string;
|
|
14
|
-
/** Active loop strategy name (e.g. "default", "research", "goal"). */
|
|
15
|
-
readonly mode?: string;
|
|
16
|
-
/**
|
|
17
|
-
* Providers the user disabled (desktop Settings → Providers toggle).
|
|
18
|
-
* Seeded into the ProviderRegistry at boot; a listed provider stays
|
|
19
|
-
* registered but can't be activated until re-enabled.
|
|
20
|
-
*/
|
|
21
|
-
readonly disabledProviders?: ReadonlyArray<string>;
|
|
22
|
-
}
|
|
23
|
-
export declare function preferencesPath(): string;
|
|
24
|
-
/**
|
|
25
|
-
* Read preferences from disk. Returns an empty object when the file
|
|
26
|
-
* doesn't exist or fails to parse — preferences are an optional layer,
|
|
27
|
-
* never a hard dependency of session bootstrap.
|
|
28
|
-
*/
|
|
29
|
-
export declare function loadPreferences(): Promise<MoxxyPreferences>;
|
|
30
|
-
/**
|
|
31
|
-
* Merge-and-write preferences. Reads the current file (if any), merges
|
|
32
|
-
* the patch on top so we don't blow away unrelated fields, and writes
|
|
33
|
-
* the result back atomically. The whole read-merge-write runs inside a
|
|
34
|
-
* module-level mutex so overlapping saves serialize (invariant #5) and
|
|
35
|
-
* never lose an update. Best-effort: a write failure logs to stderr but
|
|
36
|
-
* does not throw — the user's pick still takes effect in this session,
|
|
37
|
-
* just won't persist across invocations.
|
|
38
|
-
*/
|
|
39
|
-
export declare function savePreferences(patch: Partial<MoxxyPreferences>): Promise<void>;
|
|
40
|
-
//# sourceMappingURL=preferences.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"preferences.d.ts","sourceRoot":"","sources":["../src/preferences.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACpD;AAED,wBAAgB,eAAe,IAAI,MAAM,CAKxC;AAED;;;;GAIG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAYjE;AAUD;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAcrF"}
|
package/dist/preferences.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import { createMutex, migrateModeName } from '@moxxy/sdk';
|
|
3
|
-
import { moxxyPath, writeFileAtomic } from '@moxxy/sdk/server';
|
|
4
|
-
export function preferencesPath() {
|
|
5
|
-
// Route through `moxxyPath` so a `$MOXXY_HOME` override relocates preferences
|
|
6
|
-
// alongside the rest of the data dir (config.yaml, vault, providers.json, …).
|
|
7
|
-
// Identical to `~/.moxxy/preferences.json` when MOXXY_HOME is unset.
|
|
8
|
-
return moxxyPath('preferences.json');
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Read preferences from disk. Returns an empty object when the file
|
|
12
|
-
* doesn't exist or fails to parse — preferences are an optional layer,
|
|
13
|
-
* never a hard dependency of session bootstrap.
|
|
14
|
-
*/
|
|
15
|
-
export async function loadPreferences() {
|
|
16
|
-
try {
|
|
17
|
-
const raw = await fs.readFile(preferencesPath(), 'utf8');
|
|
18
|
-
const parsed = JSON.parse(raw);
|
|
19
|
-
if (parsed && typeof parsed === 'object') {
|
|
20
|
-
const prefs = parsed;
|
|
21
|
-
return prefs.mode ? { ...prefs, mode: migrateModeName(prefs.mode) } : prefs;
|
|
22
|
-
}
|
|
23
|
-
return {};
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
// Serializes the read-modify-write in `savePreferences` so two concurrent
|
|
30
|
-
// saves (e.g. a /model pick and a provider-disable toggle firing close
|
|
31
|
-
// together) can't both read the same snapshot and have the second write
|
|
32
|
-
// clobber the first's field. This is the single write path for the file —
|
|
33
|
-
// other packages (CLI, runner, channels) route their fire-and-forget saves
|
|
34
|
-
// through it, and any load-modify-save must go through it too.
|
|
35
|
-
const writeMutex = createMutex();
|
|
36
|
-
/**
|
|
37
|
-
* Merge-and-write preferences. Reads the current file (if any), merges
|
|
38
|
-
* the patch on top so we don't blow away unrelated fields, and writes
|
|
39
|
-
* the result back atomically. The whole read-merge-write runs inside a
|
|
40
|
-
* module-level mutex so overlapping saves serialize (invariant #5) and
|
|
41
|
-
* never lose an update. Best-effort: a write failure logs to stderr but
|
|
42
|
-
* does not throw — the user's pick still takes effect in this session,
|
|
43
|
-
* just won't persist across invocations.
|
|
44
|
-
*/
|
|
45
|
-
export async function savePreferences(patch) {
|
|
46
|
-
return writeMutex.run(async () => {
|
|
47
|
-
const current = await loadPreferences();
|
|
48
|
-
const next = { ...current, ...patch };
|
|
49
|
-
const target = preferencesPath();
|
|
50
|
-
try {
|
|
51
|
-
await writeFileAtomic(target, JSON.stringify(next, null, 2) + '\n');
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
process.stderr.write(`moxxy: failed to persist preferences to ${target}: ` +
|
|
55
|
-
`${err instanceof Error ? err.message : String(err)}\n`);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
//# sourceMappingURL=preferences.js.map
|
package/dist/preferences.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"preferences.js","sourceRoot":"","sources":["../src/preferences.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAyB/D,MAAM,UAAU,eAAe;IAC7B,8EAA8E;IAC9E,8EAA8E;IAC9E,qEAAqE;IACrE,OAAO,SAAS,CAAC,kBAAkB,CAAC,CAAC;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,EAAE,MAAM,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,MAA0B,CAAC;YACzC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAC9E,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,uEAAuE;AACvE,wEAAwE;AACxE,0EAA0E;AAC1E,2EAA2E;AAC3E,+DAA+D;AAC/D,MAAM,UAAU,GAAG,WAAW,EAAE,CAAC;AAEjC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAgC;IACpE,OAAO,UAAU,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;QAC/B,MAAM,OAAO,GAAG,MAAM,eAAe,EAAE,CAAC;QACxC,MAAM,IAAI,GAAqB,EAAE,GAAG,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;QACxD,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QACtE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,2CAA2C,MAAM,IAAI;gBACnD,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAC1D,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|