@pixelbyte-software/pixcode 1.42.1 → 1.42.2
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/assets/{index-C97kIvXz.js → index-CMeiCqQf.js} +182 -182
- package/dist/index.html +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-fallback-policy.js +114 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-fallback-policy.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-replay.js +177 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-replay.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +47 -7
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +74 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +88 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/workflow-fallback-replay.mjs +56 -0
- package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -0
- package/server/modules/orchestration/workflows/workflow-replay.ts +254 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +105 -6
- package/server/modules/orchestration/workflows/workflow-trace.ts +76 -0
- package/server/modules/orchestration/workflows/workflow.routes.ts +107 -0
- package/server/modules/orchestration/workflows/workflow.types.ts +5 -0
|
@@ -22,6 +22,10 @@ function readString(value: unknown): string | undefined {
|
|
|
22
22
|
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
26
|
+
return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
function redactionValues(run: WorkflowRun): string[] {
|
|
26
30
|
const metadata = run.metadata ?? {};
|
|
27
31
|
const workspaceTarget = metadata.workspaceTarget && typeof metadata.workspaceTarget === 'object'
|
|
@@ -140,6 +144,78 @@ export function buildWorkflowTrace(run: WorkflowRun): WorkflowTraceEvent[] {
|
|
|
140
144
|
},
|
|
141
145
|
});
|
|
142
146
|
|
|
147
|
+
const replay = readRecord(run.metadata?.replay);
|
|
148
|
+
if (replay) {
|
|
149
|
+
pushEvent(events, {
|
|
150
|
+
id: traceId([run.id, 'replay']),
|
|
151
|
+
type: 'run',
|
|
152
|
+
severity: replay.requiresApproval ? 'warning' : 'info',
|
|
153
|
+
status: run.status,
|
|
154
|
+
timestamp: run.startedAt + 0.25,
|
|
155
|
+
actor: 'Pixcode',
|
|
156
|
+
title: 'Workflow replay prepared',
|
|
157
|
+
titleKey: 'workflow.trace.replay',
|
|
158
|
+
summary: redactTraceText([
|
|
159
|
+
`Source run: ${readString(replay.sourceRunId) ?? 'unknown'}`,
|
|
160
|
+
`Scope: ${readString(replay.scope) ?? 'unknown'}`,
|
|
161
|
+
Array.isArray(replay.selectedNodeIds) ? `Selected steps: ${replay.selectedNodeIds.join(', ')}` : undefined,
|
|
162
|
+
replay.requiresApproval ? 'Replay required approval for prior shell, network, or file-write activity.' : undefined,
|
|
163
|
+
].filter(Boolean).join('\n'), run),
|
|
164
|
+
metadata: replay,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const fallbackEvents = Array.isArray(run.metadata?.fallbackEvents)
|
|
169
|
+
? run.metadata.fallbackEvents
|
|
170
|
+
: [];
|
|
171
|
+
fallbackEvents.forEach((event, index) => {
|
|
172
|
+
const record = readRecord(event);
|
|
173
|
+
if (!record) return;
|
|
174
|
+
pushEvent(events, {
|
|
175
|
+
id: traceId([run.id, 'fallback', index]),
|
|
176
|
+
type: 'node',
|
|
177
|
+
severity: 'warning',
|
|
178
|
+
status: 'submitted',
|
|
179
|
+
timestamp: typeof record.startedAt === 'number' ? record.startedAt : run.startedAt + 0.5 + index,
|
|
180
|
+
actor: 'Pixcode',
|
|
181
|
+
nodeId: readString(record.nodeId),
|
|
182
|
+
title: 'Fallback agent started',
|
|
183
|
+
titleKey: 'workflow.trace.fallback',
|
|
184
|
+
summary: redactTraceText([
|
|
185
|
+
`Trigger: ${readString(record.trigger) ?? 'unknown'}`,
|
|
186
|
+
`Source node: ${readString(record.nodeId) ?? 'unknown'}`,
|
|
187
|
+
`Fallback node: ${readString(record.fallbackNodeId) ?? 'unknown'}`,
|
|
188
|
+
readString(record.reason) ? `Reason: ${readString(record.reason)}` : undefined,
|
|
189
|
+
].filter(Boolean).join('\n'), run),
|
|
190
|
+
metadata: record,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const fallbackSkippedEvents = Array.isArray(run.metadata?.fallbackSkippedEvents)
|
|
195
|
+
? run.metadata.fallbackSkippedEvents
|
|
196
|
+
: [];
|
|
197
|
+
fallbackSkippedEvents.forEach((event, index) => {
|
|
198
|
+
const record = readRecord(event);
|
|
199
|
+
if (!record) return;
|
|
200
|
+
pushEvent(events, {
|
|
201
|
+
id: traceId([run.id, 'fallback-skipped', index]),
|
|
202
|
+
type: 'node',
|
|
203
|
+
severity: 'info',
|
|
204
|
+
status: 'skipped',
|
|
205
|
+
timestamp: typeof record.createdAt === 'number' ? record.createdAt : run.startedAt + 0.75 + index,
|
|
206
|
+
actor: 'Pixcode',
|
|
207
|
+
nodeId: readString(record.nodeId),
|
|
208
|
+
title: 'Fallback skipped',
|
|
209
|
+
titleKey: 'workflow.trace.fallback',
|
|
210
|
+
summary: redactTraceText([
|
|
211
|
+
`Trigger: ${readString(record.trigger) ?? 'unknown'}`,
|
|
212
|
+
`Skipped: ${readString(record.skippedReason) ?? 'policy did not allow fallback'}`,
|
|
213
|
+
readString(record.reason) ? `Reason: ${readString(record.reason)}` : undefined,
|
|
214
|
+
].filter(Boolean).join('\n'), run),
|
|
215
|
+
metadata: record,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
143
219
|
run.nodeRuns.forEach((node, index) => {
|
|
144
220
|
const base = eventBase(node);
|
|
145
221
|
const timestamp = nodeTimestamp(run, node, index);
|
|
@@ -2,6 +2,10 @@ import type { Router } from 'express';
|
|
|
2
2
|
import express from 'express';
|
|
3
3
|
|
|
4
4
|
import { workflowRunner } from '@/modules/orchestration/workflows/workflow-runner.js';
|
|
5
|
+
import {
|
|
6
|
+
type WorkflowReplayScope,
|
|
7
|
+
buildWorkflowReplayPlan,
|
|
8
|
+
} from '@/modules/orchestration/workflows/workflow-replay.js';
|
|
5
9
|
import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
|
|
6
10
|
import { buildWorkflowTrace } from '@/modules/orchestration/workflows/workflow-trace.js';
|
|
7
11
|
import { findPixcodeAppRoot } from '@/modules/orchestration/workflows/workspace-target.js';
|
|
@@ -45,6 +49,30 @@ function readRequestUserId(req: express.Request): string | number | null {
|
|
|
45
49
|
return user?.id ?? user?.userId ?? null;
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
function readReplayScope(value: unknown): WorkflowReplayScope {
|
|
53
|
+
return value === 'run' ? 'run' : 'node';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readOptionalString(value: unknown): string | undefined {
|
|
57
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readBooleanFlag(value: unknown): boolean {
|
|
61
|
+
return value === true || value === 'true' || value === '1';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function replayOptions(req: express.Request): {
|
|
65
|
+
scope: WorkflowReplayScope;
|
|
66
|
+
fromNodeId?: string;
|
|
67
|
+
approveReplay: boolean;
|
|
68
|
+
} {
|
|
69
|
+
return {
|
|
70
|
+
scope: readReplayScope(req.body?.scope ?? req.query.scope),
|
|
71
|
+
fromNodeId: readOptionalString(req.body?.fromNodeId ?? req.query.fromNodeId),
|
|
72
|
+
approveReplay: readBooleanFlag(req.body?.approveReplay ?? req.query.approveReplay),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
48
76
|
function sendRunSnapshot(res: express.Response, runId: string): boolean {
|
|
49
77
|
const run = workflowStore.getRun(runId);
|
|
50
78
|
if (!run) {
|
|
@@ -134,6 +162,85 @@ export function createWorkflowRouter(): Router {
|
|
|
134
162
|
});
|
|
135
163
|
});
|
|
136
164
|
|
|
165
|
+
router.get('/workflows/runs/:runId/replay-plan', (req, res) => {
|
|
166
|
+
const run = workflowStore.getRun(req.params.runId);
|
|
167
|
+
if (!run) {
|
|
168
|
+
res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const options = replayOptions(req);
|
|
174
|
+
res.json({
|
|
175
|
+
replayPlan: buildWorkflowReplayPlan(run, {
|
|
176
|
+
scope: options.scope,
|
|
177
|
+
fromNodeId: options.fromNodeId,
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
res.status(400).json({
|
|
182
|
+
error: {
|
|
183
|
+
code: 'REPLAY_PLAN_INVALID',
|
|
184
|
+
message: error instanceof Error ? error.message : String(error),
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
router.post('/workflows/runs/:runId/replay', (req, res) => {
|
|
191
|
+
const run = workflowStore.getRun(req.params.runId);
|
|
192
|
+
if (!run) {
|
|
193
|
+
res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const options = replayOptions(req);
|
|
199
|
+
const replayPlan = buildWorkflowReplayPlan(run, {
|
|
200
|
+
scope: options.scope,
|
|
201
|
+
fromNodeId: options.fromNodeId,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (replayPlan.requiresApproval && !options.approveReplay) {
|
|
205
|
+
res.status(409).json({
|
|
206
|
+
error: {
|
|
207
|
+
code: 'REPLAY_APPROVAL_REQUIRED',
|
|
208
|
+
message: 'Replay requires explicit approval because prior shell, network, or file-write activity was detected.',
|
|
209
|
+
},
|
|
210
|
+
replayPlan,
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const replayRun = workflowRunner.start(
|
|
216
|
+
replayPlan.workflow,
|
|
217
|
+
replayPlan.input,
|
|
218
|
+
{
|
|
219
|
+
...replayPlan.metadata,
|
|
220
|
+
userId: readRequestUserId(req) ?? run.metadata?.userId,
|
|
221
|
+
replay: {
|
|
222
|
+
...(replayPlan.metadata.replay && typeof replayPlan.metadata.replay === 'object'
|
|
223
|
+
? replayPlan.metadata.replay as Record<string, unknown>
|
|
224
|
+
: {}),
|
|
225
|
+
approved: options.approveReplay,
|
|
226
|
+
approvedAt: options.approveReplay ? Date.now() : undefined,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
res.status(202).json({
|
|
231
|
+
run: replayRun,
|
|
232
|
+
replayPlan,
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
res.status(400).json({
|
|
236
|
+
error: {
|
|
237
|
+
code: 'REPLAY_START_FAILED',
|
|
238
|
+
message: error instanceof Error ? error.message : String(error),
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
137
244
|
router.get('/workflows/runs/:runId', (req, res) => {
|
|
138
245
|
const run = workflowStore.getRun(req.params.runId);
|
|
139
246
|
if (!run) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WorkflowContextPacket } from '@/modules/orchestration/workflows/context-packet.js';
|
|
2
|
+
import type { WorkflowFallbackTrigger } from '@/modules/orchestration/workflows/workflow-fallback-policy.js';
|
|
2
3
|
import type { WorkflowHandoffArtifact } from '@/modules/orchestration/workflows/handoff-artifact.js';
|
|
3
4
|
|
|
4
5
|
export type WorkflowRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'canceled';
|
|
@@ -21,6 +22,8 @@ export interface WorkflowNode {
|
|
|
21
22
|
isolation?: 'host' | 'worktree' | 'docker';
|
|
22
23
|
timeoutMs?: number;
|
|
23
24
|
internal?: boolean;
|
|
25
|
+
fallbackTrigger?: WorkflowFallbackTrigger;
|
|
26
|
+
fallbackSourceNodeId?: string;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface Workflow {
|
|
@@ -44,6 +47,8 @@ export interface WorkflowNodeRun {
|
|
|
44
47
|
timeoutMs?: number;
|
|
45
48
|
stage?: string;
|
|
46
49
|
internal?: boolean;
|
|
50
|
+
fallbackTrigger?: WorkflowFallbackTrigger;
|
|
51
|
+
fallbackSourceNodeId?: string;
|
|
47
52
|
status: WorkflowNodeStatus;
|
|
48
53
|
a2aTaskId?: string;
|
|
49
54
|
startedAt?: number;
|