Skip to content

fix(workflow): set durations to seconds#1645

Closed
EnkiP wants to merge 217 commits into
mainfrom
fix/set-duration-to-seconds
Closed

fix(workflow): set durations to seconds#1645
EnkiP wants to merge 217 commits into
mainfrom
fix/set-duration-to-seconds

Conversation

@EnkiP

@EnkiP EnkiP commented Jun 10, 2026

Copy link
Copy Markdown
Member

Definition of Done

General

  • Write an explicit title for the Pull Request, following Conventional Commits specification
  • Test manually the implemented changes
  • Validate the code quality (indentation, syntax, style, simplicity, readability)

Security

  • Consider the security impact of the changes made

Note

Add @forestadmin/workflow-executor package with HTTP server, step executors, and CLI

  • Introduces a new @forestadmin/workflow-executor package that polls Forest server for available workflow runs, executes steps (condition, read-record, update-record, trigger-action, load-related-record, mcp, guidance) via LLM-assisted or manual flows, and persists state via a Sequelize-backed or in-memory RunStore.
  • Adds a BaseStepExecutor with idempotency short-circuiting, per-step timeout, and uniform error-to-outcome mapping; concrete executors extend this for each step type.
  • Exposes an authenticated Koa HTTP server (ExecutorHttpServer) with /health, GET /runs/:runId, and POST /runs/:runId/trigger endpoints; JWT is read from Authorization header or forest_session_token cookie.
  • Adds a WorkflowExecutorProxyRoute to the agent that forwards /_internal/workflow-executions/* requests to the executor when workflowExecutorUrl is configured.
  • Adds an AiClient to @forestadmin/ai-proxy with model caching, remote tool lifecycle management, and mcpServerId propagation through all RemoteTool derivatives.
  • Fixes QuerySerializer.formatFields in agent-client to emit comma-separated fields[collection]=a,b instead of repeated query parameters.
  • Risk: QuerySerializer field serialization change is a breaking wire-format change for any agent-client consumers relying on repeated fields[] parameters.
📊 Macroscope summarized f3b3102. 54 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

matthv and others added 30 commits March 17, 2026 15:00
…premature deps, add smoke test

- Rewrite CLAUDE.md with project overview and architecture principles, remove changelog
- Remove unused dependencies (ai-proxy, sequelize, zod) per YAGNI
- Add smoke test so CI passes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… document system architecture

- Lint now covers src and test directories
- Replace require() with import, use stronger assertion (toHaveLength)
- Add System Architecture section describing Front/Orchestrator/Executor/Agent
- Mark Architecture Principles as planned (not yet implemented)
- Remove redundant test/.gitkeep
- Make index.ts a valid module with export {}

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…erver (#1504)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: alban bertolini <albanb@forestadmin.com>
…ain (#1512)

Co-authored-by: alban bertolini <albanb@forestadmin.com>
- Remove McpClient.tools property, loadTools() returns local array
- Rename closeConnections() → dispose()
- Rename testConnections() → checkConnection()
- Add McpServers type export
- Rename mcpServerConfigs → toolConfigs in create-ai-provider
- Update all tests accordingly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eability

Add sourceId to McpToolRef so that persisted execution data (pendingData,
executionParams) tracks which MCP server provided the tool. This fixes
tool lookup on re-entry (confirmation flow) when multiple servers expose
tools with the same name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… collection schema

- Replace non-null assertion with explicit McpToolNotFoundError when AI
  selects a tool name that doesn't match any available tool
- Resolve related collection name from parent schema before looking up
  the related schema in cache, fixing cases where relation name differs
  from target collection name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge main into feature branch, resolve conflicts by taking main's
versions of router.ts, create-ai-provider.ts and their tests.
Remove deleted mcp-config-checker.ts. Bump workflow-executor internal
deps to match main.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
christophebrun-forest and others added 25 commits June 2, 2026 11:26
…tep logs (PRD-418) (#1616)

* feat(workflow-executor): include mcpServerId + mcpServerName in MCP step logs (PRD-418)

Adds a getExtraLogContext() hook on BaseStepExecutor (default {}) merged into
logCtx, overridden by McpStepExecutor to stamp every MCP step log line with
mcpServerId and mcpServerName. RemoteToolFetcher.fetch() now resolves the
serverName from the scoped config Record key and returns { tools, serverName },
forwarded to the executor via StepExecutorFactory. Diagnostic log lines in the
fetcher also carry mcpServerName.

fixes PRD-418

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing provider errors [PRD-409] (#1609)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: alban bertolini <albanb@forestadmin.com>
…s (PRD-432) (#1620)

The executor mints the JWT it sends to the agent from StepUser (camelCase).
Ruby/Python agents splat JWT claims straight into Caller.new, which requires
snake_case kwargs (first_name, last_name, rendering_id, permission_level), so
the call failed with a 500 ArgumentError. The TS agent reads camelCase. There
is no single canonical casing, so emit both: the Ruby Caller absorbs the extra
camelCase keys via **_extra_args and the TS agent ignores the snake_case ones.

fixes PRD-432

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…e [PRD-430] (#1621)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tring (PRD-437) (#1622)

* fix(agent-client): serialize fields[] projection as comma-separated string (PRD-437)

QuerySerializer.formatFields returned an array for the sparse fieldset
projection (e.g. { 'fields[Customer]': ['first_name','last_name','email'] }).
superagent serializes arrays via qs.stringify(obj, { indices: false }) as
repeated keys without brackets:

  fields[Customer]=first_name&fields[Customer]=last_name&fields[Customer]=email

Rack-based (Ruby) agents collapse duplicate scalar keys to the LAST value, so
parse_projection (which does fields.split(',')) only ever saw the last field.
A getData/read-record reading first_name, last_name, email returned only email
(plus the primary key). The TS agent was unaffected by luck: it does
fields.toString().split(','), and Array.toString() rejoins with commas.

Join each projection into a single comma-separated value (JSON:API sparse
fieldset standard), which both Ruby (split) and Node agents parse correctly.
Also fixes the same class of issue for update-record and getRelatedData, which
share this serialization. Sibling of PRD-273 #8 (filter operator casing).

Update the mcp-server consumer tests that asserted the old array shape, and
tighten the field assertions to token-based matching (no substring matches).

fixes PRD-437

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#1623)

fix(workflow-executor): drop dead-branch steps from context (PRD-433)

Revising a step forks the run history: the orchestrator stamps the pivot
card revised and the downstream branch cancelled, then re-appends the
still-valid steps as clones carrying originalStepIndex. The executor read
that history branch-blind, so re-executed steps inherited stale AI context
and the record pool offered records loaded by superseded steps (e.g.
revising "Load store" proposed loading from the dead branch's owner).

previousSteps now mirrors the orchestrator's own live-path filter
(!revised && !cancelled). resolveStepExecution then resolves a step's
RunStore execution own-index-first: a step the executor ran has its entry
at its own stepIndex; a revision clone — which never ran — falls back to
originalStepIndex, where the copied step's record lives (mirroring the
frontend's carry-forward, resolved on read rather than copied). The
fallback is gated on a non-error outcome, so a re-executed step that
produced no record (failed / skipped / handled manually) never resurfaces
its superseded original's record. Records are never keyed by stepName:
LinkTo loops make names non-unique on the live path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: alban bertolini <albanb@forestadmin.com>
…d route [PRD-450] (#1629)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t [PRD-450] (#1636)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: alban bertolini <albanb@forestadmin.com>
…tep-mapper

# Conflicts:
#	package.json
#	yarn.lock
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yarn no longer nests @ForestAdmin packages under packages/*/node_modules,
so the rm -rf workaround is a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qltysh

