Skip to content

feat: introduce Temporal as workflow orchestration engine#1

Merged
shibadityadeb merged 1 commit into
mainfrom
feat/temporal-orchestration
Jul 14, 2026
Merged

feat: introduce Temporal as workflow orchestration engine#1
shibadityadeb merged 1 commit into
mainfrom
feat/temporal-orchestration

Conversation

@shibadityadeb

@shibadityadeb shibadityadeb commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Temporal as the workflow orchestration engine for all future long-running processes (document ingestion, meeting processing, email/calendar/GitHub sync, memory updates, nightly jobs, approval steps). BullMQ stays for simple fire-and-forget jobs. Infrastructure only — no AI functionality.

Changes

Infrastructure

  • Temporal server (temporalio/auto-setup:1.27) on :7233 in docker-compose, reusing the existing Postgres container; auto-registers the company-brain namespace (7d retention)
  • Temporal UI on http://localhost:8233
  • New env vars: TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, TEMPORAL_TASK_QUEUE, TEMPORAL_WORKER_HEALTH_PORT

New packages

  • packages/workflows — deterministic workflow definitions (helloWorkflow, healthCheckWorkflow, storageWorkflow), shared signal/query definitions, retry policies, task-queue and workflow-type constants. Compiled with no Node types (sandbox-safe).
  • packages/activities — all side effects: printMessage, checkServices (probes Postgres/Redis/MinIO/Qdrant), uploadFile (MinIO), built as a factory over long-lived clients.

New service

  • services/temporal-worker — zod-typed config, connection manager with exponential-backoff retry, worker on task queue brain-core, health/status endpoint on :4100, graceful shutdown (drains in-flight tasks).

API

  • TemporalService (app.temporal): lazy client, workflow-ID generation, start/execute/signal/query/describe, worker-status proxy
  • /health now reports temporal
  • Demo routes under /api/v1/workflows (Bearer auth): start hello, run health-check/storage, describe, query status, skip signal, server+worker status

Docs

  • docs/temporal.md: why Temporal, how workflows/activities work, config reference, execution examples, checklist for adding future workflows
  • README: stack, layout, ports, commands, API table

Testing

  • pnpm typecheck, pnpm lint, pnpm test green across the workspace
  • Verified end-to-end: all three workflows executed successfully; skipDelay signal and getStatus/getReport queries verified; storage workflow uploaded to MinIO; graceful shutdown drained cleanly; /health reports all services up

Summary by CodeRabbit

  • New Features
    • Added Temporal workflow support for greetings, service health checks, and file storage.
    • Added authenticated workflow API endpoints for starting, monitoring, querying, and controlling executions.
    • Added Temporal Server, UI, and worker services for local development.
    • Added workflow and worker health reporting to the dashboard and aggregate health endpoint.
    • Added workflow orchestration documentation and local setup guidance.

- Temporal server (auto-setup) + UI in docker-compose, reusing Postgres,
  namespace company-brain with 7d retention
- packages/workflows: deterministic workflow definitions (hello,
  health-check, storage) with signals, queries, retry policies and
  task-queue/type constants
- packages/activities: side-effecting activity implementations
  (printMessage, checkServices, uploadFile) over long-lived clients
- services/temporal-worker: worker host with typed config, connection
  retry, health endpoint on :4100 and graceful shutdown
- API: TemporalService client wrapper, temporal in /health report,
  demo workflow routes under /api/v1/workflows (Bearer auth)
- docs/temporal.md developer guide; README updates
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Temporal workflow infrastructure is added across the monorepo, including workflow and activity packages, a Temporal worker, API endpoints and health checks, Docker services, environment configuration, dashboard support, and documentation.

Changes

Temporal integration

