@swirls/looms 0.2.0 → 0.3.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/CHANGELOG.md +22 -0
- package/dist/actor/host.js +4 -6
- package/dist/actor/index.d.ts +1 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/routing.d.ts +5 -0
- package/dist/actor/routing.js +14 -0
- package/dist/agent/agent-session.js +1 -0
- package/dist/agent/debugger/agent-chat.js +45 -11
- package/dist/agent/debugger/summarize.js +2 -0
- package/dist/agent/effects.d.ts +30 -0
- package/dist/agent/events.d.ts +2 -0
- package/dist/agent/events.js +2 -0
- package/dist/agent/llm.js +1 -8
- package/dist/agent/module.d.ts +604 -534
- package/dist/agent/module.js +11 -7
- package/dist/agent/projections.d.ts +5 -0
- package/dist/agent/projections.js +43 -2
- package/dist/agent/scope.d.ts +2 -0
- package/dist/agent/signals.d.ts +5 -0
- package/dist/agent/signals.js +7 -0
- package/dist/agent/threads.js +35 -21
- package/dist/agent/transcript.d.ts +7 -0
- package/dist/agent/transcript.js +19 -0
- package/dist/agent/types.d.ts +10 -0
- package/dist/agent/types.js +4 -0
- package/dist/approval/debugger/approvals-view.js +5 -3
- package/dist/approval/effects.d.ts +12 -0
- package/dist/approval/module.d.ts +24 -0
- package/dist/client/client.d.ts +292 -0
- package/dist/client/client.js +17 -1
- package/dist/cloudflare/router.d.ts +1 -1
- package/dist/cloudflare/router.js +6 -8
- package/dist/core/effects.d.ts +7 -0
- package/dist/core/effects.js +1 -0
- package/dist/core/fold.js +12 -6
- package/dist/core/fork.d.ts +35 -0
- package/dist/core/fork.js +129 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/module-scope.d.ts +1 -0
- package/dist/core/module.d.ts +2 -2
- package/dist/core/projection.js +1 -0
- package/dist/core/protocol.d.ts +10 -1
- package/dist/core/protocol.js +11 -0
- package/dist/core/replay.d.ts +8 -0
- package/dist/core/snapshots.js +3 -17
- package/dist/core/state.d.ts +14 -0
- package/dist/core/state.js +25 -2
- package/dist/core/store.d.ts +1 -0
- package/dist/debugger/app/assets/{index-Qb371t_O.js → index-BevQYa-U.js} +2 -2
- package/dist/debugger/app/assets/index-CgDzWJg7.css +2 -0
- package/dist/debugger/app/assets/routes-B17Om93i.js +47 -0
- package/dist/debugger/app/index.html +2 -2
- package/dist/debugger/components/composer.d.ts +5 -2
- package/dist/debugger/components/composer.js +5 -4
- package/dist/debugger/components/run-tree.js +2 -5
- package/dist/debugger/events/catalog.js +1 -3
- package/dist/debugger/events/protocol-summarize.js +7 -0
- package/dist/debugger/lib/event-counts.d.ts +2 -2
- package/dist/debugger/lib/event-counts.js +5 -9
- package/dist/debugger/shell/debugger-shell.js +1 -1
- package/dist/debugger/shell/run-controls.d.ts +2 -1
- package/dist/debugger/shell/run-controls.js +40 -13
- package/dist/debugger/shell/run-search.js +1 -3
- package/dist/evaluator/effects.d.ts +12 -0
- package/dist/evaluator/module.d.ts +24 -0
- package/dist/projectors/index-model.d.ts +13 -7
- package/dist/projectors/index-model.js +21 -26
- package/dist/projectors/memory.js +19 -11
- package/dist/projectors/postgres.js +20 -19
- package/dist/projectors/schema.js +20 -18
- package/dist/projectors/sqlite.js +32 -27
- package/dist/react/materialize.js +16 -11
- package/dist/react/react.d.ts +6 -2
- package/dist/react/react.js +5 -2
- package/dist/react/tables.d.ts +3 -2
- package/dist/runtime/cancellation.js +14 -27
- package/dist/runtime/errors.d.ts +39 -0
- package/dist/runtime/errors.js +32 -0
- package/dist/runtime/helpers.d.ts +2 -0
- package/dist/runtime/helpers.js +23 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/looms.d.ts +4 -0
- package/dist/runtime/looms.js +1 -0
- package/dist/runtime/run-start.js +1 -3
- package/dist/runtime/runtime.d.ts +1 -1
- package/dist/runtime/runtime.js +45 -18
- package/dist/runtime/server/http.d.ts +2 -0
- package/dist/runtime/server/http.js +17 -2
- package/dist/runtime/server/routes/runs.js +35 -2
- package/dist/runtime/types.d.ts +8 -0
- package/dist/runtime/wake-engine.js +5 -2
- package/dist/s2/snapshot-store.js +2 -6
- package/dist/workflow/effects.d.ts +24 -0
- package/dist/workflow/effects.js +1 -0
- package/dist/workflow/module.d.ts +48 -0
- package/package.json +1 -1
- package/dist/debugger/app/assets/index-B3Ah8WAE.css +0 -2
- package/dist/debugger/app/assets/routes-CjtEHgPQ.js +0 -47
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Looms debugger</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-BevQYa-U.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-CbXtAM7H.js">
|
|
9
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CgDzWJg7.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* Message input with one primary action. Enter sends, Shift+Enter adds a newline,
|
|
4
|
-
* and `toolbar` holds secondary controls such as delivery mode.
|
|
4
|
+
* and `toolbar` holds secondary controls such as delivery mode. `onStop` adds a
|
|
5
|
+
* Stop button beside Send while work is in progress.
|
|
5
6
|
*/
|
|
6
|
-
export declare function Composer({ value, onChange, onSubmit, pending, disabled, placeholder, submitLabel, toolbar, error, className, }: {
|
|
7
|
+
export declare function Composer({ value, onChange, onSubmit, onStop, stopping, pending, disabled, placeholder, submitLabel, toolbar, error, className, }: {
|
|
7
8
|
value: string;
|
|
8
9
|
onChange: (value: string) => void;
|
|
9
10
|
onSubmit: () => void;
|
|
11
|
+
onStop?: () => void;
|
|
12
|
+
stopping?: boolean;
|
|
10
13
|
pending?: boolean;
|
|
11
14
|
disabled?: boolean;
|
|
12
15
|
placeholder?: string;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowUp, LoaderCircle } from 'lucide-react';
|
|
2
|
+
import { ArrowUp, LoaderCircle, Square } from 'lucide-react';
|
|
3
3
|
import { cn } from '../lib/cn.js';
|
|
4
4
|
import { Button } from '../ui/button.js';
|
|
5
5
|
import { Kbd } from '../ui/panel.js';
|
|
6
6
|
/**
|
|
7
7
|
* Message input with one primary action. Enter sends, Shift+Enter adds a newline,
|
|
8
|
-
* and `toolbar` holds secondary controls such as delivery mode.
|
|
8
|
+
* and `toolbar` holds secondary controls such as delivery mode. `onStop` adds a
|
|
9
|
+
* Stop button beside Send while work is in progress.
|
|
9
10
|
*/
|
|
10
|
-
export function Composer({ value, onChange, onSubmit, pending = false, disabled = false, placeholder = 'Message', submitLabel = 'Send', toolbar, error, className, }) {
|
|
11
|
+
export function Composer({ value, onChange, onSubmit, onStop, stopping = false, pending = false, disabled = false, placeholder = 'Message', submitLabel = 'Send', toolbar, error, className, }) {
|
|
11
12
|
const canSubmit = !pending && !disabled && value.trim().length > 0;
|
|
12
13
|
return (_jsxs("form", { className: cn('border-border shrink-0 border-t p-3', className), onSubmit: (event) => {
|
|
13
14
|
event.preventDefault();
|
|
@@ -21,5 +22,5 @@ export function Composer({ value, onChange, onSubmit, pending = false, disabled
|
|
|
21
22
|
onSubmit();
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
|
-
}, className: "placeholder:text-muted-foreground block field-sizing-content max-h-48 min-h-11 w-full resize-none bg-transparent px-3 pt-2.5 pb-1 text-[13px] leading-relaxed outline-none disabled:cursor-not-allowed disabled:opacity-50" }), _jsxs("div", { className: "flex h-10 items-center gap-1 pr-1.5 pl-1.5", children: [toolbar, _jsxs("span", { className: "text-muted-foreground ml-auto hidden items-center gap-1 pr-1 text-[10px] @sm:flex", children: [_jsx(Kbd, { children: "\u21B5" }), submitLabel.toLowerCase(), _jsx(Kbd, { className: "ml-1.5", children: "\u21E7\u21B5" }), "newline"] }), _jsx(Button, { type: "
|
|
25
|
+
}, className: "placeholder:text-muted-foreground block field-sizing-content max-h-48 min-h-11 w-full resize-none bg-transparent px-3 pt-2.5 pb-1 text-[13px] leading-relaxed outline-none disabled:cursor-not-allowed disabled:opacity-50" }), _jsxs("div", { className: "flex h-10 items-center gap-1 pr-1.5 pl-1.5", children: [toolbar, _jsxs("span", { className: "text-muted-foreground ml-auto hidden items-center gap-1 pr-1 text-[10px] @sm:flex", children: [_jsx(Kbd, { children: "\u21B5" }), submitLabel.toLowerCase(), _jsx(Kbd, { className: "ml-1.5", children: "\u21E7\u21B5" }), "newline"] }), onStop ? (_jsx(Button, { type: "button", size: "icon-sm", variant: "outline", "aria-label": "Stop", title: "Stop the current turn", disabled: stopping, onClick: onStop, className: "ml-auto rounded-full @sm:ml-0", children: stopping ? (_jsx(LoaderCircle, { className: "animate-spin" })) : (_jsx(Square, { className: "fill-current" })) })) : null, _jsx(Button, { type: "submit", size: "icon-sm", "aria-label": submitLabel, disabled: !canSubmit, className: cn('rounded-full', !onStop && 'ml-auto @sm:ml-0'), children: pending ? _jsx(LoaderCircle, { className: "animate-spin" }) : _jsx(ArrowUp, {}) })] })] }), error ? _jsx("p", { className: "text-status-failed mt-2 px-1 text-[11px]", children: error }) : null] }));
|
|
25
26
|
}
|
|
@@ -21,11 +21,8 @@ function flatten(root, collapsed) {
|
|
|
21
21
|
}
|
|
22
22
|
rows.push(next);
|
|
23
23
|
if (!collapsed.has(next.node.threadId)) {
|
|
24
|
-
for (
|
|
25
|
-
|
|
26
|
-
if (child) {
|
|
27
|
-
stack.push({ node: child, depth: next.depth + 1 });
|
|
28
|
-
}
|
|
24
|
+
for (const child of next.node.children.toReversed()) {
|
|
25
|
+
stack.push({ node: child, depth: next.depth + 1 });
|
|
29
26
|
}
|
|
30
27
|
}
|
|
31
28
|
}
|
|
@@ -12,9 +12,7 @@ function searchText(event, summary) {
|
|
|
12
12
|
/** Each event type belongs to the family view with the longest matching prefix. */
|
|
13
13
|
export function createEventCatalog(plugins = []) {
|
|
14
14
|
const views = [...plugins.flatMap((plugin) => plugin.families ?? []), ...builtinFamilies];
|
|
15
|
-
|
|
16
|
-
// oxlint-disable-next-line unicorn/no-array-sort
|
|
17
|
-
const byPrefix = views.sort((left, right) => prefixOf(right).length - prefixOf(left).length);
|
|
15
|
+
const byPrefix = views.toSorted((left, right) => prefixOf(right).length - prefixOf(left).length);
|
|
18
16
|
const colors = new Map(views.map((view) => [view.family, view.color]));
|
|
19
17
|
function viewOf(type) {
|
|
20
18
|
return byPrefix.find((view) => type.startsWith(prefixOf(view))) ?? runtimeFamily;
|
|
@@ -19,6 +19,13 @@ export function summarizeProtocolEvent(event) {
|
|
|
19
19
|
};
|
|
20
20
|
case 'runtime.run.cancelled':
|
|
21
21
|
return { title: 'run cancelled', detail: jsonText(payload.reason) };
|
|
22
|
+
case 'runtime.run.forked':
|
|
23
|
+
return {
|
|
24
|
+
title: 'run forked',
|
|
25
|
+
detail: `from ${jsonText(payload.sourceRunId) ?? 'unknown'} at seq ${jsonNumberText(payload.sourceSeq) ?? '?'}`,
|
|
26
|
+
};
|
|
27
|
+
case 'runtime.fork.elided':
|
|
28
|
+
return { title: 'left out of fork', detail: jsonText(payload.type) };
|
|
22
29
|
case 'runtime.thread.started':
|
|
23
30
|
return {
|
|
24
31
|
title: 'thread started',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type EventEnvelope } from '@swirls/looms/core';
|
|
1
|
+
import { type EventEnvelope, type RunStatus } from '@swirls/looms/core';
|
|
2
2
|
/** Build thread→count map via the shared EventIndex. */
|
|
3
3
|
export declare function countEventsByThread(events: readonly EventEnvelope[]): Map<string, number>;
|
|
4
|
-
export declare function runStatusFromEvents(events: readonly EventEnvelope[]):
|
|
4
|
+
export declare function runStatusFromEvents(events: readonly EventEnvelope[]): RunStatus;
|
|
5
5
|
export declare function startedAtFromEvents(events: readonly EventEnvelope[]): number | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { completedRunStatus, EventIndex } from '@swirls/looms/core';
|
|
2
2
|
/** Build thread→count map via the shared EventIndex. */
|
|
3
3
|
export function countEventsByThread(events) {
|
|
4
4
|
const index = new EventIndex();
|
|
@@ -6,15 +6,11 @@ export function countEventsByThread(events) {
|
|
|
6
6
|
return new Map(index.getCounts());
|
|
7
7
|
}
|
|
8
8
|
export function runStatusFromEvents(events) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
continue;
|
|
13
|
-
}
|
|
14
|
-
const payload = isJsonObject(event.payload) ? event.payload : {};
|
|
15
|
-
return isJsonString(payload.error) && payload.error.length > 0 ? 'failed' : 'completed';
|
|
9
|
+
const ended = events.findLast((event) => event.type === 'runtime.run.cancelled' || event.type === 'runtime.run.completed');
|
|
10
|
+
if (!ended) {
|
|
11
|
+
return 'running';
|
|
16
12
|
}
|
|
17
|
-
return '
|
|
13
|
+
return ended.type === 'runtime.run.cancelled' ? 'cancelled' : completedRunStatus(ended.payload);
|
|
18
14
|
}
|
|
19
15
|
export function startedAtFromEvents(events) {
|
|
20
16
|
const index = new EventIndex();
|
|
@@ -36,7 +36,7 @@ function DebuggerFrame({ selection, onSelectionChange, }) {
|
|
|
36
36
|
kind: run.kind ?? undefined,
|
|
37
37
|
name: run.definitionName ?? undefined,
|
|
38
38
|
run: run.runId,
|
|
39
|
-
}) }), _jsxs("div", { className: "ml-auto flex min-w-0 items-center gap-2", children: [error ? (_jsx("p", { className: "text-status-failed truncate text-[11px]", title: error, children: error })) : null, selection.run ? _jsx(RunControls, { runId: selection.run }) : null] })] }), _jsxs(ResizablePanelGroup, { id: "looms-debugger-columns", className: "min-h-0 flex-1", defaultLayout: layout.defaultLayout, onLayoutChanged: layout.onLayoutChanged, children: [_jsx(ResizablePanel, { id: "tree", defaultSize: "17%", minSize: "12rem", maxSize: "30%", className: "bg-sidebar", children: _jsx("section", { "aria-label": "Run tree", className: "h-full min-h-0 overflow-hidden", children: selection.run ? (_jsx(RunTreePane, { runId: selection.run, threadId: selection.thread, onSelectThread: (thread) => onSelectionChange({ ...selection, thread, seq: undefined }) })) : (_jsxs("div", { className: "flex h-full flex-col", children: [_jsx(PanelHeader, { children: _jsx(PanelTitle, { children: "Run tree" }) }), _jsx(EmptyState, { icon: _jsx(ListTree, {}), title: "No run selected", children: "Threads and child runs appear here once a run starts." })] })) }) }), _jsx(ResizableHandle, {}), _jsx(ResizablePanel, { id: "workspace", defaultSize: "29%", minSize: "16rem", children: _jsx("section", { "aria-label": "Workspace", className: "@container h-full min-h-0 overflow-hidden", children: _jsx(Workspace, { definition: selected, runId: selection.run, onStarted: (run) => onSelectionChange({ kind, name, run }) }) }) }), _jsx(ResizableHandle, {}), _jsx(ResizablePanel, { id: "events", defaultSize: "32%", minSize: "16rem", children: _jsx("section", { "aria-label": "Events", className: "h-full min-h-0 overflow-hidden", children: selection.run ? (_jsx(EventsPane, { runId: selection.run, threadId: selection.thread, seq: selection.seq, onSelectSeq: (seq) => onSelectionChange({ ...selection, seq }), onClearThread: () => onSelectionChange({ ...selection, thread: undefined }) })) : (_jsxs("div", { className: "flex h-full flex-col", children: [_jsx(PanelHeader, { children: _jsx(PanelTitle, { children: "Events" }) }), _jsx(EmptyState, { icon: _jsx(Activity, {}), title: "No events yet", children: "Events stream in live once a run starts." })] })) }) }), _jsx(ResizableHandle, {}), _jsx(ResizablePanel, { id: "projections", defaultSize: "22%", minSize: "14rem", className: "bg-sidebar", children: _jsx("section", { "aria-label": "Projections", className: "h-full min-h-0 overflow-hidden", children: _jsx(ProjectionsPanel, { runId: selection.run, names: catalog.projections }) }) })] })] }));
|
|
39
|
+
}) }), _jsxs("div", { className: "ml-auto flex min-w-0 items-center gap-2", children: [error ? (_jsx("p", { className: "text-status-failed truncate text-[11px]", title: error, children: error })) : null, selection.run ? (_jsx(RunControls, { runId: selection.run, onForked: (run) => onSelectionChange({ kind, name, run }) })) : null] })] }), _jsxs(ResizablePanelGroup, { id: "looms-debugger-columns", className: "min-h-0 flex-1", defaultLayout: layout.defaultLayout, onLayoutChanged: layout.onLayoutChanged, children: [_jsx(ResizablePanel, { id: "tree", defaultSize: "17%", minSize: "12rem", maxSize: "30%", className: "bg-sidebar", children: _jsx("section", { "aria-label": "Run tree", className: "h-full min-h-0 overflow-hidden", children: selection.run ? (_jsx(RunTreePane, { runId: selection.run, threadId: selection.thread, onSelectThread: (thread) => onSelectionChange({ ...selection, thread, seq: undefined }) })) : (_jsxs("div", { className: "flex h-full flex-col", children: [_jsx(PanelHeader, { children: _jsx(PanelTitle, { children: "Run tree" }) }), _jsx(EmptyState, { icon: _jsx(ListTree, {}), title: "No run selected", children: "Threads and child runs appear here once a run starts." })] })) }) }), _jsx(ResizableHandle, {}), _jsx(ResizablePanel, { id: "workspace", defaultSize: "29%", minSize: "16rem", children: _jsx("section", { "aria-label": "Workspace", className: "@container h-full min-h-0 overflow-hidden", children: _jsx(Workspace, { definition: selected, runId: selection.run, onStarted: (run) => onSelectionChange({ kind, name, run }) }) }) }), _jsx(ResizableHandle, {}), _jsx(ResizablePanel, { id: "events", defaultSize: "32%", minSize: "16rem", children: _jsx("section", { "aria-label": "Events", className: "h-full min-h-0 overflow-hidden", children: selection.run ? (_jsx(EventsPane, { runId: selection.run, threadId: selection.thread, seq: selection.seq, onSelectSeq: (seq) => onSelectionChange({ ...selection, seq }), onClearThread: () => onSelectionChange({ ...selection, thread: undefined }) })) : (_jsxs("div", { className: "flex h-full flex-col", children: [_jsx(PanelHeader, { children: _jsx(PanelTitle, { children: "Events" }) }), _jsx(EmptyState, { icon: _jsx(Activity, {}), title: "No events yet", children: "Events stream in live once a run starts." })] })) }) }), _jsx(ResizableHandle, {}), _jsx(ResizablePanel, { id: "projections", defaultSize: "22%", minSize: "14rem", className: "bg-sidebar", children: _jsx("section", { "aria-label": "Projections", className: "h-full min-h-0 overflow-hidden", children: _jsx(ProjectionsPanel, { runId: selection.run, names: catalog.projections }) }) })] })] }));
|
|
40
40
|
}
|
|
41
41
|
export function DebuggerShell({ plugins, selection, onSelectionChange, endpoint, }) {
|
|
42
42
|
return (_jsx(DebuggerProvider, { endpoint: endpoint, plugins: plugins, children: _jsx(DebuggerFrame, { selection: selection, onSelectionChange: onSelectionChange }) }));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Effect, Fiber } from 'effect';
|
|
3
|
+
import { AlarmClock, CircleStop, GitFork, Radio } from 'lucide-react';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
4
5
|
import { cleanUndefined } from '@swirls/looms/core';
|
|
5
6
|
import { useRunStore, useRunSummary } from '@swirls/looms/react';
|
|
6
7
|
import { useDebugger } from '../context.js';
|
|
@@ -10,7 +11,7 @@ import { Input } from '../ui/input.js';
|
|
|
10
11
|
import { Kbd } from '../ui/panel.js';
|
|
11
12
|
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover.js';
|
|
12
13
|
import { Textarea } from '../ui/textarea.js';
|
|
13
|
-
const
|
|
14
|
+
const NOTICE_MS = 3_000;
|
|
14
15
|
function useRunAction() {
|
|
15
16
|
const [error, setError] = useState(null);
|
|
16
17
|
const [pending, setPending] = useState(false);
|
|
@@ -18,7 +19,7 @@ function useRunAction() {
|
|
|
18
19
|
setPending(true);
|
|
19
20
|
setError(null);
|
|
20
21
|
action()
|
|
21
|
-
.then(() => onDone?.())
|
|
22
|
+
.then((value) => onDone?.(value))
|
|
22
23
|
.catch((cause) => setError(errorMessage(cause)))
|
|
23
24
|
.finally(() => setPending(false));
|
|
24
25
|
}
|
|
@@ -29,7 +30,7 @@ function Field({ label, hint, children }) {
|
|
|
29
30
|
}
|
|
30
31
|
function SignalForm({ runId, rootThreadId }) {
|
|
31
32
|
const { client } = useDebugger();
|
|
32
|
-
const [type, setType] = useState(
|
|
33
|
+
const [type, setType] = useState('');
|
|
33
34
|
const [payload, setPayload] = useState('{}');
|
|
34
35
|
const [thread, setThread] = useState('');
|
|
35
36
|
const [sent, setSent] = useState(false);
|
|
@@ -53,7 +54,7 @@ function SignalForm({ runId, rootThreadId }) {
|
|
|
53
54
|
return (_jsxs("form", { className: "grid gap-3", onSubmit: (event) => {
|
|
54
55
|
event.preventDefault();
|
|
55
56
|
submit();
|
|
56
|
-
}, children: [_jsxs("div", { children: [_jsx("p", { className: "text-[13px] font-medium", children: "Send a signal" }), _jsx("p", { className: "text-muted-foreground text-xs", children: "Append an event to this run's log." })] }), _jsx(Field, { label: "Type", children: _jsx(Input, { value: type, onChange: (event) => setType(event.target.value), className: "h-7 font-mono text-xs", autoFocus: true }) }), _jsx(Field, { label: "Thread", hint: "defaults to root", children: _jsx(Input, { value: thread, placeholder: rootThreadId ?? 'thread id', onChange: (event) => setThread(event.target.value), className: "h-7 font-mono text-xs" }) }), _jsx(Field, { label: "Payload", hint: "JSON", children: _jsx(Textarea, { value: payload, onChange: (event) => setPayload(event.target.value), onKeyDown: (event) => {
|
|
57
|
+
}, children: [_jsxs("div", { children: [_jsx("p", { className: "text-[13px] font-medium", children: "Send a signal" }), _jsx("p", { className: "text-muted-foreground text-xs", children: "Append an event to this run's log." })] }), _jsx(Field, { label: "Type", children: _jsx(Input, { value: type, placeholder: "module.event", onChange: (event) => setType(event.target.value), className: "h-7 font-mono text-xs", autoFocus: true }) }), _jsx(Field, { label: "Thread", hint: "defaults to root", children: _jsx(Input, { value: thread, placeholder: rootThreadId ?? 'thread id', onChange: (event) => setThread(event.target.value), className: "h-7 font-mono text-xs" }) }), _jsx(Field, { label: "Payload", hint: "JSON", children: _jsx(Textarea, { value: payload, onChange: (event) => setPayload(event.target.value), onKeyDown: (event) => {
|
|
57
58
|
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
|
58
59
|
event.preventDefault();
|
|
59
60
|
submit();
|
|
@@ -61,12 +62,38 @@ function SignalForm({ runId, rootThreadId }) {
|
|
|
61
62
|
}, className: "max-h-48 min-h-20 font-mono text-xs", spellCheck: false }) }), _jsxs("div", { className: "flex items-center gap-2", children: [error ? (_jsx("p", { className: "text-status-failed min-w-0 flex-1 truncate text-[11px]", title: error, children: error })) : sent ? (_jsx("p", { className: "text-status-completed flex-1 text-[11px]", children: "Signal sent" })) : (_jsxs("p", { className: "text-muted-foreground flex flex-1 items-center gap-1 text-[11px]", children: [_jsx(Kbd, { children: "\u2318" }), _jsx(Kbd, { children: "\u21B5" })] })), _jsx(Button, { type: "submit", size: "sm", disabled: pending || type.length === 0, children: "Send signal" })] })] }));
|
|
62
63
|
}
|
|
63
64
|
/** Run-scoped actions that live beside the run switcher. */
|
|
64
|
-
export function RunControls({ runId }) {
|
|
65
|
+
export function RunControls({ runId, onForked, }) {
|
|
65
66
|
const { client } = useDebugger();
|
|
66
|
-
const { rootThreadId,
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
const { rootThreadId, finished, resumable } = useRunSummary(useRunStore(runId));
|
|
68
|
+
// A wake holds its request open until the run parks, so Cancel needs its own pending state.
|
|
69
|
+
const waking = useRunAction();
|
|
70
|
+
const cancelling = useRunAction();
|
|
71
|
+
const forking = useRunAction();
|
|
72
|
+
const error = forking.error ?? cancelling.error ?? waking.error;
|
|
73
|
+
const [notice, setNotice] = useState(null);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!notice) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const clear = Effect.runFork(Effect.sleep(NOTICE_MS).pipe(Effect.andThen(Effect.sync(() => setNotice(null)))));
|
|
79
|
+
return () => {
|
|
80
|
+
Effect.runFork(Fiber.interrupt(clear));
|
|
81
|
+
};
|
|
82
|
+
}, [notice]);
|
|
83
|
+
function wake() {
|
|
84
|
+
setNotice(null);
|
|
85
|
+
cancelling.setError(null);
|
|
86
|
+
waking.run(() => client.wake(runId), ({ appended }) => setNotice(appended === 0
|
|
87
|
+
? 'Nothing pending'
|
|
88
|
+
: `Woke: ${appended} new event${appended === 1 ? '' : 's'}`));
|
|
89
|
+
}
|
|
90
|
+
return (_jsxs("div", { className: "flex items-center gap-0.5", children: [error ? (_jsx("span", { className: "text-status-failed mr-2 max-w-60 truncate text-[11px]", title: error, children: error })) : notice ? (_jsx("span", { role: "status", className: "text-muted-foreground mr-2 text-[11px]", children: notice })) : null, _jsxs(Button, { variant: "ghost", size: "sm", disabled: waking.pending, title: "Process pending work: due timers, retries, and effects stalled by a restart", onClick: wake, children: [_jsx(AlarmClock, {}), "Wake"] }), resumable ? (_jsxs(Button, { variant: "ghost", size: "sm", disabled: forking.pending, title: "Start a new run that picks up where this one stopped", onClick: () => {
|
|
91
|
+
setNotice(null);
|
|
92
|
+
waking.setError(null);
|
|
93
|
+
forking.run(() => client.fork(runId), (forked) => onForked(forked.runId));
|
|
94
|
+
}, children: [_jsx(GitFork, {}), "Continue"] })) : (_jsxs(Button, { variant: "destructive", size: "sm", disabled: cancelling.pending || finished, onClick: () => {
|
|
95
|
+
setNotice(null);
|
|
96
|
+
waking.setError(null);
|
|
97
|
+
cancelling.run(() => client.cancel(runId));
|
|
98
|
+
}, children: [_jsx(CircleStop, {}), "Cancel"] })), _jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "data-[state=open]:bg-accent data-[state=open]:text-foreground", children: [_jsx(Radio, {}), "Signal"] }) }), _jsx(PopoverContent, { align: "end", className: "w-80", children: _jsx(SignalForm, { runId: runId, rootThreadId: rootThreadId }) })] })] }));
|
|
72
99
|
}
|
|
@@ -43,9 +43,7 @@ function indexRuns(runs) {
|
|
|
43
43
|
.join(' ')
|
|
44
44
|
.toLowerCase(),
|
|
45
45
|
}));
|
|
46
|
-
|
|
47
|
-
// oxlint-disable-next-line unicorn/no-array-sort
|
|
48
|
-
return indexed.sort((left, right) => right.createdAt - left.createdAt);
|
|
46
|
+
return indexed.toSorted((left, right) => right.createdAt - left.createdAt);
|
|
49
47
|
}
|
|
50
48
|
function matches(entry, terms) {
|
|
51
49
|
return terms.every((term) => entry.haystack.includes(term));
|
|
@@ -120,6 +120,11 @@ export declare const evaluateEffect: NoInfer<import("@swirls/looms/core").Effect
|
|
|
120
120
|
readonly attempt: number;
|
|
121
121
|
readonly deadlineAt: number | null;
|
|
122
122
|
};
|
|
123
|
+
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
124
|
+
readonly type: "runtime.fork.elided";
|
|
125
|
+
readonly payload: {
|
|
126
|
+
readonly type: string;
|
|
127
|
+
};
|
|
123
128
|
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
124
129
|
readonly type: "runtime.run.cancelled";
|
|
125
130
|
readonly payload: {
|
|
@@ -131,6 +136,12 @@ export declare const evaluateEffect: NoInfer<import("@swirls/looms/core").Effect
|
|
|
131
136
|
readonly output: Schema.Json;
|
|
132
137
|
readonly error: string | null;
|
|
133
138
|
};
|
|
139
|
+
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
140
|
+
readonly type: "runtime.run.forked";
|
|
141
|
+
readonly payload: {
|
|
142
|
+
readonly sourceRunId: string;
|
|
143
|
+
readonly sourceSeq: number;
|
|
144
|
+
};
|
|
134
145
|
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
135
146
|
readonly type: "runtime.run.started";
|
|
136
147
|
readonly payload: {
|
|
@@ -163,6 +174,7 @@ export declare const evaluateEffect: NoInfer<import("@swirls/looms/core").Effect
|
|
|
163
174
|
readonly payload: {
|
|
164
175
|
readonly threadId: string;
|
|
165
176
|
readonly reason?: string | undefined;
|
|
177
|
+
readonly resumeSeq?: number | undefined;
|
|
166
178
|
};
|
|
167
179
|
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
168
180
|
readonly type: "runtime.thread.completed";
|
|
@@ -155,6 +155,11 @@ export declare function evaluator(options?: EvaluatorModuleOptions): import("@sw
|
|
|
155
155
|
readonly attempt: number;
|
|
156
156
|
readonly deadlineAt: number | null;
|
|
157
157
|
};
|
|
158
|
+
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
159
|
+
readonly type: "runtime.fork.elided";
|
|
160
|
+
readonly payload: {
|
|
161
|
+
readonly type: string;
|
|
162
|
+
};
|
|
158
163
|
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
159
164
|
readonly type: "runtime.run.cancelled";
|
|
160
165
|
readonly payload: {
|
|
@@ -166,6 +171,12 @@ export declare function evaluator(options?: EvaluatorModuleOptions): import("@sw
|
|
|
166
171
|
readonly output: import("effect/Schema").Json;
|
|
167
172
|
readonly error: string | null;
|
|
168
173
|
};
|
|
174
|
+
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
175
|
+
readonly type: "runtime.run.forked";
|
|
176
|
+
readonly payload: {
|
|
177
|
+
readonly sourceRunId: string;
|
|
178
|
+
readonly sourceSeq: number;
|
|
179
|
+
};
|
|
169
180
|
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
170
181
|
readonly type: "runtime.run.started";
|
|
171
182
|
readonly payload: {
|
|
@@ -198,6 +209,7 @@ export declare function evaluator(options?: EvaluatorModuleOptions): import("@sw
|
|
|
198
209
|
readonly payload: {
|
|
199
210
|
readonly threadId: string;
|
|
200
211
|
readonly reason?: string | undefined;
|
|
212
|
+
readonly resumeSeq?: number | undefined;
|
|
201
213
|
};
|
|
202
214
|
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
203
215
|
readonly type: "runtime.thread.completed";
|
|
@@ -438,6 +450,11 @@ export declare function evaluator(options?: EvaluatorModuleOptions): import("@sw
|
|
|
438
450
|
readonly attempt: number;
|
|
439
451
|
readonly deadlineAt: number | null;
|
|
440
452
|
};
|
|
453
|
+
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
454
|
+
readonly type: "runtime.fork.elided";
|
|
455
|
+
readonly payload: {
|
|
456
|
+
readonly type: string;
|
|
457
|
+
};
|
|
441
458
|
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
442
459
|
readonly type: "runtime.run.cancelled";
|
|
443
460
|
readonly payload: {
|
|
@@ -449,6 +466,12 @@ export declare function evaluator(options?: EvaluatorModuleOptions): import("@sw
|
|
|
449
466
|
readonly output: import("effect/Schema").Json;
|
|
450
467
|
readonly error: string | null;
|
|
451
468
|
};
|
|
469
|
+
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
470
|
+
readonly type: "runtime.run.forked";
|
|
471
|
+
readonly payload: {
|
|
472
|
+
readonly sourceRunId: string;
|
|
473
|
+
readonly sourceSeq: number;
|
|
474
|
+
};
|
|
452
475
|
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
453
476
|
readonly type: "runtime.run.started";
|
|
454
477
|
readonly payload: {
|
|
@@ -481,6 +504,7 @@ export declare function evaluator(options?: EvaluatorModuleOptions): import("@sw
|
|
|
481
504
|
readonly payload: {
|
|
482
505
|
readonly threadId: string;
|
|
483
506
|
readonly reason?: string | undefined;
|
|
507
|
+
readonly resumeSeq?: number | undefined;
|
|
484
508
|
};
|
|
485
509
|
}) | (Omit<import("@swirls/looms/core").EventInput, "payload" | "type"> & {
|
|
486
510
|
readonly type: "runtime.thread.completed";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EventEnvelope } from '@swirls/looms/core';
|
|
2
2
|
import type { Projector } from './projector';
|
|
3
|
+
/** A run (`actorId === runId`) or one of its threads. Thread ids are unique within a run. */
|
|
3
4
|
export interface ActorIndexRow {
|
|
5
|
+
runId: string;
|
|
4
6
|
actorId: string;
|
|
5
7
|
kind: string | null;
|
|
6
8
|
status: string | null;
|
|
@@ -9,14 +11,15 @@ export interface ActorIndexRow {
|
|
|
9
11
|
updatedAt: number;
|
|
10
12
|
}
|
|
11
13
|
export interface ReviewIndexRow {
|
|
14
|
+
runId: string;
|
|
12
15
|
reviewId: string;
|
|
13
|
-
actorId: string;
|
|
14
16
|
status: string;
|
|
15
17
|
title: string;
|
|
16
18
|
updatedAt: number;
|
|
17
19
|
}
|
|
18
20
|
export type IndexOp = {
|
|
19
21
|
type: 'upsertActor';
|
|
22
|
+
runId: string;
|
|
20
23
|
actorId: string;
|
|
21
24
|
kind: string | null;
|
|
22
25
|
status: string | null;
|
|
@@ -25,32 +28,35 @@ export type IndexOp = {
|
|
|
25
28
|
updatedAt: number;
|
|
26
29
|
} | {
|
|
27
30
|
type: 'setActorStatus';
|
|
31
|
+
runId: string;
|
|
28
32
|
actorId: string;
|
|
29
33
|
status: string;
|
|
30
34
|
updatedAt: number;
|
|
31
35
|
} | {
|
|
32
36
|
type: 'upsertReview';
|
|
37
|
+
runId: string;
|
|
33
38
|
reviewId: string;
|
|
34
|
-
actorId: string;
|
|
35
39
|
status: string;
|
|
36
40
|
title: string;
|
|
37
41
|
updatedAt: number;
|
|
38
42
|
} | {
|
|
39
43
|
type: 'setReviewStatus';
|
|
44
|
+
runId: string;
|
|
40
45
|
reviewId: string;
|
|
41
46
|
status: string;
|
|
42
47
|
updatedAt: number;
|
|
43
48
|
};
|
|
44
49
|
export interface IndexBackend {
|
|
45
50
|
applyOps: (ops: ReadonlyArray<IndexOp>) => Promise<void>;
|
|
46
|
-
getActor: (actorId: string) => Promise<ActorIndexRow | null>;
|
|
47
|
-
listReviews: (
|
|
51
|
+
getActor: (runId: string, actorId: string) => Promise<ActorIndexRow | null>;
|
|
52
|
+
listReviews: (runId?: string) => Promise<ReviewIndexRow[]>;
|
|
48
53
|
init?: () => Promise<void>;
|
|
49
54
|
dispose?: () => Promise<void>;
|
|
50
55
|
}
|
|
51
56
|
export interface IndexProjector extends Projector {
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
/** The run's row, or one of its threads when `actorId` is given. */
|
|
58
|
+
getActor: (runId: string, actorId?: string) => Promise<ActorIndexRow | null>;
|
|
59
|
+
listReviews: (runId?: string) => Promise<ReviewIndexRow[]>;
|
|
54
60
|
}
|
|
55
61
|
export declare function indexOpsFor(event: EventEnvelope): IndexOp[];
|
|
56
62
|
export declare function createIndexProjector(name: string, backend: IndexBackend, version?: string): IndexProjector;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Predicate } from 'effect';
|
|
2
|
+
import { completedRunStatus } from '@swirls/looms/core';
|
|
2
3
|
function payloadObject(event) {
|
|
3
4
|
if (!Predicate.isObject(event.payload)) {
|
|
4
5
|
return {};
|
|
@@ -11,12 +12,15 @@ function readString(obj, key) {
|
|
|
11
12
|
}
|
|
12
13
|
export function indexOpsFor(event) {
|
|
13
14
|
const payload = payloadObject(event);
|
|
15
|
+
const runId = event.runId;
|
|
16
|
+
const actorId = readString(payload, 'threadId') ?? event.threadId ?? runId;
|
|
14
17
|
switch (event.type) {
|
|
15
18
|
case 'runtime.run.started':
|
|
16
19
|
return [
|
|
17
20
|
{
|
|
18
21
|
type: 'upsertActor',
|
|
19
|
-
|
|
22
|
+
runId,
|
|
23
|
+
actorId: runId,
|
|
20
24
|
kind: readString(payload, 'kind') ?? null,
|
|
21
25
|
status: 'running',
|
|
22
26
|
definitionName: readString(payload, 'definitionName') ?? null,
|
|
@@ -28,7 +32,8 @@ export function indexOpsFor(event) {
|
|
|
28
32
|
return [
|
|
29
33
|
{
|
|
30
34
|
type: 'upsertActor',
|
|
31
|
-
|
|
35
|
+
runId,
|
|
36
|
+
actorId,
|
|
32
37
|
kind: readString(payload, 'kind') ?? null,
|
|
33
38
|
status: 'running',
|
|
34
39
|
definitionName: readString(payload, 'definitionName') ?? null,
|
|
@@ -37,39 +42,28 @@ export function indexOpsFor(event) {
|
|
|
37
42
|
},
|
|
38
43
|
];
|
|
39
44
|
case 'runtime.run.completed':
|
|
40
|
-
case 'runtime.thread.completed':
|
|
41
45
|
return [
|
|
42
46
|
{
|
|
43
47
|
type: 'setActorStatus',
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
runId,
|
|
49
|
+
actorId: runId,
|
|
50
|
+
status: completedRunStatus(event.payload),
|
|
46
51
|
updatedAt: event.ts,
|
|
47
52
|
},
|
|
48
53
|
];
|
|
54
|
+
case 'runtime.thread.completed':
|
|
55
|
+
return [{ type: 'setActorStatus', runId, actorId, status: 'completed', updatedAt: event.ts }];
|
|
49
56
|
case 'runtime.thread.failed':
|
|
50
|
-
return [
|
|
51
|
-
|
|
52
|
-
type: 'setActorStatus',
|
|
53
|
-
actorId: readString(payload, 'threadId') ?? event.threadId ?? event.runId,
|
|
54
|
-
status: 'failed',
|
|
55
|
-
updatedAt: event.ts,
|
|
56
|
-
},
|
|
57
|
-
];
|
|
57
|
+
return [{ type: 'setActorStatus', runId, actorId, status: 'failed', updatedAt: event.ts }];
|
|
58
|
+
case 'runtime.run.cancelled':
|
|
58
59
|
case 'runtime.thread.cancelled':
|
|
59
|
-
return [
|
|
60
|
-
{
|
|
61
|
-
type: 'setActorStatus',
|
|
62
|
-
actorId: readString(payload, 'threadId') ?? event.threadId ?? event.runId,
|
|
63
|
-
status: 'cancelled',
|
|
64
|
-
updatedAt: event.ts,
|
|
65
|
-
},
|
|
66
|
-
];
|
|
60
|
+
return [{ type: 'setActorStatus', runId, actorId, status: 'cancelled', updatedAt: event.ts }];
|
|
67
61
|
case 'approval.requested':
|
|
68
62
|
return [
|
|
69
63
|
{
|
|
70
64
|
type: 'upsertReview',
|
|
65
|
+
runId,
|
|
71
66
|
reviewId: readString(payload, 'approvalId') ?? 'unknown',
|
|
72
|
-
actorId: event.runId,
|
|
73
67
|
status: 'pending',
|
|
74
68
|
title: readString(payload, 'title') ?? 'Approval',
|
|
75
69
|
updatedAt: event.ts,
|
|
@@ -79,8 +73,8 @@ export function indexOpsFor(event) {
|
|
|
79
73
|
return [
|
|
80
74
|
{
|
|
81
75
|
type: 'upsertReview',
|
|
76
|
+
runId,
|
|
82
77
|
reviewId: readString(payload, 'approvalId') ?? 'unknown',
|
|
83
|
-
actorId: event.runId,
|
|
84
78
|
status: payload.outcome === 'reject' ? 'rejected' : 'approved',
|
|
85
79
|
title: 'Approval',
|
|
86
80
|
updatedAt: event.ts,
|
|
@@ -90,6 +84,7 @@ export function indexOpsFor(event) {
|
|
|
90
84
|
return [
|
|
91
85
|
{
|
|
92
86
|
type: 'setReviewStatus',
|
|
87
|
+
runId,
|
|
93
88
|
reviewId: readString(payload, 'approvalId') ?? 'unknown',
|
|
94
89
|
status: 'timed_out',
|
|
95
90
|
updatedAt: event.ts,
|
|
@@ -99,7 +94,7 @@ export function indexOpsFor(event) {
|
|
|
99
94
|
return [];
|
|
100
95
|
}
|
|
101
96
|
}
|
|
102
|
-
export function createIndexProjector(name, backend, version = '
|
|
97
|
+
export function createIndexProjector(name, backend, version = '2') {
|
|
103
98
|
return {
|
|
104
99
|
name,
|
|
105
100
|
version,
|
|
@@ -109,7 +104,7 @@ export function createIndexProjector(name, backend, version = '1') {
|
|
|
109
104
|
const ops = events.flatMap(indexOpsFor);
|
|
110
105
|
return ops.length === 0 ? Promise.resolve() : backend.applyOps(ops);
|
|
111
106
|
},
|
|
112
|
-
getActor: (actorId) => backend.getActor(actorId),
|
|
113
|
-
listReviews: (
|
|
107
|
+
getActor: (runId, actorId) => backend.getActor(runId, actorId ?? runId),
|
|
108
|
+
listReviews: (runId) => backend.listReviews(runId),
|
|
114
109
|
};
|
|
115
110
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { createIndexProjector, } from './index-model.js';
|
|
2
2
|
import { runProjectorSync } from './projector.js';
|
|
3
|
+
function rowKey(runId, id) {
|
|
4
|
+
return `${runId}\0${id}`;
|
|
5
|
+
}
|
|
3
6
|
function applyOp(actors, reviews, op) {
|
|
4
7
|
switch (op.type) {
|
|
5
8
|
case 'upsertActor':
|
|
6
|
-
actors.set(op.actorId, {
|
|
9
|
+
actors.set(rowKey(op.runId, op.actorId), {
|
|
10
|
+
runId: op.runId,
|
|
7
11
|
actorId: op.actorId,
|
|
8
12
|
kind: op.kind,
|
|
9
13
|
status: op.status,
|
|
@@ -13,8 +17,10 @@ function applyOp(actors, reviews, op) {
|
|
|
13
17
|
});
|
|
14
18
|
return;
|
|
15
19
|
case 'setActorStatus': {
|
|
16
|
-
const
|
|
17
|
-
actors.
|
|
20
|
+
const key = rowKey(op.runId, op.actorId);
|
|
21
|
+
const existing = actors.get(key);
|
|
22
|
+
actors.set(key, {
|
|
23
|
+
runId: op.runId,
|
|
18
24
|
actorId: op.actorId,
|
|
19
25
|
kind: existing?.kind ?? null,
|
|
20
26
|
status: op.status,
|
|
@@ -25,10 +31,11 @@ function applyOp(actors, reviews, op) {
|
|
|
25
31
|
return;
|
|
26
32
|
}
|
|
27
33
|
case 'upsertReview': {
|
|
28
|
-
const
|
|
29
|
-
reviews.
|
|
34
|
+
const key = rowKey(op.runId, op.reviewId);
|
|
35
|
+
const existing = reviews.get(key);
|
|
36
|
+
reviews.set(key, {
|
|
37
|
+
runId: op.runId,
|
|
30
38
|
reviewId: op.reviewId,
|
|
31
|
-
actorId: op.actorId,
|
|
32
39
|
status: op.status,
|
|
33
40
|
title: existing?.title ?? op.title,
|
|
34
41
|
updatedAt: op.updatedAt,
|
|
@@ -36,9 +43,10 @@ function applyOp(actors, reviews, op) {
|
|
|
36
43
|
return;
|
|
37
44
|
}
|
|
38
45
|
case 'setReviewStatus': {
|
|
39
|
-
const
|
|
46
|
+
const key = rowKey(op.runId, op.reviewId);
|
|
47
|
+
const existing = reviews.get(key);
|
|
40
48
|
if (existing) {
|
|
41
|
-
reviews.set(
|
|
49
|
+
reviews.set(key, { ...existing, status: op.status, updatedAt: op.updatedAt });
|
|
42
50
|
}
|
|
43
51
|
return;
|
|
44
52
|
}
|
|
@@ -57,10 +65,10 @@ export function memory() {
|
|
|
57
65
|
applyOp(actors, reviews, op);
|
|
58
66
|
}
|
|
59
67
|
}),
|
|
60
|
-
getActor: (actorId) => runProjectorSync('memory', () => actors.get(actorId) ?? null),
|
|
61
|
-
listReviews: (
|
|
68
|
+
getActor: (runId, actorId) => runProjectorSync('memory', () => actors.get(rowKey(runId, actorId)) ?? null),
|
|
69
|
+
listReviews: (runId) => runProjectorSync('memory', () => {
|
|
62
70
|
const rows = [...reviews.values()];
|
|
63
|
-
return
|
|
71
|
+
return runId ? rows.filter((row) => row.runId === runId) : rows;
|
|
64
72
|
}),
|
|
65
73
|
dispose: () => runProjectorSync('memory', () => {
|
|
66
74
|
actors.clear();
|