qltysh Bot commented Jun 10, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (69)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/agent/src/routes/workflow/workflow-executor-proxy.ts97.4%80
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/integrations/snowflake/tools.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/get-ai-configuration.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/remote-tool.ts100.0%
Coverage rating: A Coverage rating: A
packages/forestadmin-client/src/index.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/validate-ai-configurations.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent-client/src/domains/collection.ts0.0%175
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/integrations/zendesk/tools.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/forest-integration-client.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/mcp-client.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/create-base-chat-model.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/ai-client.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/integrations/kolar/tools.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/utils/options-validator.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/index.ts66.7%179
Coverage rating: A Coverage rating: A
packages/agent-client/src/query-serializer.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/index.ts100.0%
Coverage rating: A Coverage rating: A
...r/src/adapters/forestadmin-client-activity-log-port-factory.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/adapters/console-logger.ts100.0%
Coverage rating: F Coverage rating: F
packages/workflow-executor/src/cli.ts0.0%4-14
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/executors/step-executor-factory.ts100.0%
Coverage rating: A Coverage rating: A
...ow-executor/src/executors/load-related-record-step-executor.ts99.0%226, 377
Coverage rating: A Coverage rating: A
...orkflow-executor/src/executors/summary/step-summary-builder.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/build-workflow-executor.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/adapters/pretty-logger.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/adapters/server-types.ts100.0%
Coverage rating: A Coverage rating: A
...-executor/src/adapters/forestadmin-client-activity-log-port.ts100.0%
Coverage rating: A Coverage rating: A
...s/workflow-executor/src/executors/read-record-step-executor.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/executors/record-step-executor.ts100.0%
Coverage rating: A Coverage rating: A
...ges/workflow-executor/src/executors/condition-step-executor.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/http/pending-data-validators.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/adapters/activity-log-drainer.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/executors/base-step-executor.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/http/executor-http-server.ts99.0%121
Coverage rating: A Coverage rating: A
.../workflow-executor/src/adapters/forest-server-workflow-port.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/adapters/record-id-serializer.ts100.0%
Coverage rating: B Coverage rating: B
packages/workflow-executor/src/adapters/server-ai-adapter.ts80.0%26-27, 70-71
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/in-flight-run-registry.ts100.0%
Coverage rating: A Coverage rating: A
...workflow-executor/src/executors/update-record-step-executor.ts98.9%94
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/executors/activity-log.ts100.0%
Coverage rating: C Coverage rating: C
packages/workflow-executor/src/adapters/ai-client-adapter.ts73.3%20-21, 37-38
Coverage rating: A Coverage rating: A
...-executor/src/executors/trigger-record-action-step-executor.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/adapters/with-retry.ts94.1%45
Coverage rating: A Coverage rating: A
...ages/workflow-executor/src/adapters/agent-client-agent-port.ts100.0%
Coverage rating: A Coverage rating: A
...workflow-executor/src/adapters/run-to-available-step-mapper.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/errors.ts99.2%267
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/executors/mcp-step-executor.ts98.4%181
Coverage rating: A Coverage rating: A
...ow-executor/src/adapters/step-outcome-to-update-step-mapper.ts100.0%
Coverage rating: A Coverage rating: A
...s/workflow-executor/src/adapters/always-error-ai-model-port.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/defaults.ts100.0%
Coverage rating: A Coverage rating: A
...ow-executor/src/executors/summary/step-execution-formatters.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/adapters/step-definition-mapper.ts100.0%
Coverage rating: B Coverage rating: B
...ages/workflow-executor/src/executors/guidance-step-executor.ts86.7%20-21
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/executors/agent-with-log.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/cli-core.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/http/step-serializer.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/types/validated/execution.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/runner.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/remote-tool-fetcher.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/stores/in-memory-store.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/stores/build-run-store.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/schema-cache.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/types/validated/step-outcome.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/index.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/types/validated/step-definition.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/stores/database-store.ts97.1%71
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/types/validated/collection.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/validate-secrets.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/schema-resolver.ts100.0%
Total98.6%
🤖 Increase coverage with AI coding...
In the `fix/set-duration-to-seconds` branch, add test coverage for this new code:

