@agent-native/core 0.131.7 → 0.131.9
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +24 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/application-state/store.ts +18 -31
- package/corpus/core/src/cli/create.ts +115 -16
- package/corpus/core/src/client/AssistantChat.tsx +10 -2
- package/corpus/core/src/client/frame-protocol.ts +5 -1
- package/corpus/core/src/workspace-files/tool.ts +18 -11
- package/corpus/templates/clips/actions/generate-workflow.ts +6 -4
- package/corpus/templates/clips/actions/reconcile-workflow-generation.ts +257 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +166 -4
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -1
- package/corpus/templates/clips/changelog/2026-07-27-stopped-agent-runs-no-longer-leave-generated-workflow-cards-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-30-workflow-generation-now-stops-retrying-after-repeated-failur.md +6 -0
- package/corpus/templates/clips/shared/workflow.ts +5 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +20 -36
- package/dist/application-state/store.js.map +1 -1
- package/dist/cli/create.d.ts +8 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +98 -16
- package/dist/cli/create.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +10 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/frame-protocol.d.ts +1 -0
- package/dist/client/frame-protocol.d.ts.map +1 -1
- package/dist/client/frame-protocol.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/workspace-files/tool.d.ts.map +1 -1
- package/dist/workspace-files/tool.js +18 -11
- package/dist/workspace-files/tool.js.map +1 -1
- package/package.json +3 -3
- package/src/application-state/store.ts +18 -31
- package/src/cli/create.ts +115 -16
- package/src/client/AssistantChat.tsx +10 -2
- package/src/client/frame-protocol.ts +5 -1
- package/src/workspace-files/tool.ts +18 -11
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
compareAndSetAppState,
|
|
4
|
+
compareAndSetManyAppState,
|
|
5
|
+
readAppState,
|
|
6
|
+
} from "@agent-native/core/application-state";
|
|
7
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { WorkflowKindSchema } from "../shared/workflow.js";
|
|
11
|
+
|
|
12
|
+
const WorkflowStateSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
kind: WorkflowKindSchema.optional(),
|
|
15
|
+
status: z.string().optional(),
|
|
16
|
+
content: z.string().optional(),
|
|
17
|
+
recordingId: z.string().optional(),
|
|
18
|
+
requestedAt: z.string().optional(),
|
|
19
|
+
tabId: z.string().optional(),
|
|
20
|
+
claimedAt: z.string().optional(),
|
|
21
|
+
})
|
|
22
|
+
.passthrough();
|
|
23
|
+
|
|
24
|
+
const WorkflowRequestSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
requestedAt: z.string(),
|
|
27
|
+
deliveredAt: z.string().optional(),
|
|
28
|
+
deliveredTabId: z.string().optional(),
|
|
29
|
+
})
|
|
30
|
+
.passthrough();
|
|
31
|
+
|
|
32
|
+
const CLAIM_LEASE_MS = 30_000;
|
|
33
|
+
|
|
34
|
+
export default defineAction({
|
|
35
|
+
description:
|
|
36
|
+
"Track and reconcile the agent run responsible for a generated workflow.",
|
|
37
|
+
agentTool: false,
|
|
38
|
+
schema: z.object({
|
|
39
|
+
operation: z.enum([
|
|
40
|
+
"track",
|
|
41
|
+
"release",
|
|
42
|
+
"mark-delivered",
|
|
43
|
+
"consume",
|
|
44
|
+
"stop",
|
|
45
|
+
]),
|
|
46
|
+
recordingId: z.string().min(1),
|
|
47
|
+
requestedAt: z.string().min(1),
|
|
48
|
+
tabId: z.string().min(1),
|
|
49
|
+
}),
|
|
50
|
+
run: async ({ operation, recordingId, requestedAt, tabId }) => {
|
|
51
|
+
await assertAccess("recording", recordingId, "viewer");
|
|
52
|
+
|
|
53
|
+
const requestKey = `clips-ai-request-${recordingId}`;
|
|
54
|
+
if (operation === "mark-delivered" || operation === "consume") {
|
|
55
|
+
const rawRequest = await readAppState(requestKey);
|
|
56
|
+
if (rawRequest === null) {
|
|
57
|
+
return operation === "consume"
|
|
58
|
+
? { reconciled: false, consumed: true, reason: "missing" as const }
|
|
59
|
+
: { reconciled: false, delivered: true, reason: "missing" as const };
|
|
60
|
+
}
|
|
61
|
+
const parsedRequest = WorkflowRequestSchema.safeParse(rawRequest);
|
|
62
|
+
if (!parsedRequest.success) {
|
|
63
|
+
throw new Error(`Invalid workflow request state for ${recordingId}`);
|
|
64
|
+
}
|
|
65
|
+
if (parsedRequest.data.requestedAt !== requestedAt) {
|
|
66
|
+
return operation === "consume"
|
|
67
|
+
? {
|
|
68
|
+
reconciled: false,
|
|
69
|
+
consumed: true,
|
|
70
|
+
reason: "newer-request" as const,
|
|
71
|
+
}
|
|
72
|
+
: {
|
|
73
|
+
reconciled: false,
|
|
74
|
+
delivered: true,
|
|
75
|
+
reason: "newer-request" as const,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (operation === "mark-delivered") {
|
|
79
|
+
if (parsedRequest.data.deliveredTabId === tabId) {
|
|
80
|
+
return { reconciled: false, delivered: true };
|
|
81
|
+
}
|
|
82
|
+
if (parsedRequest.data.deliveredTabId) {
|
|
83
|
+
return {
|
|
84
|
+
reconciled: false,
|
|
85
|
+
delivered: false,
|
|
86
|
+
reason: "different-run" as const,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const stateKey = `clips-workflow-${recordingId}`;
|
|
91
|
+
const rawState = await readAppState(stateKey);
|
|
92
|
+
if (rawState === null) {
|
|
93
|
+
return { reconciled: false, delivered: false, reason: "missing" };
|
|
94
|
+
}
|
|
95
|
+
const parsedState = WorkflowStateSchema.safeParse(rawState);
|
|
96
|
+
if (!parsedState.success) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Invalid generated workflow state for ${recordingId}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (parsedState.data.status !== "generating") {
|
|
102
|
+
const consumed = await compareAndSetAppState(
|
|
103
|
+
requestKey,
|
|
104
|
+
rawRequest,
|
|
105
|
+
null,
|
|
106
|
+
);
|
|
107
|
+
return consumed
|
|
108
|
+
? {
|
|
109
|
+
reconciled: false,
|
|
110
|
+
delivered: true,
|
|
111
|
+
consumed: true,
|
|
112
|
+
reason: "terminal",
|
|
113
|
+
}
|
|
114
|
+
: {
|
|
115
|
+
reconciled: false,
|
|
116
|
+
delivered: false,
|
|
117
|
+
consumed: false,
|
|
118
|
+
reason: "stale",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
parsedState.data.requestedAt !== requestedAt ||
|
|
123
|
+
parsedState.data.tabId !== tabId
|
|
124
|
+
) {
|
|
125
|
+
return {
|
|
126
|
+
reconciled: false,
|
|
127
|
+
delivered: false,
|
|
128
|
+
reason: "different-run",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const deliveredAt = new Date().toISOString();
|
|
133
|
+
const delivered = await compareAndSetManyAppState([
|
|
134
|
+
{
|
|
135
|
+
key: stateKey,
|
|
136
|
+
expectedValue: rawState,
|
|
137
|
+
nextValue: { ...rawState, claimedAt: deliveredAt },
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
key: requestKey,
|
|
141
|
+
expectedValue: rawRequest,
|
|
142
|
+
nextValue: { ...rawRequest, deliveredAt, deliveredTabId: tabId },
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
return delivered
|
|
146
|
+
? { reconciled: false, delivered: true }
|
|
147
|
+
: { reconciled: false, delivered: false, reason: "stale" as const };
|
|
148
|
+
}
|
|
149
|
+
if (parsedRequest.data.deliveredTabId !== tabId) {
|
|
150
|
+
return {
|
|
151
|
+
reconciled: false,
|
|
152
|
+
consumed: false,
|
|
153
|
+
reason: "not-delivered" as const,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const consumed = await compareAndSetAppState(
|
|
157
|
+
requestKey,
|
|
158
|
+
rawRequest,
|
|
159
|
+
null,
|
|
160
|
+
);
|
|
161
|
+
return consumed
|
|
162
|
+
? { reconciled: false, consumed: true }
|
|
163
|
+
: { reconciled: false, consumed: false, reason: "stale" as const };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const stateKey = `clips-workflow-${recordingId}`;
|
|
167
|
+
const rawState = await readAppState(stateKey);
|
|
168
|
+
if (rawState === null) {
|
|
169
|
+
return { reconciled: false, reason: "missing" as const };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const parsedState = WorkflowStateSchema.safeParse(rawState);
|
|
173
|
+
if (!parsedState.success) {
|
|
174
|
+
throw new Error(`Invalid generated workflow state for ${recordingId}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const state = parsedState.data;
|
|
178
|
+
if (state.status !== "generating") {
|
|
179
|
+
return { reconciled: false, reason: "terminal" as const };
|
|
180
|
+
}
|
|
181
|
+
if (state.requestedAt !== requestedAt) {
|
|
182
|
+
return { reconciled: false, reason: "newer-request" as const };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (operation === "track") {
|
|
186
|
+
if (state.tabId === tabId) {
|
|
187
|
+
return { reconciled: false, tracked: true };
|
|
188
|
+
}
|
|
189
|
+
if (state.tabId) {
|
|
190
|
+
const claimedAt = Date.parse(
|
|
191
|
+
state.claimedAt ?? state.requestedAt ?? "",
|
|
192
|
+
);
|
|
193
|
+
if (
|
|
194
|
+
!Number.isFinite(claimedAt) ||
|
|
195
|
+
Date.now() - claimedAt < CLAIM_LEASE_MS
|
|
196
|
+
) {
|
|
197
|
+
return {
|
|
198
|
+
reconciled: false,
|
|
199
|
+
tracked: false,
|
|
200
|
+
reason: "claimed" as const,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const rawRequest = await readAppState(requestKey);
|
|
205
|
+
if (rawRequest === null) {
|
|
206
|
+
return { reconciled: false, reason: "request-missing" as const };
|
|
207
|
+
}
|
|
208
|
+
const parsedRequest = WorkflowRequestSchema.safeParse(rawRequest);
|
|
209
|
+
if (!parsedRequest.success) {
|
|
210
|
+
throw new Error(`Invalid workflow request state for ${recordingId}`);
|
|
211
|
+
}
|
|
212
|
+
if (parsedRequest.data.requestedAt !== requestedAt) {
|
|
213
|
+
return { reconciled: false, reason: "newer-request" as const };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const tracked = await compareAndSetAppState(stateKey, rawState, {
|
|
217
|
+
...rawState,
|
|
218
|
+
tabId,
|
|
219
|
+
claimedAt: new Date().toISOString(),
|
|
220
|
+
});
|
|
221
|
+
return tracked
|
|
222
|
+
? { reconciled: false, tracked: true }
|
|
223
|
+
: { reconciled: false, tracked: false, reason: "stale" as const };
|
|
224
|
+
}
|
|
225
|
+
if (state.tabId !== tabId) {
|
|
226
|
+
return operation === "release"
|
|
227
|
+
? {
|
|
228
|
+
reconciled: false,
|
|
229
|
+
released: true,
|
|
230
|
+
reason: "different-run" as const,
|
|
231
|
+
}
|
|
232
|
+
: { reconciled: false, reason: "different-run" as const };
|
|
233
|
+
}
|
|
234
|
+
if (operation === "release") {
|
|
235
|
+
const untrackedState = { ...rawState };
|
|
236
|
+
delete untrackedState.tabId;
|
|
237
|
+
delete untrackedState.claimedAt;
|
|
238
|
+
const released = await compareAndSetAppState(
|
|
239
|
+
stateKey,
|
|
240
|
+
rawState,
|
|
241
|
+
untrackedState,
|
|
242
|
+
);
|
|
243
|
+
return released
|
|
244
|
+
? { reconciled: false, released: true }
|
|
245
|
+
: { reconciled: false, released: false, reason: "stale" as const };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const reconciled = await compareAndSetAppState(stateKey, rawState, {
|
|
249
|
+
...rawState,
|
|
250
|
+
status: "failed",
|
|
251
|
+
failedAt: new Date().toISOString(),
|
|
252
|
+
});
|
|
253
|
+
return reconciled
|
|
254
|
+
? { reconciled: true }
|
|
255
|
+
: { reconciled: false, reason: "stale" as const };
|
|
256
|
+
},
|
|
257
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
generateTabId,
|
|
2
3
|
sendToAgentChat,
|
|
4
|
+
sendToAgentChatAndConfirm,
|
|
3
5
|
type AgentChatMessage,
|
|
4
6
|
} from "@agent-native/core/client/agent-chat";
|
|
5
7
|
import { agentNativePath } from "@agent-native/core/client/api-path";
|
|
@@ -11,6 +13,8 @@ import { useRecordings, type RecordingSummary } from "./use-library";
|
|
|
11
13
|
|
|
12
14
|
const DEFAULT_TITLE = "Untitled recording";
|
|
13
15
|
const TWO_MINUTES_MS = 2 * 60 * 1000;
|
|
16
|
+
export const WORKFLOW_ACTION_MAX_ATTEMPTS = 5;
|
|
17
|
+
const WORKFLOW_ACTION_RETRY_DELAY_MS = 1000;
|
|
14
18
|
|
|
15
19
|
/** True when `title` is blank or equal to the server-seeded default. */
|
|
16
20
|
export function isDefaultTitle(title: string | null | undefined): boolean {
|
|
@@ -45,6 +49,8 @@ interface AiRequest {
|
|
|
45
49
|
message?: string;
|
|
46
50
|
includeFullVideoInAi?: boolean;
|
|
47
51
|
openInChat?: boolean;
|
|
52
|
+
deliveredAt?: string;
|
|
53
|
+
deliveredTabId?: string;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
const DISPATCHABLE_REQUESTS = new Set([
|
|
@@ -99,6 +105,36 @@ export function useAutoTitleBridge(): void {
|
|
|
99
105
|
const dispatched = useRef<Set<string>>(new Set());
|
|
100
106
|
const inflight = useRef<boolean>(false);
|
|
101
107
|
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const handleChatRunning = (event: Event) => {
|
|
110
|
+
const detail = (event as CustomEvent).detail;
|
|
111
|
+
if (
|
|
112
|
+
detail?.isRunning !== false ||
|
|
113
|
+
(detail.reason !== "stopped" && detail.reason !== "failed") ||
|
|
114
|
+
typeof detail.tabId !== "string"
|
|
115
|
+
)
|
|
116
|
+
return;
|
|
117
|
+
|
|
118
|
+
const recordingId = recordingIdFromTab(detail.tabId);
|
|
119
|
+
const requestedAt = requestedAtFromTab(detail.tabId);
|
|
120
|
+
if (!recordingId || !requestedAt) return;
|
|
121
|
+
|
|
122
|
+
void retryWorkflowAction(
|
|
123
|
+
{
|
|
124
|
+
operation: "stop",
|
|
125
|
+
recordingId,
|
|
126
|
+
requestedAt,
|
|
127
|
+
tabId: detail.tabId,
|
|
128
|
+
},
|
|
129
|
+
"reconciled",
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
window.addEventListener("agentNative.chatRunning", handleChatRunning);
|
|
134
|
+
return () =>
|
|
135
|
+
window.removeEventListener("agentNative.chatRunning", handleChatRunning);
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
102
138
|
const readyRecordings = recordings.filter((r) => r.status === "ready");
|
|
103
139
|
const readyRecordingsKey = readyRecordings
|
|
104
140
|
.map(
|
|
@@ -144,7 +180,6 @@ export function useAutoTitleBridge(): void {
|
|
|
144
180
|
request.requestedAt ?? "0"
|
|
145
181
|
}`;
|
|
146
182
|
if (dispatched.current.has(dispatchKey)) continue;
|
|
147
|
-
dispatched.current.add(dispatchKey);
|
|
148
183
|
if (
|
|
149
184
|
request.kind === "generate-metadata" ||
|
|
150
185
|
request.kind === "regenerate-title"
|
|
@@ -155,8 +190,67 @@ export function useAutoTitleBridge(): void {
|
|
|
155
190
|
dispatched.current.add(`${rec.id}:fallback`);
|
|
156
191
|
}
|
|
157
192
|
|
|
158
|
-
|
|
193
|
+
if (
|
|
194
|
+
request.kind === "generate-workflow" &&
|
|
195
|
+
typeof request.requestedAt === "string"
|
|
196
|
+
) {
|
|
197
|
+
if (request.deliveredTabId) {
|
|
198
|
+
dispatched.current.add(dispatchKey);
|
|
199
|
+
void consumeWorkflowRequest({
|
|
200
|
+
recordingId: rec.id,
|
|
201
|
+
requestedAt: request.requestedAt,
|
|
202
|
+
tabId: request.deliveredTabId,
|
|
203
|
+
});
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
159
206
|
|
|
207
|
+
const tabId = workflowTabId(rec.id, request.requestedAt);
|
|
208
|
+
try {
|
|
209
|
+
const result = (await callAction(
|
|
210
|
+
"reconcile-workflow-generation" as any,
|
|
211
|
+
{
|
|
212
|
+
operation: "track",
|
|
213
|
+
recordingId: rec.id,
|
|
214
|
+
requestedAt: request.requestedAt,
|
|
215
|
+
tabId,
|
|
216
|
+
} as any,
|
|
217
|
+
)) as { tracked?: boolean };
|
|
218
|
+
if (result.tracked !== true) {
|
|
219
|
+
fallbackTimer = setTimeout(() => void tick(), 1000);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
fallbackTimer = setTimeout(() => void tick(), 1000);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const delivery = await sendToAgentChatAndConfirm({
|
|
227
|
+
...buildAiRequestChatOptions(rec, request),
|
|
228
|
+
tabId,
|
|
229
|
+
chatTarget: "local",
|
|
230
|
+
});
|
|
231
|
+
if (!delivery.delivered) {
|
|
232
|
+
await retryWorkflowAction(
|
|
233
|
+
{
|
|
234
|
+
operation: "release",
|
|
235
|
+
recordingId: rec.id,
|
|
236
|
+
requestedAt: request.requestedAt,
|
|
237
|
+
tabId,
|
|
238
|
+
},
|
|
239
|
+
"released",
|
|
240
|
+
);
|
|
241
|
+
fallbackTimer = setTimeout(() => void tick(), 1000);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
dispatched.current.add(dispatchKey);
|
|
245
|
+
void persistAndConsumeWorkflowRequest({
|
|
246
|
+
recordingId: rec.id,
|
|
247
|
+
requestedAt: request.requestedAt,
|
|
248
|
+
tabId,
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
dispatchAiRequest(rec, request);
|
|
253
|
+
dispatched.current.add(dispatchKey);
|
|
160
254
|
void clearRequest(rec.id);
|
|
161
255
|
} else if (isAutoTitleReplaceable(rec.title, rec.titleSource)) {
|
|
162
256
|
// No server-queued delegation. Only dispatch the fallback for
|
|
@@ -294,8 +388,76 @@ export function buildAiRequestChatOptions(
|
|
|
294
388
|
};
|
|
295
389
|
}
|
|
296
390
|
|
|
297
|
-
|
|
298
|
-
|
|
391
|
+
interface WorkflowRunRequest {
|
|
392
|
+
recordingId: string;
|
|
393
|
+
requestedAt: string;
|
|
394
|
+
tabId: string;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function retryWorkflowAction(
|
|
398
|
+
request: WorkflowRunRequest & { operation: string },
|
|
399
|
+
successKey: string,
|
|
400
|
+
): Promise<boolean> {
|
|
401
|
+
for (let attempt = 0; attempt < WORKFLOW_ACTION_MAX_ATTEMPTS; attempt += 1) {
|
|
402
|
+
try {
|
|
403
|
+
const result = (await callAction(
|
|
404
|
+
"reconcile-workflow-generation" as any,
|
|
405
|
+
request as any,
|
|
406
|
+
)) as Record<string, unknown>;
|
|
407
|
+
if (result[successKey] === true) return true;
|
|
408
|
+
if (typeof result.reason === "string" && result.reason !== "stale") {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
} catch {}
|
|
412
|
+
|
|
413
|
+
if (attempt === WORKFLOW_ACTION_MAX_ATTEMPTS - 1) return false;
|
|
414
|
+
await new Promise((resolve) =>
|
|
415
|
+
setTimeout(resolve, WORKFLOW_ACTION_RETRY_DELAY_MS * 2 ** attempt),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function consumeWorkflowRequest(
|
|
423
|
+
request: WorkflowRunRequest,
|
|
424
|
+
): Promise<boolean> {
|
|
425
|
+
return retryWorkflowAction({ ...request, operation: "consume" }, "consumed");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function persistAndConsumeWorkflowRequest(
|
|
429
|
+
request: WorkflowRunRequest,
|
|
430
|
+
): Promise<void> {
|
|
431
|
+
const delivered = await retryWorkflowAction(
|
|
432
|
+
{ ...request, operation: "mark-delivered" },
|
|
433
|
+
"delivered",
|
|
434
|
+
);
|
|
435
|
+
if (delivered) await consumeWorkflowRequest(request);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function workflowTabId(recordingId: string, requestedAt: string) {
|
|
439
|
+
return `clips-workflow:${recordingId}:${encodeURIComponent(requestedAt)}:${generateTabId()}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function recordingIdFromTab(tabId: string) {
|
|
443
|
+
const match = /^clips-workflow:([^:]+):/.exec(tabId);
|
|
444
|
+
return match?.[1];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function requestedAtFromTab(tabId: string) {
|
|
448
|
+
const match = /^clips-workflow:[^:]+:([^:]+):/.exec(tabId);
|
|
449
|
+
return match ? decodeURIComponent(match[1]) : undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function dispatchAiRequest(
|
|
453
|
+
rec: RecordingSummary,
|
|
454
|
+
request: AiRequest,
|
|
455
|
+
tabId?: string,
|
|
456
|
+
) {
|
|
457
|
+
return sendToAgentChat({
|
|
458
|
+
...buildAiRequestChatOptions(rec, request),
|
|
459
|
+
...(tabId ? { tabId } : {}),
|
|
460
|
+
});
|
|
299
461
|
}
|
|
300
462
|
|
|
301
463
|
function parseJsonArray(raw: string | undefined): unknown[] {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
DASHBOARD_REDIRECT_VALUE,
|
|
28
28
|
REF_PARAM,
|
|
29
29
|
} from "@shared/share-attribution";
|
|
30
|
+
import type { WorkflowKind } from "@shared/workflow";
|
|
30
31
|
import {
|
|
31
32
|
IconShare3,
|
|
32
33
|
IconArrowLeft,
|
|
@@ -109,7 +110,6 @@ export function meta() {
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
type SidePanel = "transcript" | "comments" | "insights" | "agent" | "settings";
|
|
112
|
-
type WorkflowKind = "pr" | "sop" | "ticket" | "email";
|
|
113
113
|
|
|
114
114
|
const WORKFLOW_MENU_ITEMS: Array<{
|
|
115
115
|
kind: WorkflowKind;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/application-state/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/application-state/store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAyF9D,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CASzC;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,CAiBzD;AAED,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EAC7C,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACzC,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAkBlB;AAED,MAAM,WAAW,8BAA8B;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9C,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC3C;AA0GD,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,SAAS,8BAA8B,EAAE,EACrD,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,OAAO,CAAC,CA+FlB;AAED,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAAC,CAWjE;AAED,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsBjB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDbExec, getDialect, isLocalDatabase,
|
|
1
|
+
import { getDbExec, getDialect, isLocalDatabase, isPostgres, intType, } from "../db/client.js";
|
|
2
2
|
import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
|
|
3
3
|
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
4
4
|
import { emitAppStateChange, emitAppStateDelete } from "./emitter.js";
|
|
@@ -79,24 +79,15 @@ async function ensureTable() {
|
|
|
79
79
|
return _initPromise;
|
|
80
80
|
}
|
|
81
81
|
export async function appStateGet(sessionId, key) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return JSON.parse(rows[0].value);
|
|
92
|
-
}
|
|
93
|
-
catch (err) {
|
|
94
|
-
// Transient WS / connection drops (Neon serverless) — caller polls every
|
|
95
|
-
// 2s and will see the value on the next tick. Swallow rather than 500.
|
|
96
|
-
if (isConnectionError(err))
|
|
97
|
-
return null;
|
|
98
|
-
throw err;
|
|
99
|
-
}
|
|
82
|
+
await ensureTable();
|
|
83
|
+
const client = getDbExec();
|
|
84
|
+
const { rows } = await client.execute({
|
|
85
|
+
sql: `SELECT value FROM application_state WHERE session_id = ? AND key = ?`,
|
|
86
|
+
args: [sessionId, key],
|
|
87
|
+
});
|
|
88
|
+
if (rows.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
return JSON.parse(rows[0].value);
|
|
100
91
|
}
|
|
101
92
|
/**
|
|
102
93
|
* Read several application-state keys for one session in a single SQL query.
|
|
@@ -110,24 +101,17 @@ export async function appStateGetMany(sessionId, keys) {
|
|
|
110
101
|
values[key] = null;
|
|
111
102
|
if (uniqueKeys.length === 0)
|
|
112
103
|
return values;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
values[row.key] = JSON.parse(row.value);
|
|
123
|
-
}
|
|
124
|
-
return values;
|
|
125
|
-
}
|
|
126
|
-
catch (err) {
|
|
127
|
-
if (isConnectionError(err))
|
|
128
|
-
return values;
|
|
129
|
-
throw err;
|
|
104
|
+
await ensureTable();
|
|
105
|
+
const client = getDbExec();
|
|
106
|
+
const placeholders = uniqueKeys.map(() => "?").join(", ");
|
|
107
|
+
const { rows } = await client.execute({
|
|
108
|
+
sql: `SELECT key, value FROM application_state WHERE session_id = ? AND key IN (${placeholders})`,
|
|
109
|
+
args: [sessionId, ...uniqueKeys],
|
|
110
|
+
});
|
|
111
|
+
for (const row of rows) {
|
|
112
|
+
values[row.key] = JSON.parse(row.value);
|
|
130
113
|
}
|
|
114
|
+
return values;
|
|
131
115
|
}
|
|
132
116
|
export async function appStatePut(sessionId, key, value, options) {
|
|
133
117
|
await ensureTable();
|