Layer / File(s) Summary
Workflow and activity foundation
packages/activities/*, packages/workflows/*
Adds activity contracts and implementations for service checks, messaging, and storage uploads, plus workflow definitions, signals, queries, retry policies, constants, and sample workflows.
Temporal worker runtime
services/temporal-worker/*, package.json
Adds worker configuration, retrying server connections, activity context creation, workflow registration, health endpoints, lifecycle handling, and development scripts.
API workflow integration
apps/api/*, packages/types/src/api.ts, apps/web/.../dashboard/page.tsx
Adds a lazy Temporal client service, authenticated workflow routes, Temporal configuration, aggregate health reporting, and dashboard status display.
Local Temporal infrastructure
.env.example, infrastructure/docker/docker-compose.yml, pnpm-workspace.yaml
Adds Temporal environment placeholders, Temporal Server and UI containers, health checks, and build dependency configuration.
Temporal documentation
README.md, docs/temporal.md
Documents the repository architecture, local setup, API endpoints, workflow conventions, execution examples, and observability endpoints.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant API
  participant TemporalService
  participant TemporalServer
  participant TemporalWorker
  participant Activities
  Client->>API: submit workflow request
  API->>TemporalService: start or execute workflow
  TemporalService->>TemporalServer: send workflow command
  TemporalServer->>TemporalWorker: dispatch workflow task
  TemporalWorker->>Activities: run side-effect activity
  Activities-->>TemporalWorker: return activity result
  TemporalWorker-->>TemporalServer: complete workflow task
  TemporalServer-->>API: return workflow result or status
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing Temporal as the workflow orchestration engine.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/temporal-orchestration

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: private package registry requires authentication. Disable ESLint in CodeRabbit settings or use public packages.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@shibadityadeb
shibadityadeb merged commit f38535e into main Jul 14, 2026
1 of 2 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/services/temporal.service.ts`:
- Around line 125-129: Update TemporalService.close() to await any in-flight
this.connecting promise before closing the client, and ensure the pending
connection cannot be installed after shutdown. Preserve cleanup of this.client,
this.connecting, and the existing connection-close behavior.

In `@docs/temporal.md`:
- Around line 145-149: Update the curl examples for helloWorkflow and
storageWorkflow in the temporal documentation to include the required
Authorization Bearer header, matching the documented authentication format so
both copied requests are authenticated.
- Around line 151-154: Update the curl examples in the workflow API section to
define a WORKFLOW_ID placeholder and use "${WORKFLOW_ID}" within quoted URLs for
the status, skip, and workflow-detail requests, avoiding shell redirection
parsing.
- Line 30: Update the fenced code block in docs/temporal.md to include an
explicit language identifier, such as text or ascii, so it satisfies
markdownlint MD040.

In `@packages/activities/src/activities.ts`:
- Around line 77-80: Update the fetch call inside the Qdrant readiness probe to
pass an AbortSignal with an appropriate timeout, ensuring hung requests abort
and the probe returns a down status instead of waiting indefinitely.
- Around line 43-46: Update the connection callback in socket.connect to call
socket.destroy() instead of socket.end() after a successful connection, while
preserving the existing resolve() behavior.

In `@packages/activities/src/context.ts`:
- Around line 46-50: Update the Redis client initialization in the context setup
to connect eagerly during worker boot, ensuring lazyConnect does not buffer the
first Redis operation in ioredis’s offline queue. Preserve maxRetriesPerRequest:
2 and verify startup surfaces connection failures before activities begin; only
disable the offline queue if this eager startup connection is implemented.

In `@services/temporal-worker/src/config.ts`:
- Around line 13-16: Remove the duplicated Temporal worker health endpoint
defaults by establishing one shared source of truth for the port and deriving
the API health URL from it. Update services/temporal-worker/src/config.ts lines
13-16 and apps/api/src/config/env.ts lines 49-53 so both services use the same
TEMPORAL_WORKER_HEALTH_PORT value, with the API constructing the health URL
rather than maintaining an independent full-URL default.

In `@services/temporal-worker/src/health-server.ts`:
- Around line 16-45: Register an error listener on the HTTP server created in
startHealthServer before calling server.listen. Handle listen-time errors
through the provided logger, including the error details, so health-server
failures do not become unhandled EventEmitter errors that crash the worker.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4caf2ae3-ceae-4cf0-af80-6bcc4e393fcf

📥 Commits

Reviewing files that changed from the base of the PR and between 045aae0 and d346da2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (41)
  • .env.example
  • README.md
  • apps/api/package.json
  • apps/api/src/app.ts
  • apps/api/src/config/env.ts
  • apps/api/src/config/index.ts
  • apps/api/src/modules/health/health.routes.ts
  • apps/api/src/modules/workflows/workflow.routes.ts
  • apps/api/src/modules/workflows/workflow.schemas.ts
  • apps/api/src/plugins/services.ts
  • apps/api/src/services/temporal.service.ts
  • apps/web/src/app/(app)/dashboard/page.tsx
  • docs/temporal.md
  • infrastructure/docker/docker-compose.yml
  • package.json
  • packages/activities/eslint.config.mjs
  • packages/activities/package.json
  • packages/activities/src/activities.ts
  • packages/activities/src/context.ts
  • packages/activities/src/index.ts
  • packages/activities/tsconfig.json
  • packages/types/src/api.ts
  • packages/workflows/eslint.config.mjs
  • packages/workflows/package.json
  • packages/workflows/src/constants.ts
  • packages/workflows/src/definitions.ts
  • packages/workflows/src/index.ts
  • packages/workflows/src/retry-policies.ts
  • packages/workflows/src/workflows/health-check.workflow.ts
  • packages/workflows/src/workflows/hello.workflow.ts
  • packages/workflows/src/workflows/storage.workflow.ts
  • packages/workflows/tsconfig.json
  • pnpm-workspace.yaml
  • services/temporal-worker/eslint.config.mjs
  • services/temporal-worker/package.json
  • services/temporal-worker/src/config.ts
  • services/temporal-worker/src/connection.ts
  • services/temporal-worker/src/health-server.ts
  • services/temporal-worker/src/index.ts
  • services/temporal-worker/tsconfig.build.json
  • services/temporal-worker/tsconfig.json

Comment on lines +125 to +129
async close(): Promise<void> {
if (this.client) await this.client.connection.close();
this.client = null;
this.connecting = null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

close() doesn't wait for an in-flight connection attempt.

If close() runs while getClient()'s this.connecting promise is still pending, this.client is still null so nothing is closed here — but the pending promise's .then() will later set this.client to a fresh connection that's never closed, leaking a gRPC connection past shutdown.

🩹 Proposed fix
   async close(): Promise<void> {
-    if (this.client) await this.client.connection.close();
+    if (this.connecting) {
+      await this.connecting.catch(() => undefined);
+    }
+    if (this.client) await this.client.connection.close();
     this.client = null;
     this.connecting = null;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async close(): Promise<void> {
if (this.client) await this.client.connection.close();
this.client = null;
this.connecting = null;
}
async close(): Promise<void> {
if (this.connecting) {
await this.connecting.catch(() => undefined);
}
if (this.client) await this.client.connection.close();
this.client = null;
this.connecting = null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/services/temporal.service.ts` around lines 125 - 129, Update
TemporalService.close() to await any in-flight this.connecting promise before
closing the client, and ensure the pending connection cannot be installed after
shutdown. Preserve cleanup of this.client, this.connecting, and the existing
connection-close behavior.

Comment thread docs/temporal.md

## Architecture

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify the fenced-block language.

markdownlint reports MD040 for this fence. Use a language such as text or ascii.

Proposed fix
-```
+```text
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 30-30: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/temporal.md` at line 30, Update the fenced code block in
docs/temporal.md to include an explicit language identifier, such as text or
ascii, so it satisfies markdownlint MD040.

Source: Linters/SAST tools

Comment thread docs/temporal.md
Comment on lines +145 to +149
```bash
# start helloWorkflow (returns workflowId)
curl -X POST localhost:4000/api/v1/workflows/hello \
-H 'content-type: application/json' \
-d '{"name":"Ada"}'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include the required Bearer header in every authenticated example.

The section states that Bearer authentication is required, but the helloWorkflow and storageWorkflow requests omit the authorization header; copied commands will return 401.

Also applies to: 157-160

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/temporal.md` around lines 145 - 149, Update the curl examples for
helloWorkflow and storageWorkflow in the temporal documentation to include the
required Authorization Bearer header, matching the documented authentication
format so both copied requests are authenticated.

Comment thread docs/temporal.md
Comment on lines +151 to +154
# query its phase / signal it to finish early / describe it
curl localhost:4000/api/v1/workflows/<workflowId>/status -H "authorization: Bearer $TOKEN"
curl -X POST localhost:4000/api/v1/workflows/<workflowId>/skip -H "authorization: Bearer $TOKEN"
curl localhost:4000/api/v1/workflows/<workflowId> -H "authorization: Bearer $TOKEN"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a shell-safe workflow ID placeholder.

Unquoted <workflowId> is parsed by the shell as redirection syntax. Define WORKFLOW_ID and use ${WORKFLOW_ID} in quoted URLs instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/temporal.md` around lines 151 - 154, Update the curl examples in the
workflow API section to define a WORKFLOW_ID placeholder and use
"${WORKFLOW_ID}" within quoted URLs for the status, skip, and workflow-detail
requests, avoiding shell redirection parsing.

Comment on lines +43 to +46
socket.connect(port, host, () => {
socket.end();
resolve();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use socket.destroy() to immediately release resources.

Using socket.end() performs a graceful half-close (sending a FIN) but leaves the socket and its associated timeout active until the remote peer also closes the connection. For a simple TCP health probe, immediately destroying the socket upon successful connection is safer as it guarantees all timers are cleared and resources are freed without waiting.

🔧 Proposed fix
     socket.connect(port, host, () => {
-      socket.end();
+      socket.destroy();
       resolve();
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
socket.connect(port, host, () => {
socket.end();
resolve();
});
socket.connect(port, host, () => {
socket.destroy();
resolve();
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/activities/src/activities.ts` around lines 43 - 46, Update the
connection callback in socket.connect to call socket.destroy() instead of
socket.end() after a successful connection, while preserving the existing
resolve() behavior.

Comment on lines +77 to +80
probe(async () => {
const response = await fetch(`${ctx.config.qdrantUrl}/readyz`);
if (!response.ok) throw new Error(`qdrant readyz returned ${response.status}`);
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the fetch request.

Node's fetch does not have a default timeout. If the Qdrant server accepts the connection but hangs without sending a response, this probe will wait indefinitely, leaking a promise and a connection in the background. Supplying an AbortSignal ensures the probe fails fast and accurately returns a 'down' status.

🔧 Proposed fix
-        probe(async () => {
-          const response = await fetch(`${ctx.config.qdrantUrl}/readyz`);
-          if (!response.ok) throw new Error(`qdrant readyz returned ${response.status}`);
-        }),
+        probe(async () => {
+          const response = await fetch(`${ctx.config.qdrantUrl}/readyz`, {
+            signal: AbortSignal.timeout(5000),
+          });
+          if (!response.ok) throw new Error(`qdrant readyz returned ${response.status}`);
+        }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
probe(async () => {
const response = await fetch(`${ctx.config.qdrantUrl}/readyz`);
if (!response.ok) throw new Error(`qdrant readyz returned ${response.status}`);
}),
probe(async () => {
const response = await fetch(`${ctx.config.qdrantUrl}/readyz`, {
signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error(`qdrant readyz returned ${response.status}`);
}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/activities/src/activities.ts` around lines 77 - 80, Update the fetch
call inside the Qdrant readiness probe to pass an AbortSignal with an
appropriate timeout, ensuring hung requests abort and the probe returns a down
status instead of waiting indefinitely.

Comment on lines +46 to +50
// Do not queue commands forever while Redis is down — activities should
// fail fast and let Temporal's retry policy handle it.
maxRetriesPerRequest: 2,
lazyConnect: true,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Test ioredis lazyConnect and enableOfflineQueue interaction.

npm install ioredis --no-save > /dev/null 2>&1
node -e "
const { Redis } = require('ioredis');
const redis = new Redis({
  host: '127.0.0.1',
  port: 9999, // Unreachable port to simulate offline
  maxRetriesPerRequest: 2,
  enableOfflineQueue: false,
  lazyConnect: true,
});
redis.ping().then(() => console.log('Ping success')).catch(err => {
  console.error('Failure:', err.message);
  process.exit(0);
});
"

Repository: shibadityadeb/Context-Brain

Length of output: 157


🏁 Script executed:

set -euo pipefail

printf '\n## context.ts\n'
sed -n '1,140p' packages/activities/src/context.ts

printf '\n## dependency metadata\n'
git ls-files | rg '(^|/)(package.json|package-lock.json|pnpm-lock.yaml|yarn.lock)$'
if [ -f package.json ]; then
  printf '\n### root package.json (ioredis refs)\n'
  rg -n '"ioredis"|enableOfflineQueue|lazyConnect|maxRetriesPerRequest' package.json packages -g 'package.json' -g '*.ts' -g '*.js'
fi

printf '\n### files mentioning ioredis\n'
rg -n 'ioredis|enableOfflineQueue|lazyConnect|maxRetriesPerRequest' . -g '!**/node_modules/**'

Repository: shibadityadeb/Context-Brain

Length of output: 4234


🌐 Web query:

ioredis enableOfflineQueue lazyConnect maxRetriesPerRequest offline queue documentation

💡 Result:

In ioredis, these options control how the client handles connection states and command execution [1][2]. enableOfflineQueue (default: true) By default, if you send a command before the connection to the Redis server is established or "ready," ioredis adds that command to an internal "offline queue" and executes it automatically once the connection becomes ready [1][3]. If you set enableOfflineQueue to false, any command sent while the connection is not ready will immediately return an error [1][4]. lazyConnect (default: false) By default, an ioredis instance attempts to establish a connection to the Redis server immediately upon initialization [5][6]. When lazyConnect is set to true, this automatic connection attempt is disabled [5]. The client will instead wait until the first command is sent or until you manually call redis.connect [5][6]. Calling redis.connect returns a promise that resolves when the connection is established or rejects if it fails [5][6]. maxRetriesPerRequest (default: 20) This option manages how commands are handled when a connection is lost during execution [2]. If a command does not receive a reply because the connection was lost, ioredis queues it to be resent upon reconnection (provided the retryStrategy allows it) [2]. The maxRetriesPerRequest option limits the number of reconnection attempts allowed for these queued commands before they are flushed and rejected with a MaxRetriesPerRequestError [2]. You can set this to null to allow commands to wait indefinitely for the connection to be restored [2].

Citations:


🌐 Web query:

site:github.com/redis/ioredis "enableOfflineQueue" "lazyConnect" first command

💡 Result:

In the ioredis library, lazyConnect and enableOfflineQueue both influence how the client handles commands when it is not yet connected to a Redis server [1][2]. lazyConnect: When set to true, the client delays establishing a connection to the Redis server until either the first command is sent or the redis.connect method is called explicitly [1][2]. By default (false), the client attempts to connect immediately upon initialization [1][2]. enableOfflineQueue: When set to true (the default), commands sent while the client is not connected are added to an internal queue and executed automatically once the connection is established and the client is ready [1][2]. If this is set to false, commands sent while the connection is not ready will immediately return an error [1][2]. Interaction: When both are used, the "first command" mentioned in the lazyConnect description acts as the trigger to initiate the connection [2]. If enableOfflineQueue is also true, that first command (and any subsequent commands sent before the connection is established) will be queued until the connection is successfully opened [2]. If enableOfflineQueue is false and you send a command before calling redis.connect, the command will fail because the client is not connected and is not permitted to queue the request [2].

Citations:


🏁 Script executed:

set -euo pipefail
rg -n 'lazyConnect:\s*true|enableOfflineQueue:\s*false|maxRetriesPerRequest:\s*2' packages . -g '!**/node_modules/**'

Repository: shibadityadeb/Context-Brain

Length of output: 425


🏁 Script executed:

set -euo pipefail
rg -n "createActivityContext\(" packages services -g '!**/node_modules/**'

Repository: shibadityadeb/Context-Brain

Length of output: 393


Fail fast on Redis startup maxRetriesPerRequest: 2 still leaves ioredis’ offline queue in place. With lazyConnect: true, the first Redis call will be buffered unless you connect during worker boot; if the goal is immediate activity failure, connect eagerly (or disable the offline queue only with that startup path in place).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/activities/src/context.ts` around lines 46 - 50, Update the Redis
client initialization in the context setup to connect eagerly during worker
boot, ensuring lazyConnect does not buffer the first Redis operation in
ioredis’s offline queue. Preserve maxRetriesPerRequest: 2 and verify startup
surfaces connection failures before activities begin; only disable the offline
queue if this eager startup connection is implemented.

Comment on lines +13 to +16
TEMPORAL_ADDRESS: z.string().default('localhost:7233'),
TEMPORAL_NAMESPACE: z.string().default('company-brain'),
TEMPORAL_TASK_QUEUE: z.string().default('brain-core'),
TEMPORAL_WORKER_HEALTH_PORT: z.coerce.number().int().positive().default(4100),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Worker health endpoint value duplicated across two services with no shared source of truth. The health port/URL that gates the API's Temporal status reporting is independently defaulted in both services; nothing enforces they stay consistent.

  • services/temporal-worker/src/config.ts#L13-L16: this is where TEMPORAL_WORKER_HEALTH_PORT (default 4100) is actually bound by health-server.ts.
  • apps/api/src/config/env.ts#L49-L53: TEMPORAL_WORKER_HEALTH_URL (default http://localhost:4100/health) must be kept manually in sync with the port above; consider deriving one from the other (e.g. document the coupling in .env.example, or have the API construct the URL from a shared TEMPORAL_WORKER_HEALTH_PORT env var instead of a separate full URL) to remove the drift risk.
📍 Affects 2 files
  • services/temporal-worker/src/config.ts#L13-L16 (this comment)
  • apps/api/src/config/env.ts#L49-L53
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/temporal-worker/src/config.ts` around lines 13 - 16, Remove the
duplicated Temporal worker health endpoint defaults by establishing one shared
source of truth for the port and deriving the API health URL from it. Update
services/temporal-worker/src/config.ts lines 13-16 and
apps/api/src/config/env.ts lines 49-53 so both services use the same
TEMPORAL_WORKER_HEALTH_PORT value, with the API constructing the health URL
rather than maintaining an independent full-URL default.

Comment on lines +16 to +45
export function startHealthServer(source: WorkerStatusSource, logger: Logger): Server {
const startedAt = Date.now();

const server = createServer((request, response) => {
if (request.url !== '/health') {
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'not found' }));
return;
}
const workerState = source.getWorkerState();
const connection = source.getConnectionStatus();
const healthy = workerState === 'RUNNING' && connection === 'connected';
response.writeHead(healthy ? 200 : 503, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
status: healthy ? 'healthy' : 'degraded',
worker: workerState,
connection,
namespace: config.temporal.namespace,
taskQueue: config.temporal.taskQueue,
uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
}),
);
});

server.listen(config.temporal.healthPort, () =>
logger.info({ port: config.temporal.healthPort }, 'worker health endpoint listening'),
);
return server;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing error handler on the health HTTP server.

server.listen() is called with no error listener registered on the server. If the health port is already in use (or any other listen-time error occurs), Node will throw the unhandled 'error' event and crash the entire worker process — not just the health check — since http.Server is an EventEmitter and unhandled 'error' events throw.

🩹 Proposed fix
+  server.on('error', (error) => {
+    logger.error({ err: error, port: config.temporal.healthPort }, 'health server error');
+  });
+
   server.listen(config.temporal.healthPort, () =>
     logger.info({ port: config.temporal.healthPort }, 'worker health endpoint listening'),
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function startHealthServer(source: WorkerStatusSource, logger: Logger): Server {
const startedAt = Date.now();
const server = createServer((request, response) => {
if (request.url !== '/health') {
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'not found' }));
return;
}
const workerState = source.getWorkerState();
const connection = source.getConnectionStatus();
const healthy = workerState === 'RUNNING' && connection === 'connected';
response.writeHead(healthy ? 200 : 503, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
status: healthy ? 'healthy' : 'degraded',
worker: workerState,
connection,
namespace: config.temporal.namespace,
taskQueue: config.temporal.taskQueue,
uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
}),
);
});
server.listen(config.temporal.healthPort, () =>
logger.info({ port: config.temporal.healthPort }, 'worker health endpoint listening'),
);
return server;
}
export function startHealthServer(source: WorkerStatusSource, logger: Logger): Server {
const startedAt = Date.now();
const server = createServer((request, response) => {
if (request.url !== '/health') {
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'not found' }));
return;
}
const workerState = source.getWorkerState();
const connection = source.getConnectionStatus();
const healthy = workerState === 'RUNNING' && connection === 'connected';
response.writeHead(healthy ? 200 : 503, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
status: healthy ? 'healthy' : 'degraded',
worker: workerState,
connection,
namespace: config.temporal.namespace,
taskQueue: config.temporal.taskQueue,
uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
}),
);
});
server.on('error', (error) => {
logger.error({ err: error, port: config.temporal.healthPort }, 'health server error');
});
server.listen(config.temporal.healthPort, () =>
logger.info({ port: config.temporal.healthPort }, 'worker health endpoint listening'),
);
return server;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/temporal-worker/src/health-server.ts` around lines 16 - 45, Register
an error listener on the HTTP server created in startHealthServer before calling
server.listen. Handle listen-time errors through the provided logger, including
the error details, so health-server failures do not become unhandled
EventEmitter errors that crash the worker.

This was referenced Jul 14, 2026
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.

1 participant