- `packages/agent-client/src/domains/collection.ts` -- Line 175
- `packages/agent/src/routes/index.ts` -- Line 179
- `packages/agent/src/routes/workflow/workflow-executor-proxy.ts` -- Line 80
- `packages/workflow-executor/src/adapters/ai-client-adapter.ts` -- Lines 20-21 and 37-38
- `packages/workflow-executor/src/adapters/server-ai-adapter.ts` -- Lines 26-27 and 70-71
- `packages/workflow-executor/src/adapters/with-retry.ts` -- Line 45
- `packages/workflow-executor/src/cli.ts` -- Line 4-14
- `packages/workflow-executor/src/errors.ts` -- Line 267
- `packages/workflow-executor/src/executors/guidance-step-executor.ts` -- Line 20-21
- `packages/workflow-executor/src/executors/load-related-record-step-executor.ts` -- Lines 226 and 377
- `packages/workflow-executor/src/executors/mcp-step-executor.ts` -- Line 181
- `packages/workflow-executor/src/executors/update-record-step-executor.ts` -- Line 94
- `packages/workflow-executor/src/http/executor-http-server.ts` -- Line 121
- `packages/workflow-executor/src/stores/database-store.ts` -- Line 71

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@EnkiP EnkiP changed the base branch from feat/prd-214-server-step-mapper to main June 10, 2026 11:56
@EnkiP

EnkiP commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

wrong branch, new PR: #1646

