@moxxy/sdk 0.18.0 → 0.20.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/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/event-store.d.ts +113 -0
- package/dist/event-store.d.ts.map +1 -0
- package/dist/event-store.js +2 -0
- package/dist/event-store.js.map +1 -0
- package/dist/events.d.ts +17 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/hooks.d.ts +6 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mode/collect-stream.d.ts.map +1 -1
- package/dist/mode/collect-stream.js +1 -0
- package/dist/mode/collect-stream.js.map +1 -1
- package/dist/mode/stuck-loop.d.ts +20 -6
- package/dist/mode/stuck-loop.d.ts.map +1 -1
- package/dist/mode/stuck-loop.js +14 -3
- package/dist/mode/stuck-loop.js.map +1 -1
- package/dist/mode-helpers.d.ts +1 -1
- package/dist/mode-helpers.d.ts.map +1 -1
- package/dist/mode-helpers.js.map +1 -1
- package/dist/mode.d.ts +9 -0
- package/dist/mode.d.ts.map +1 -1
- package/dist/mode.js.map +1 -1
- package/dist/plugin.d.ts +10 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/schemas.d.ts +7 -7
- package/dist/services.d.ts +47 -0
- package/dist/services.d.ts.map +1 -0
- package/dist/services.js +2 -0
- package/dist/services.js.map +1 -0
- package/dist/session-like.d.ts +50 -3
- package/dist/session-like.d.ts.map +1 -1
- package/dist/tool-dispatch.d.ts.map +1 -1
- package/dist/tool-dispatch.js +1 -0
- package/dist/tool-dispatch.js.map +1 -1
- package/dist/workflow.d.ts +20 -0
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +1 -0
- package/src/event-store.ts +118 -0
- package/src/events.ts +18 -0
- package/src/hooks.ts +6 -0
- package/src/index.ts +15 -0
- package/src/mode/collect-stream.ts +1 -0
- package/src/mode/stuck-loop.ts +28 -11
- package/src/mode-helpers.ts +1 -0
- package/src/mode.ts +9 -0
- package/src/plugin.ts +10 -1
- package/src/services.ts +47 -0
- package/src/session-like.ts +52 -3
- package/src/stuck-loop.test.ts +16 -3
- package/src/tool-dispatch.ts +1 -0
- package/src/workflow.ts +20 -0
package/src/mode/stuck-loop.ts
CHANGED
|
@@ -34,6 +34,27 @@ export interface StuckLoopDetector {
|
|
|
34
34
|
record(toolName: string, input: unknown): StuckSignal;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* User-tunable loop-guard settings (config `context.loopGuard`). All optional —
|
|
39
|
+
* omitted fields use the defaults below. Set `enabled: false` to turn the guard
|
|
40
|
+
* off entirely and rely solely on the mode's `maxIterations` cap.
|
|
41
|
+
*/
|
|
42
|
+
export interface LoopGuardSettings {
|
|
43
|
+
readonly enabled?: boolean;
|
|
44
|
+
readonly windowSize?: number;
|
|
45
|
+
readonly repeatThreshold?: number;
|
|
46
|
+
readonly nearWindowSize?: number;
|
|
47
|
+
readonly nearThreshold?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Default exact-repeat window + trip count. Deliberately generous: the
|
|
51
|
+
* `maxIterations` cap (500 in default mode) is the real runaway backstop, so
|
|
52
|
+
* this only needs to catch a *tight* same-call loop, not legitimately repeated
|
|
53
|
+
* work (re-reading a file, re-running `git status` across steps, a couple of
|
|
54
|
+
* retries). Tunable via `context.loopGuard`. */
|
|
55
|
+
export const DEFAULT_LOOP_WINDOW_SIZE = 12;
|
|
56
|
+
export const DEFAULT_LOOP_REPEAT_THRESHOLD = 8;
|
|
57
|
+
|
|
37
58
|
/** Identity arguments that pin "the same target" across volatile-arg variation,
|
|
38
59
|
* best-first. The first present string field wins. */
|
|
39
60
|
const IDENTITY_ARG_KEYS = ['url', 'file_path', 'path', 'command', 'cmd', 'query', 'pattern'];
|
|
@@ -48,19 +69,13 @@ function identityArg(input: unknown): string | null {
|
|
|
48
69
|
return null;
|
|
49
70
|
}
|
|
50
71
|
|
|
51
|
-
export function createStuckLoopDetector(
|
|
52
|
-
opts
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
nearWindowSize?: number;
|
|
56
|
-
nearThreshold?: number;
|
|
57
|
-
} = {},
|
|
58
|
-
): StuckLoopDetector {
|
|
59
|
-
const windowSize = opts.windowSize ?? 8;
|
|
60
|
-
const repeatThreshold = opts.repeatThreshold ?? 3;
|
|
72
|
+
export function createStuckLoopDetector(opts: LoopGuardSettings = {}): StuckLoopDetector {
|
|
73
|
+
const enabled = opts.enabled ?? true;
|
|
74
|
+
const windowSize = opts.windowSize ?? DEFAULT_LOOP_WINDOW_SIZE;
|
|
75
|
+
const repeatThreshold = opts.repeatThreshold ?? DEFAULT_LOOP_REPEAT_THRESHOLD;
|
|
61
76
|
// Near-dups need a higher count + a wider window (they're spread out across a
|
|
62
77
|
// burst of other calls), and tolerate a couple of legit "bigger refetch" tries.
|
|
63
|
-
const nearWindowSize = opts.nearWindowSize ?? Math.max(windowSize * 2,
|
|
78
|
+
const nearWindowSize = opts.nearWindowSize ?? Math.max(windowSize * 2, 24);
|
|
64
79
|
const nearThreshold = opts.nearThreshold ?? Math.max(repeatThreshold + 2, 5);
|
|
65
80
|
const recent: string[] = [];
|
|
66
81
|
const recentNear: string[] = [];
|
|
@@ -68,6 +83,8 @@ export function createStuckLoopDetector(
|
|
|
68
83
|
windowSize,
|
|
69
84
|
repeatThreshold,
|
|
70
85
|
record(toolName, input): StuckSignal {
|
|
86
|
+
// Disabled → never trip (rely on the maxIterations cap alone).
|
|
87
|
+
if (!enabled) return { stuck: false, count: 0, kind: 'exact' };
|
|
71
88
|
const key = `${toolName}|${stableHash(input)}`;
|
|
72
89
|
recent.push(key);
|
|
73
90
|
if (recent.length > windowSize) recent.shift();
|
package/src/mode-helpers.ts
CHANGED
package/src/mode.ts
CHANGED
|
@@ -2,10 +2,12 @@ import type { CacheStrategyDef } from './cache-strategy.js';
|
|
|
2
2
|
import type { CompactorDef } from './compactor.js';
|
|
3
3
|
import type { EmittedEvent, MoxxyEvent } from './events.js';
|
|
4
4
|
import type { HookDispatcher } from './hooks.js';
|
|
5
|
+
import type { ServiceRegistry } from './services.js';
|
|
5
6
|
import type { SessionId, TurnId } from './ids.js';
|
|
6
7
|
import type { EventLogReader } from './log.js';
|
|
7
8
|
import type { PermissionResolver } from './permission.js';
|
|
8
9
|
import type { LLMProvider } from './provider.js';
|
|
10
|
+
import type { LoopGuardSettings } from './mode/stuck-loop.js';
|
|
9
11
|
import type { Skill } from './skill.js';
|
|
10
12
|
import type { SubagentSpawner } from './subagent.js';
|
|
11
13
|
import type { ToolDef } from './tool.js';
|
|
@@ -106,8 +108,15 @@ export interface ModeContext {
|
|
|
106
108
|
readonly approval?: ApprovalResolver;
|
|
107
109
|
readonly hooks: HookDispatcher;
|
|
108
110
|
readonly pluginHost: PluginHostHandle;
|
|
111
|
+
/** Inter-plugin service registry (mirrors {@link AppContext.services}). */
|
|
112
|
+
readonly services: ServiceRegistry;
|
|
109
113
|
readonly signal: AbortSignal;
|
|
110
114
|
readonly maxIterations?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Loop-guard tuning (config `context.loopGuard`), forwarded to each mode's
|
|
117
|
+
* stuck-loop detector. When omitted the detector uses its defaults.
|
|
118
|
+
*/
|
|
119
|
+
readonly loopGuard?: LoopGuardSettings;
|
|
111
120
|
/**
|
|
112
121
|
* Spawn one or more child agents that share the parent's registries
|
|
113
122
|
* but run in isolation. Children stream their events back to the
|
package/src/plugin.ts
CHANGED
|
@@ -16,8 +16,9 @@ import type { SynthesizerDef } from './synthesizer.js';
|
|
|
16
16
|
import type { ViewRendererDef } from './view-renderer.js';
|
|
17
17
|
import type { TunnelProviderDef } from './tunnel.js';
|
|
18
18
|
import type { WorkflowExecutorDef } from './workflow.js';
|
|
19
|
+
import type { EventStoreDef } from './event-store.js';
|
|
19
20
|
|
|
20
|
-
export type PluginKind = 'tools' | 'provider' | 'mode' | 'compactor' | 'cache-strategy' | 'view-renderer' | 'tunnel-provider' | 'mcp' | 'cli' | 'channel' | 'surface' | 'hooks' | 'agent' | 'command' | 'transcriber' | 'synthesizer' | 'embedder' | 'isolator' | 'workflow-executor';
|
|
21
|
+
export type PluginKind = 'tools' | 'provider' | 'mode' | 'compactor' | 'cache-strategy' | 'view-renderer' | 'tunnel-provider' | 'mcp' | 'cli' | 'channel' | 'surface' | 'hooks' | 'agent' | 'command' | 'transcriber' | 'synthesizer' | 'embedder' | 'isolator' | 'workflow-executor' | 'event-store';
|
|
21
22
|
|
|
22
23
|
export interface PluginSpec {
|
|
23
24
|
readonly name: string;
|
|
@@ -44,6 +45,14 @@ export interface PluginSpec {
|
|
|
44
45
|
* the proxy relay). One active per session; core seeds a `localhost` no-op.
|
|
45
46
|
*/
|
|
46
47
|
readonly tunnelProviders?: ReadonlyArray<TunnelProviderDef>;
|
|
48
|
+
/**
|
|
49
|
+
* Event-store backends — the storage behind a session's event log. Core seeds
|
|
50
|
+
* a protected JSONL default; a plugin can contribute an alternative (SQLite,
|
|
51
|
+
* remote, encrypted), activated only by explicit `plugins.eventStore.default`
|
|
52
|
+
* (a discovered store is registered but never auto-active — the trust
|
|
53
|
+
* boundary, since the store sees every event). See {@link EventStoreDef}.
|
|
54
|
+
*/
|
|
55
|
+
readonly eventStores?: ReadonlyArray<EventStoreDef>;
|
|
47
56
|
readonly channels?: ReadonlyArray<ChannelDef>;
|
|
48
57
|
/**
|
|
49
58
|
* Interactive surfaces contributed by the plugin — long-lived panes a human
|
package/src/services.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal read-only view of a name-keyed registry. The host publishes its core
|
|
3
|
+
* registries (agents, tools, …) on the {@link ServiceRegistry} under these
|
|
4
|
+
* names, so a discovery-loaded plugin can resolve one in `onInit` (e.g.
|
|
5
|
+
* `ctx.services.require<NamedRegistry<AgentDef>>('agents')`) without importing
|
|
6
|
+
* `@moxxy/core`'s concrete registry types. Core's `DefMapRegistry`-based
|
|
7
|
+
* registries satisfy it structurally.
|
|
8
|
+
*
|
|
9
|
+
* Well-known service names the host publishes: `'agents'`, `'tools'`,
|
|
10
|
+
* `'providers'`, `'viewRenderers'`, `'synthesizers'`.
|
|
11
|
+
*/
|
|
12
|
+
export interface NamedRegistry<T> {
|
|
13
|
+
get(name: string): T | undefined;
|
|
14
|
+
list(): ReadonlyArray<T>;
|
|
15
|
+
has(name: string): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Inter-plugin service registry. A plugin can **publish** a named service in its
|
|
20
|
+
* `onInit` hook (e.g. the vault plugin publishes its secret store) and another
|
|
21
|
+
* plugin can **consume** it in its own `onInit` — decoupling cross-plugin
|
|
22
|
+
* dependencies from the host's constructor wiring so a plugin can be
|
|
23
|
+
* discovery-loaded (default-exported, no `build*({deps})` closure) instead of
|
|
24
|
+
* hand-built by the orchestrator.
|
|
25
|
+
*
|
|
26
|
+
* Ordering is by plugin requirements: declare `moxxy.requirements` on the
|
|
27
|
+
* consumer (e.g. `@moxxy/plugin-oauth` requires `@moxxy/plugin-vault`) so the
|
|
28
|
+
* provider's `onInit` runs first and the service is registered before the
|
|
29
|
+
* consumer resolves it. Exposed on {@link AppContext.services}.
|
|
30
|
+
*
|
|
31
|
+
* Plugin `onInit` already runs with full in-process privileges (the security
|
|
32
|
+
* isolation wraps *tool* execution, not plugin code), so reaching a sibling
|
|
33
|
+
* plugin's service here doesn't widen the effective trust surface.
|
|
34
|
+
*/
|
|
35
|
+
export interface ServiceRegistry {
|
|
36
|
+
/** Publish a named service for other plugins to consume in their `onInit`. */
|
|
37
|
+
register<T>(name: string, impl: T): void;
|
|
38
|
+
/** Resolve a published service, or `undefined` when it isn't registered. */
|
|
39
|
+
get<T>(name: string): T | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a published service or throw — use when a declared requirement
|
|
42
|
+
* guarantees the provider ran first.
|
|
43
|
+
*/
|
|
44
|
+
require<T>(name: string): T;
|
|
45
|
+
/** Whether a service has been published. */
|
|
46
|
+
has(name: string): boolean;
|
|
47
|
+
}
|
package/src/session-like.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MoxxyEvent, UserPromptAttachment } from './events.js';
|
|
1
|
+
import type { MoxxyEvent, TriggerOrigin, UserPromptAttachment } from './events.js';
|
|
2
2
|
import type { SessionId, TurnId } from './ids.js';
|
|
3
3
|
import type { EventLogReader } from './log.js';
|
|
4
4
|
import type { ApprovalResolver, ModeBadge } from './mode.js';
|
|
@@ -22,6 +22,13 @@ export interface RunTurnOptions {
|
|
|
22
22
|
readonly signal?: AbortSignal;
|
|
23
23
|
/** Inline attachments shipped alongside the prompt (images, audio, stdin). */
|
|
24
24
|
readonly attachments?: ReadonlyArray<UserPromptAttachment>;
|
|
25
|
+
/**
|
|
26
|
+
* Provenance for a machine-initiated turn (a fired webhook/schedule/workflow).
|
|
27
|
+
* Stamped onto the resulting {@link UserPromptEvent} so renderers show a
|
|
28
|
+
* compact "trigger fired" marker instead of the raw synthesized prompt. Omit
|
|
29
|
+
* for an ordinary user-typed turn.
|
|
30
|
+
*/
|
|
31
|
+
readonly origin?: TriggerOrigin;
|
|
25
32
|
/**
|
|
26
33
|
* Pre-minted turn id. When omitted, `runTurn` mints one. The runner passes
|
|
27
34
|
* this so it can return the id to the client *before* the turn starts and
|
|
@@ -191,6 +198,12 @@ export interface WorkflowSummaryView {
|
|
|
191
198
|
readonly steps: number;
|
|
192
199
|
/** Human-readable trigger summary, e.g. `cron(0 8 * * *)` or `on-demand`. */
|
|
193
200
|
readonly triggers: string;
|
|
201
|
+
/**
|
|
202
|
+
* Session this workflow's triggered runs are pinned to (where they run +
|
|
203
|
+
* display), or null when unpinned. Optional for wire back-compat — an older
|
|
204
|
+
* runner omits it; the desktop treats absent as null.
|
|
205
|
+
*/
|
|
206
|
+
readonly targetSessionId?: string | null;
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
/** Result of running a workflow from the modal. */
|
|
@@ -278,6 +291,30 @@ export interface LoadedPluginView {
|
|
|
278
291
|
readonly kinds: ReadonlyArray<string>;
|
|
279
292
|
}
|
|
280
293
|
|
|
294
|
+
/** One swappable contribution within a {@link CategoryView}. */
|
|
295
|
+
export interface CategoryItemView {
|
|
296
|
+
/** Contribution name (e.g. provider `anthropic`, compactor `summarize`). */
|
|
297
|
+
readonly name: string;
|
|
298
|
+
/** Whether this item is the active default for its category. */
|
|
299
|
+
readonly isDefault: boolean;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* One registry category in {@link PluginsAdminView.categories} — the active
|
|
304
|
+
* default plus the available items to swap between. Powers the per-category
|
|
305
|
+
* tabs in the `/plugins` picker and the `list_defaults` tool.
|
|
306
|
+
*/
|
|
307
|
+
export interface CategoryView {
|
|
308
|
+
/** Registry kind, e.g. `provider` | `compactor` | `mode`. */
|
|
309
|
+
readonly category: string;
|
|
310
|
+
/** Active default contribution name, or null when the category is empty. */
|
|
311
|
+
readonly active: string | null;
|
|
312
|
+
/** The protected floor's name (never disableable), or null when none. */
|
|
313
|
+
readonly floor: string | null;
|
|
314
|
+
/** Swappable contributions registered for this category. */
|
|
315
|
+
readonly items: ReadonlyArray<CategoryItemView>;
|
|
316
|
+
}
|
|
317
|
+
|
|
281
318
|
/**
|
|
282
319
|
* The slice of plugin management a channel needs to drive the `/plugins`
|
|
283
320
|
* picker: which plugins are disabled, what's installable, and a persist+
|
|
@@ -288,12 +325,24 @@ export interface LoadedPluginView {
|
|
|
288
325
|
export interface PluginsAdminView {
|
|
289
326
|
/** Currently-loaded plugins with their contribution kinds (for kind tabs). */
|
|
290
327
|
loaded(): ReadonlyArray<LoadedPluginView>;
|
|
291
|
-
/** Package names currently disabled (config `plugins[name].enabled=false`). */
|
|
328
|
+
/** Package names currently disabled (config `plugins.packages[name].enabled=false`). */
|
|
292
329
|
disabled(): ReadonlyArray<string>;
|
|
330
|
+
/** Kernel package names that cannot be disabled (swap their default instead). */
|
|
331
|
+
protectedPackages(): ReadonlyArray<string>;
|
|
293
332
|
/** Curated installable-plugin catalog for the picker's "Installable" tab. */
|
|
294
333
|
catalog(): ReadonlyArray<InstallablePluginView>;
|
|
295
|
-
/** Persist `plugins[name].enabled` AND apply it to the live session. */
|
|
334
|
+
/** Persist `plugins.packages[name].enabled` AND apply it to the live session. */
|
|
296
335
|
setEnabled(packageName: string, enabled: boolean): Promise<void>;
|
|
336
|
+
/**
|
|
337
|
+
* Per-category active default + swappable items (the swap axis). Powers the
|
|
338
|
+
* `/plugins` category tabs and the `list_defaults` tool.
|
|
339
|
+
*/
|
|
340
|
+
categories(): ReadonlyArray<CategoryView>;
|
|
341
|
+
/**
|
|
342
|
+
* Persist `plugins.<category>.default` AND apply it to the live session
|
|
343
|
+
* (`setActive`). Rejects an unknown category or an unregistered name.
|
|
344
|
+
*/
|
|
345
|
+
setCategoryDefault(category: string, name: string): Promise<void>;
|
|
297
346
|
}
|
|
298
347
|
|
|
299
348
|
/**
|
package/src/stuck-loop.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createStuckLoopDetector, stableHash } from './mode-helpers.js';
|
|
|
3
3
|
|
|
4
4
|
describe('createStuckLoopDetector', () => {
|
|
5
5
|
it('trips on exact-input repeats at repeatThreshold', () => {
|
|
6
|
-
const d = createStuckLoopDetector(
|
|
6
|
+
const d = createStuckLoopDetector({ repeatThreshold: 3 });
|
|
7
7
|
const input = { x: 1 };
|
|
8
8
|
expect(d.record('Read', input).stuck).toBe(false);
|
|
9
9
|
expect(d.record('Read', input).stuck).toBe(false);
|
|
@@ -11,8 +11,21 @@ describe('createStuckLoopDetector', () => {
|
|
|
11
11
|
expect(sig).toMatchObject({ stuck: true, count: 3, kind: 'exact' });
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
it('uses a generous default exact threshold (does not trip on a few repeats)', () => {
|
|
15
|
+
const d = createStuckLoopDetector(); // default repeatThreshold = 8
|
|
16
|
+
const input = { x: 1 };
|
|
17
|
+
for (let i = 0; i < 7; i++) expect(d.record('Read', input).stuck).toBe(false);
|
|
18
|
+
expect(d.record('Read', input).stuck).toBe(true); // 8th identical call trips
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('never trips when disabled (relies on maxIterations alone)', () => {
|
|
22
|
+
const d = createStuckLoopDetector({ enabled: false, repeatThreshold: 2 });
|
|
23
|
+
const input = { x: 1 };
|
|
24
|
+
for (let i = 0; i < 20; i++) expect(d.record('Read', input).stuck).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
14
27
|
it('trips on same-target near-dups even when volatile args vary', () => {
|
|
15
|
-
const d = createStuckLoopDetector(); // nearThreshold 5
|
|
28
|
+
const d = createStuckLoopDetector({ repeatThreshold: 3 }); // nearThreshold → 5
|
|
16
29
|
const url = 'https://example.com/big';
|
|
17
30
|
// Same url, different maxBytes each time — exact check never fires.
|
|
18
31
|
for (let i = 0; i < 4; i++) {
|
|
@@ -44,7 +57,7 @@ describe('createStuckLoopDetector', () => {
|
|
|
44
57
|
// dispatch path; a throw there crashes the whole turn. These assert it stays
|
|
45
58
|
// total on hostile/partial input — the worst case the provider can hand us.
|
|
46
59
|
it('does not throw recording a tool input with a circular reference', () => {
|
|
47
|
-
const d = createStuckLoopDetector();
|
|
60
|
+
const d = createStuckLoopDetector({ repeatThreshold: 3 });
|
|
48
61
|
const input: Record<string, unknown> = { a: 1 };
|
|
49
62
|
input.self = input;
|
|
50
63
|
expect(() => d.record('weird', input)).not.toThrow();
|
package/src/tool-dispatch.ts
CHANGED
package/src/workflow.ts
CHANGED
|
@@ -151,6 +151,26 @@ export interface Workflow {
|
|
|
151
151
|
readonly enabled: boolean;
|
|
152
152
|
readonly inputs: Record<string, WorkflowInputSpec>;
|
|
153
153
|
readonly on?: WorkflowTrigger;
|
|
154
|
+
/**
|
|
155
|
+
* Pin this workflow's *triggered* runs (schedule / fileChanged / afterWorkflow)
|
|
156
|
+
* to one chosen runner's session — identified by that runner's
|
|
157
|
+
* `MOXXY_SESSION_ID` — so the run executes and displays in the session the user
|
|
158
|
+
* picked. On the desktop, one `moxxy serve` runner == one session.
|
|
159
|
+
*
|
|
160
|
+
* - schedule: the scheduler mirror row is stamped with this as its owner, so the
|
|
161
|
+
* existing poller owner-gate fires the run on the target runner.
|
|
162
|
+
* - fileChanged: only the runner whose id matches watches/fires the trigger. If
|
|
163
|
+
* that runner is offline when the file changes, the event is missed — there is
|
|
164
|
+
* no fs-event replay (unlike webhooks/schedules, which catch up).
|
|
165
|
+
* - afterWorkflow: a dependent pinned to a *different* session than the runner
|
|
166
|
+
* that observed the parent's completion is skipped (the completion event is
|
|
167
|
+
* in-process); co-locate the chain or use a schedule trigger for fan-out.
|
|
168
|
+
*
|
|
169
|
+
* Unset (the default) preserves today's ambient behavior: triggered runs fire
|
|
170
|
+
* once across runners via the cross-process lock. Ignored in single-process
|
|
171
|
+
* CLI/TUI (no `MOXXY_SESSION_ID`).
|
|
172
|
+
*/
|
|
173
|
+
readonly targetSessionId?: string;
|
|
154
174
|
readonly delivery?: WorkflowDelivery;
|
|
155
175
|
/** GUI-only metadata persisted with the YAML artifact. */
|
|
156
176
|
readonly ui?: WorkflowUi;
|