Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"test": "vitest run"
},
"dependencies": {
"@t3tools/core": "workspace:*",
"@t3tools/infra-sqlite": "workspace:*",
"effect": "^3.18.4",
"node-pty": "^1.1.0",
"open": "^10.1.0",
"ws": "^8.18.0"
Expand Down
108 changes: 108 additions & 0 deletions apps/server/src/coreRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import path from "node:path";
import crypto from "node:crypto";
import type { ProviderEvent, ProviderSession, TerminalEvent } from "@t3tools/contracts";
import { EffectFanout, OrchestrationEngine, QueueProjector, type AppViewState } from "@t3tools/core";
import { createSqliteStores } from "@t3tools/infra-sqlite";

import type { ProviderManager } from "./providerManager";
import type { TerminalManager } from "./terminalManager";

function nowIso(): string {
return new Date().toISOString();
}

export class CoreRuntime {
private readonly fanout = new EffectFanout();
private readonly stores;
private readonly projector: QueueProjector;
private readonly engine: OrchestrationEngine;

constructor(dbDir: string) {
const dbPath = path.join(dbDir, "event-store.sqlite");
this.stores = createSqliteStores(dbPath);
this.projector = new QueueProjector(this.stores.projectionStore, this.fanout);
this.engine = new OrchestrationEngine(this.stores.eventStore, this.stores.projectionStore, this.projector);
}

async start(cwd: string, projectName: string): Promise<void> {
await this.engine.start();
await this.engine.execute({
id: crypto.randomUUID(),
type: "app.bootstrap",
issuedAt: nowIso(),
payload: { cwd, projectName },
});
}

async stop(): Promise<void> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High src/coreRuntime.ts:37

Event listeners bound via bindProviderEvents and bindTerminalEvents are never removed in stop(). If events arrive after this.stores.db.close(), dispatch() will attempt to write to the closed database and crash. Consider storing the listener references and calling off() in stop().

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/coreRuntime.ts around line 37:

Event listeners bound via `bindProviderEvents` and `bindTerminalEvents` are never removed in `stop()`. If events arrive after `this.stores.db.close()`, `dispatch()` will attempt to write to the closed database and crash. Consider storing the listener references and calling `off()` in `stop()`.

Evidence trail:
apps/server/src/coreRuntime.ts lines 37-40 (stop() method only calls engine.stop() and db.close(), no listener removal), lines 54-58 (bindProviderEvents adds listener with .on()), lines 60-74 (bindTerminalEvents adds listener with .on() that calls this.dispatch()), lines 46-48 (dispatch() calls engine.execute()), line 24 (engine constructed with stores) at commit ac44561cafdcb8a14780fde9171b38ee9774eb40

await this.engine.stop();
this.stores.db.close();
}

async state(): Promise<AppViewState> {
return this.engine.currentState();
}

async dispatch(command: Parameters<OrchestrationEngine["execute"]>[0]): Promise<AppViewState> {
return this.engine.execute(command);
}

subscribe() {
return this.fanout.subscribe();
}

bindProviderEvents(providerManager: ProviderManager): void {
providerManager.on("event", (event: ProviderEvent) => {
void this.ingestProviderEvent(event);
});
}
Comment on lines +54 to +58
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unhandled errors in fire-and-forget provider event ingestion.

The void this.ingestProviderEvent(event) pattern silently swallows any errors that occur during event ingestion. Under load or during failures, this could lead to lost events without any indication.

Consider adding error handling to maintain predictable behavior during failures.

🛡️ Proposed fix to log ingestion errors
   bindProviderEvents(providerManager: ProviderManager): void {
     providerManager.on("event", (event: ProviderEvent) => {
-      void this.ingestProviderEvent(event);
+      this.ingestProviderEvent(event).catch((err) => {
+        console.error("Failed to ingest provider event", { sessionId: event.sessionId, error: err });
+      });
     });
   }

As per coding guidelines: "Maintain predictable behavior under load and during failures (session restarts, reconnects, partial streams)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/coreRuntime.ts` around lines 54 - 58, The current
fire-and-forget call in bindProviderEvents silently swallows errors by using
"void this.ingestProviderEvent(event)"; change it to attach explicit error
handling so ingestion failures are surfaced and logged—e.g. call
this.ingestProviderEvent(event).catch(err => /* log with context */) and include
contextual info (event, ProviderManager) in the message; reference
bindProviderEvents and ingestProviderEvent and use the class logger if available
(this.logger.error) or console.error as a fallback.


bindTerminalEvents(terminalManager: TerminalManager): void {
terminalManager.on("event", (event: TerminalEvent) => {
if (event.type !== "activity" && event.type !== "error" && event.type !== "exited") return;
void this.dispatch({
id: crypto.randomUUID(),
type: "thread.setTerminalActivity",
issuedAt: event.createdAt,
payload: {
threadId: event.threadId,
terminalId: event.terminalId,
running: event.type === "activity" ? event.hasRunningSubprocess : false,
},
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unused bindTerminalEvents method is dead code

Low Severity

bindTerminalEvents is defined on CoreRuntime but never called anywhere. Terminal event dispatch is handled inline in onTerminalEvent within wsServer.ts. This creates confusing duplicate logic — one active (in wsServer) and one dead (in CoreRuntime) — that could diverge silently during future maintenance.

Fix in Cursor Fix in Web

}

async bindProviderSession(threadId: string, session: ProviderSession): Promise<void> {
await this.dispatch({
id: crypto.randomUUID(),
type: "thread.updateProviderSession",
issuedAt: nowIso(),
payload: { threadId, session },
});
}

async clearProviderSession(threadId: string): Promise<void> {
await this.dispatch({
id: crypto.randomUUID(),
type: "thread.updateProviderSession",
issuedAt: nowIso(),
payload: { threadId, session: null },
});
}

async ingestProviderEvent(event: ProviderEvent): Promise<void> {
const state = await this.state();
const target = state.threads.find((thread) => thread.session?.sessionId === event.sessionId);
if (!target) return;
await this.dispatch({
id: crypto.randomUUID(),
type: "thread.recordProviderEvent",
issuedAt: event.createdAt,
payload: {
threadId: target.id,
event,
},
});
}
}
1 change: 1 addition & 0 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ async function main() {
staticDir,
devUrl,
projectRegistry,
stateDir,
authToken,
});
await server.start();
Expand Down
Loading
Loading