@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.
Files changed (20) hide show
  1. package/dist/assets/{index-C97kIvXz.js → index-CMeiCqQf.js} +182 -182
  2. package/dist/index.html +1 -1
  3. package/dist-server/server/modules/orchestration/workflows/workflow-fallback-policy.js +114 -0
  4. package/dist-server/server/modules/orchestration/workflows/workflow-fallback-policy.js.map +1 -0
  5. package/dist-server/server/modules/orchestration/workflows/workflow-replay.js +177 -0
  6. package/dist-server/server/modules/orchestration/workflows/workflow-replay.js.map +1 -0
  7. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +47 -7
  8. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  9. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +74 -0
  10. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
  11. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +88 -0
  12. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
  13. package/package.json +1 -1
  14. package/scripts/smoke/workflow-fallback-replay.mjs +56 -0
  15. package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -0
  16. package/server/modules/orchestration/workflows/workflow-replay.ts +254 -0
  17. package/server/modules/orchestration/workflows/workflow-runner.ts +105 -6
  18. package/server/modules/orchestration/workflows/workflow-trace.ts +76 -0
  19. package/server/modules/orchestration/workflows/workflow.routes.ts +107 -0
  20. 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;