@EnkiP EnkiP closed this Jun 10, 2026
Comment on lines +41 to +49
async loadRemoteTools(configs: Record<string, ToolConfig>): Promise<RemoteTool[]> {
await this.disposeToolProviders('Error closing previous remote tool connection');

const providers = createToolProviders(configs, this.logger);
const toolsByProvider = await Promise.all(providers.map(p => p.loadTools()));
this.toolProviders = providers;

return toolsByProvider.flat();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/ai-client.ts:41

If Promise.all(providers.map(p => p.loadTools())) throws, this.toolProviders is never updated with the new providers array. Created ToolProvider instances will leak since closeConnections() and subsequent loadRemoteTools() calls have no reference to them. Consider assigning this.toolProviders = providers before awaiting the promises, or using try/finally to ensure disposal if loading fails.

  async loadRemoteTools(configs: Record<string, ToolConfig>): Promise<RemoteTool[]> {
     await this.disposeToolProviders('Error closing previous remote tool connection');
 
     const providers = createToolProviders(configs, this.logger);
-    const toolsByProvider = await Promise.all(providers.map(p => p.loadTools()));
     this.toolProviders = providers;
+    const toolsByProvider = await Promise.all(providers.map(p => p.loadTools()));
 
-    return toolsByProvider.flat();
+    return toolsByProvider.flat();
   }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/ai-proxy/src/ai-client.ts around lines 41-49:

If `Promise.all(providers.map(p => p.loadTools()))` throws, `this.toolProviders` is never updated with the new providers array. Created `ToolProvider` instances will leak since `closeConnections()` and subsequent `loadRemoteTools()` calls have no reference to them. Consider assigning `this.toolProviders = providers` before awaiting the promises, or using `try/finally` to ensure disposal if loading fails.

Evidence trail:
packages/ai-proxy/src/ai-client.ts lines 41-49: `loadRemoteTools` creates providers (line 44), awaits loadTools (line 45), and only assigns to `this.toolProviders` on line 46 after success. Lines 55-70: `disposeToolProviders` sets `this.toolProviders = []` (line 59) and calls `dispose()` on providers (line 61), confirming these are disposable resources. On line 45 rejection, providers created on line 44 become unreachable and are never disposed.

Comment on lines +167 to +185
private async saveFrontendResult(
target: ActionTarget,
actionResult: unknown,
existingExecution: TriggerRecordActionStepExecutionData,
): Promise<StepExecutionResult> {
const { selectedRecordRef, displayName, name } = target;

await this.context.runStore.saveStepExecution(this.context.runId, {
...existingExecution,
type: 'trigger-action',
stepIndex: this.context.stepIndex,
executionParams: { displayName, name },
executionResult: { success: true, actionResult },
selectedRecordRef,
});

return this.buildOutcomeResult({ status: 'success' });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium executors/trigger-record-action-step-executor.ts:167

saveFrontendResult does not set idempotencyPhase: 'done' when saving the execution result, unlike executeOnExecutor which does (line 161). This causes checkIdempotency to return null instead of recognizing the step as complete on re-entry, potentially triggering incorrect re-processing of the confirmation flow.

  private async saveFrontendResult(
    target: ActionTarget,
    actionResult: unknown,
    existingExecution: TriggerRecordActionStepExecutionData,
  ): Promise<StepExecutionResult> {
    const { selectedRecordRef, displayName, name } = target;

    await this.context.runStore.saveStepExecution(this.context.runId, {
      ...existingExecution,
      type: 'trigger-action',
      stepIndex: this.context.stepIndex,
      executionParams: { displayName, name },
      executionResult: { success: true, actionResult },
      selectedRecordRef,
+      idempotencyPhase: 'done',
    });

    return this.buildOutcomeResult({ status: 'success' });
  }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts around lines 167-185:

`saveFrontendResult` does not set `idempotencyPhase: 'done'` when saving the execution result, unlike `executeOnExecutor` which does (line 161). This causes `checkIdempotency` to return `null` instead of recognizing the step as complete on re-entry, potentially triggering incorrect re-processing of the confirmation flow.

Evidence trail:
- packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts lines 174-181: `saveFrontendResult` saves without `idempotencyPhase: 'done'`
- packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts line 160: `executeOnExecutor` saves with `idempotencyPhase: 'done'`
- packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts lines 31-45: `checkIdempotency` checks for `idempotencyPhase === 'done'` and returns null otherwise
- packages/workflow-executor/src/executors/trigger-record-action-step-executor.ts lines 123-128: Branch C saves initial pending data without `idempotencyPhase`, so `existingExecution` spread won't provide it
- packages/workflow-executor/src/executors/update-record-step-executor.ts line 280: consistent pattern uses `idempotencyPhase: 'done'`
- packages/workflow-executor/src/executors/mcp-step-executor.ts line 152: consistent pattern uses `idempotencyPhase: 'done'`

Comment on lines +64 to +66
if (outcomeType === 'guidance') {
const status: GuidanceStepOutcome['status'] = ctx.status === 'error' ? 'error' : 'success';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low adapters/run-to-available-step-mapper.ts:64

The guidance branch in toStepOutcome ignores 'awaiting-input' status and returns 'success' instead. When ctx.status is 'awaiting-input', this causes the orchestrator to treat a paused guidance step as completed. Consider using toRecordStatus(ctx.status) to match the other branches.

  if (outcomeType === 'guidance') {
-    const status: GuidanceStepOutcome['status'] = ctx.status === 'error' ? 'error' : 'success';
+    const status: GuidanceStepOutcome['status'] = toRecordStatus(ctx.status);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/workflow-executor/src/adapters/run-to-available-step-mapper.ts around lines 64-66:

The `guidance` branch in `toStepOutcome` ignores `'awaiting-input'` status and returns `'success'` instead. When `ctx.status` is `'awaiting-input'`, this causes the orchestrator to treat a paused guidance step as completed. Consider using `toRecordStatus(ctx.status)` to match the other branches.

Evidence trail:
packages/workflow-executor/src/adapters/run-to-available-step-mapper.ts:31-36 (`toRecordStatus` handles 'awaiting-input'), :64-68 (guidance branch only checks for 'error', maps everything else to 'success'), :52-61 (condition branch uses `toRecordStatus`), :70-76 (mcp and record branches use `toRecordStatus`). packages/workflow-executor/src/types/validated/step-outcome.ts:9 (`RecordStepStatusSchema` = `['success', 'error', 'awaiting-input']`), :55-62 (`GuidanceStepOutcomeSchema` uses `RecordStepStatusSchema` for status).

Comment on lines +4 to +14
error(message: string, context: Record<string, unknown>): void {
console.error(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
}

warn(message: string, context: Record<string, unknown>): void {
console.warn(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
}

info(message: string, context: Record<string, unknown>): void {
console.info(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low adapters/console-logger.ts:4

The spread order { message, timestamp, ...context } allows properties in context to overwrite message and timestamp. If a caller passes context with a message or timestamp key, the log entry will show the caller's values instead of the actual message and timestamp. Change the spread to { ...context, message, timestamp } so the logger's fields take precedence.

-    console.error(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
+    console.error(JSON.stringify({ ...context, message, timestamp: new Date().toISOString() }));
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/workflow-executor/src/adapters/console-logger.ts around lines 4-14:

The spread order `{ message, timestamp, ...context }` allows properties in `context` to overwrite `message` and `timestamp`. If a caller passes `context` with a `message` or `timestamp` key, the log entry will show the caller's values instead of the actual message and timestamp. Change the spread to `{ ...context, message, timestamp }` so the logger's fields take precedence.

Evidence trail:
packages/workflow-executor/src/adapters/console-logger.ts lines 5, 9, 13 at REVIEWED_COMMIT — all three methods use `{ message, timestamp: ..., ...context }` with `...context` last, allowing context properties to overwrite `message` and `timestamp`.

Comment on lines +188 to +195
async stop() {
process.removeListener('SIGTERM', onSignal);
process.removeListener('SIGINT', onSignal);

if (!shutdownPromise) shutdownPromise = shutdown();
await shutdownPromise;
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/build-workflow-executor.ts:188

After stop() is called, shutdownPromise retains its resolved value. If start() is called again on the same instance, a subsequent SIGTERM/SIGINT will see shutdownPromise is still truthy, skip calling shutdown(), and exit with code 0 without actually stopping the server or runner. Consider resetting shutdownPromise to null after stop() completes so the next signal triggers a fresh shutdown.

     async stop() {
       process.removeListener('SIGTERM', onSignal);
       process.removeListener('SIGINT', onSignal);
 
       if (!shutdownPromise) shutdownPromise = shutdown();
       await shutdownPromise;
+      shutdownPromise = null;
     },
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/workflow-executor/src/build-workflow-executor.ts around lines 188-195:

After `stop()` is called, `shutdownPromise` retains its resolved value. If `start()` is called again on the same instance, a subsequent SIGTERM/SIGINT will see `shutdownPromise` is still truthy, skip calling `shutdown()`, and exit with code 0 without actually stopping the server or runner. Consider resetting `shutdownPromise` to `null` after `stop()` completes so the next signal triggers a fresh shutdown.

Evidence trail:
packages/workflow-executor/src/build-workflow-executor.ts lines 134-196 at REVIEWED_COMMIT. Key lines: 139 (`shutdownPromise` init to null), 157 (guard `if (!shutdownPromise)`), 180-185 (`start()` re-registers signal handlers without resetting `shutdownPromise`), 188-194 (`stop()` sets `shutdownPromise` but never resets to null).

aiModelPort,
activityLogPortFactory,
logger,
pollingIntervalS: options.pollingIntervalS ?? DEFAULT_POLLING_INTERVAL_S,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/build-workflow-executor.ts:124

pollingIntervalS uses ?? instead of positiveOrDefault, so passing pollingIntervalS: 0 creates a 0-second polling interval that hammers the server instead of falling back to DEFAULT_POLLING_INTERVAL_S. This contradicts the comment at lines 60-61 which states that bad timeout configs must fall back to defaults. Consider using positiveOrDefault for pollingIntervalS to match stepTimeoutS, aiInvokeTimeoutS, and schemaCacheTtlS.

-    pollingIntervalS: options.pollingIntervalS ?? DEFAULT_POLLING_INTERVAL_S,
+    pollingIntervalS: positiveOrDefault(options.pollingIntervalS, DEFAULT_POLLING_INTERVAL_S),
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/workflow-executor/src/build-workflow-executor.ts around line 124:

`pollingIntervalS` uses `??` instead of `positiveOrDefault`, so passing `pollingIntervalS: 0` creates a 0-second polling interval that hammers the server instead of falling back to `DEFAULT_POLLING_INTERVAL_S`. This contradicts the comment at lines 60-61 which states that bad timeout configs must fall back to defaults. Consider using `positiveOrDefault` for `pollingIntervalS` to match `stepTimeoutS`, `aiInvokeTimeoutS`, and `schemaCacheTtlS`.

Evidence trail:
packages/workflow-executor/src/build-workflow-executor.ts lines 60-64 (comment + positiveOrDefault helper), line 99 (schemaCacheTtlS uses positiveOrDefault), line 124 (pollingIntervalS uses ?? instead), line 128 (stepTimeoutS uses positiveOrDefault), line 129 (aiInvokeTimeoutS uses positiveOrDefault) at REVIEWED_COMMIT.

Comment on lines +33 to +34
const { runId } = context.params;
const isTrigger = context.method === 'POST';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low workflow/workflow-executor-proxy.ts:33

runId from route parameters is interpolated directly into executorRelativeUrl without validation. When runId contains path traversal sequences like ../, new URL(executorRelativeUrl, this.executorUrl) normalizes the path, allowing requests to arbitrary endpoints on the executor. For example, runId='../admin' resolves /runs/../admin to /admin on the target server. Consider validating that runId matches a safe pattern (e.g., UUID or alphanumeric) before constructing the target URL.

-    const { runId } = context.params;
+    const { runId } = context.params;
+
+    // Validate runId to prevent path traversal
+    if (!/^[a-zA-Z0-9_-]+$/.test(runId)) {
+      context.throw(400, 'Invalid runId format');
+      return;
+    }
+
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/agent/src/routes/workflow/workflow-executor-proxy.ts around lines 33-34:

`runId` from route parameters is interpolated directly into `executorRelativeUrl` without validation. When `runId` contains path traversal sequences like `../`, `new URL(executorRelativeUrl, this.executorUrl)` normalizes the path, allowing requests to arbitrary endpoints on the executor. For example, `runId='../admin'` resolves `/runs/../admin` to `/admin` on the target server. Consider validating that `runId` matches a safe pattern (e.g., UUID or alphanumeric) before constructing the target URL.

Evidence trail:
packages/agent/src/routes/workflow/workflow-executor-proxy.ts lines 33, 36-37 at REVIEWED_COMMIT: runId extracted from context.params, interpolated into URL path, passed to new URL() which normalizes '..' segments. No validation exists in this file or BaseRoute (packages/agent/src/routes/base-route.ts). Route type is PrivateRoute (line 18) but auth headers forwarded to target (lines 66-67). @koa/router `:runId` matches [^/]+ so '..' (no slashes) is a valid match, and %2F-encoded slashes allow deeper traversal via decodeURIComponent in path-to-regexp.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants