Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
479ccd4
docs: outline non-turn-based surface problem
omegent-app[bot] Jul 31, 2026
6658073
docs: develop NTBS event execution proposal
omegent-app[bot] Aug 4, 2026
6176db3
chore: defined processing
enricopolanski Aug 4, 2026
068d2f5
chore: planning of ntsb processing
enricopolanski Aug 4, 2026
6d7069d
chore: update ntsb planning
enricopolanski Aug 4, 2026
77b38e7
chore: planning ntsb output
enricopolanski Aug 5, 2026
d9630de
chore: more processing
enricopolanski Aug 5, 2026
aa2dc1f
feat: finish processing
enricopolanski Aug 5, 2026
15e309d
chore: kickstart architecture document
enricopolanski Aug 5, 2026
5308496
chore: settle on generic definition
enricopolanski Aug 5, 2026
fb65536
chore: document lifecycle
enricopolanski Aug 5, 2026
865dc6c
feat: define ntbs architeture
enricopolanski Aug 5, 2026
eb9e111
feat: rename ntsb -> ntbs
enricopolanski Aug 5, 2026
ea73116
fix: ntsb -> ntbs
enricopolanski Aug 5, 2026
5d051b5
feat: write plan
enricopolanski Aug 6, 2026
1899746
feat: implement basic lifecycle types
enricopolanski Aug 7, 2026
5b9658c
feat: implement adapter context service
enricopolanski Aug 7, 2026
fd92f5b
feat: work on processor
enricopolanski Aug 7, 2026
432e839
feat: processor types
enricopolanski Aug 7, 2026
b307399
add inbout and outbound processor function
enricopolanski Aug 7, 2026
ccbd749
feat: add makeProcessor declaration
enricopolanski Aug 7, 2026
db363ec
feat: more ntbs processor work
enricopolanski Aug 7, 2026
49c91aa
feat: document ntbs processor declarations
enricopolanski Aug 7, 2026
f7b70d7
fix: ntbs processor and schemas flows
enricopolanski Aug 7, 2026
cd68ff9
fix: docs in ntbs processor
enricopolanski Aug 7, 2026
ce8b7aa
chore: complete abstract/declaration phase
enricopolanski Aug 8, 2026
d991699
chore: fixes part 1
enricopolanski Aug 8, 2026
afdbb5e
feat: review removal of the requestaccepted lifecycle event
enricopolanski Aug 8, 2026
73121f7
chore: finish reviewing first 4 points of adversarial review
enricopolanski Aug 8, 2026
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
74 changes: 74 additions & 0 deletions apps/server/src/ntbs/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { ThreadId } from "@t3tools/contracts";
import * as NTBS from "./schemas.ts";
import { Context, Data, Effect } from "effect";

export class ThreadNotFound extends Data.TaggedError("ThreadNotFound") {}

/**
* Generic error catcher, will be refined later
*/
export class AdapterError extends Data.TaggedError("AdapterError")<{
readonly reason: string;
}> {}

/**
* Defines the platform-specific operations used by the shared NTBS processor.
*
* The adapter detects duplicate requests, stores lifecycle data, finds that data
* from a T3 thread ID, and posts acknowledgements and responses.
*
* It does not create T3 threads or interpret T3 events.
*/
export interface NTBSAdapter<P extends NTBS.PlatformData> {
/**
* Stores the request before any T3 work begins.
*
* Returns `"duplicate"` if the same platform request was already stored.
*
* Returning `"accepted"` means this `RequestAccepted` state has been stored.
*/
readonly accept: (
event: NTBS.RequestAccepted<P>,
) => Effect.Effect<"accepted" | "duplicate", AdapterError>;
/**
* Stores a lifecycle state. Does not perform any other business logic.
*/
readonly save: (lifecycleEvent: NTBS.NTBSLifecycle<P>) => Effect.Effect<void, AdapterError>;
/**
* Posts the working acknowledgement at the response destination,
* described by the event.
*
* Returns the platform's identifier for the posted message.
*
* The processor uses that identifier to save `ThreadStartedAcknowledgement`.
*/
readonly postAcknowledgement: (
event: NTBS.ThreadStarted<P>,
) => Effect.Effect<string, AdapterError>;
/**
* Posts the final T3 outcome at the response destination described
* by the event.
*
* Returns the platform's idenitifier for the posted message.
* The processor uses that identifier to save `ResponsePosted`.
*/
readonly postResponse: (
event: NTBS.ResponseAvailable<P>,
text: string,
) => Effect.Effect<string, AdapterError>;
/**
* Finds the latest lifecycle state associated with a T3 thread.
*
* Fails with `ThreadNotFound` when this adapter has no request associated
* with that thread.
*/
readonly findByThreadId: (
threadId: ThreadId,
) => Effect.Effect<
Exclude<NTBS.NTBSLifecycle<P>, NTBS.RequestAccepted<P>>,
ThreadNotFound | AdapterError
>;
}

export const makeNTBSAdapterTag = <P extends NTBS.PlatformData>(key: string) =>
Context.Service<NTBSAdapter<P>>(key);
26 changes: 26 additions & 0 deletions apps/server/src/ntbs/platform-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Context, Data, Effect } from "effect";

export class NTBSPlatformHandlerError extends Data.TaggedError("NTBSPlatformHandlerError")<{
reason: string;
}> {}

/**
* Connects a platform's incoming messages or comments to shared NTBS processor.
*
* It determines whether the input should start work. If so, it captures the platform data,
* source snapshot, and T3 context, then passes them to the processor.
*
* Duplicate detection, lifecycle storage, and platform API calls belong to the adapter.
*/
export interface NTBSPlatformHandler<Input> {
readonly handle: (input: Input) => Effect.Effect<void, NTBSPlatformHandlerError>;
}

/**
* Creates the Effect service tag used to provide and access one platform handler.
*
* This identifies the handler in the Effect context.
* It does not create the handler implementation.
*/
export const makeNTBSPlatformHandlerTag = <Input>(key: string) =>
Context.Service<NTBSPlatformHandler<Input>>(key);
168 changes: 168 additions & 0 deletions apps/server/src/ntbs/processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { ThreadId, type OrchestrationEvent, type ProjectId } from "@t3tools/contracts";
import type * as NTBS from "./schemas.ts";
import { Context, Crypto, Data, Effect } from "effect";
import type { NTBSAdapter } from "./adapter.ts";
import type { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts";
import type { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts";
import type { GitWorkflowService } from "../git/GitWorkflowService.ts";
import type { ProjectSetupScriptRunner } from "../project/ProjectSetupScriptRunner.ts";

/*
NTBS architectural description:
1. Generic NTBS processor:
- Contains the shared workflow for every adapter. The business logic, regardless of the actual NTBS is identical
- Makes queries to the specific platform adapter
- Uses private T3-specific effect to create a fresh worktree and T3 thread
- Saves `ThreadStarted`
- Posts the acknowledgment through the adapter and saves `ThreadStartedAcknowledgement`
- Starts the first turn with `snapshot`

- Watches T3 events for completed work.
- Finds the adapter record by T3 thread ID, posts the final result
and saves `ResponseAvailable` and `ResponsePosted`

2. Platform handler
- Receives raw platform data (Jira, Discord, Github, Teams)
- Builds `RequestAccepted<P> and `T3Context`
- Calls the processor

3. Adapter
- Owns platform storage, duplicate detection and platform API calls
- Knows how to post acknowledgments and responses
- Knows how platform identifiers are represented
- Knows nothing about creating T3 threads or interpreting T3 events
*/

export type T3Context = {
readonly projectId: ProjectId;
readonly revision: string;
};

export type ProcessorEvent<P extends NTBS.PlatformData> =
| {
readonly source: "adapter";
readonly event: NTBS.RequestAccepted<P>;
readonly t3Context: T3Context;
}
| {
readonly source: "t3";
readonly event: OrchestrationEvent;
};

export class NTBSProcessorError extends Data.TaggedError("NTBSProcessorError")<{
reason: string;
}> {}

export interface NTBSProcessor<P extends NTBS.PlatformData> {
/**
* Routes adapter requests and T3 events through the shared NTBS workflow.
*/
readonly process: (event: ProcessorEvent<P>) => Effect.Effect<void, NTBSProcessorError>;

/**
* Consumes T3 events and passes them to `processT3Event`.
*
* Runs until interrupted by its caller.
* Logs individual processing failures and continues with later events.
*/
readonly subscribeToT3Events: Effect.Effect<void>;
}

export const makeNTBSProcessorTag = <P extends NTBS.PlatformData>(key: string) =>
Context.Service<NTBSProcessor<P>>(key);

type NTBSProcessorRequirements =
/*
Dispatches thread creation and turn-start commands.
Provides the T3 event stream used to detect outcomes.
*/
| OrchestrationEngineService
/*
Loads the selected T3 project and reads the completed thread
state and response tex.
*/
| ProjectionSnapshotQuery
/*
Creates the isolated branch and worktree for each accepted external request.
*/
| GitWorkflowService
/*
Runs the project setup scripts in the newly created worktree before agent work begins.
*/
| ProjectSetupScriptRunner
/*
Generates unique identifiers for the new thread, message, commands, and worktree branch.
*/
| Crypto.Crypto;

/**
* Creates an isolated worktree and a new T3 thread.
*
* Does not start a turn, read platform data or call the adapter.
*
* The final title of the thread is generated by T3 after the first turn starts.
*/
declare const createT3Thread: (t3Context: T3Context) => Effect.Effect<ThreadId, NTBSProcessorError>;

/**
* Starts the first turn in an existing T3 thread.
*/
declare const startT3Turn: (
threadId: ThreadId,
snapshot: string,
) => Effect.Effect<void, NTBSProcessorError>;

/**
* Handles an external request in this order:
*
* 1. Ask the adapter to accept it and stop if it is a duplicate.
* 2. Create the worktree and T3 thread.
* 3. Record `ThreadStarted`
* 4. Post and record the acknowledgement.
* 5. Start the first T3 turn with the source snapshot
*/
declare const processAcceptedRequest: <P extends NTBS.PlatformData>(
request: NTBS.RequestAccepted<P>,
t3Context: T3Context,
) => Effect.Effect<void, NTBSProcessorError>;

/**
* Provider runtimes (like Claude Code) emit `turn.completed` events.
* T3 consumes those internally and exposes the resulting session change through a `thread.session-set` event.
*
* Native T3 clients can react by refreshing the thread projection.
* External NTBS adapters do not consume T3 projections automatically, so they must read the thread state themselves.
*
* This function reads the projected thread identified by the session event.
* It returns `null` if the latest turn has not ended.
* Otherwise it returns the final assistant text or plain text error.
*/
declare const resolveT3Outcome: (
event: Extract<OrchestrationEvent, { type: "thread.session-set" }>,
) => Effect.Effect<
{ readonly threadId: ThreadId; readonly text: string } | null,
NTBSProcessorError
>;

/**
* Handles T3 events that may indicate that a turn has ended.
*
* 1. Ignore events other than `thread.session-set`.
* 2. Find the adapter record by thread ID.
* 3. Stop if no record exists or the response was already posted.
* 4. Resolve the T3 outcome and stop if the turn has not ended.
* 5. Record `ResponseAvailable`.
* 6. Post the response and record `ResponsePosted`.
*/
declare const processT3Event: (
event: OrchestrationEvent,
) => Effect.Effect<void, NTBSProcessorError>;

/**
* Creates an NTBS processor for one adapter.
*
* Resolves the required T3 services and returns processor operations with no remaining requirements.
*/
export declare const makeNTBSProcessor: <P extends NTBS.PlatformData>(
adapter: NTBSAdapter<P>,
) => Effect.Effect<NTBSProcessor<P>, never, NTBSProcessorRequirements>;
69 changes: 69 additions & 0 deletions apps/server/src/ntbs/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { ThreadId } from "@t3tools/contracts";

/**
* Describes the platform-specific data of a
* Non-Turn-Based-Surface.
*
* When receiving an NTBS event (a comment, a message tagging
* a bot, etc) `source` and `responseDestination` hold the details
* necessary to process the what and why.
*/
export type PlatformData<Source = unknown, ResponseDestination = unknown> = {
source: Source;
responseDestination: ResponseDestination;
};

export type LifecycleEvent<P extends PlatformData> = {
/**
* Each NTBSEvent carries the adapter-defined external data.
* T3 never inspects it. Only the adapter deals with it.
*/
platformData: P;
/**
* The captured source text used to send the first T3 user message.
* Platform-independent.
*/
snapshot: string;
};

export type ThreadEvent<P extends PlatformData> = LifecycleEvent<P> & {
t3Data: {
/** The T3 thread created by the lifecycle event */
threadId: ThreadId;
};
};

export type RequestAccepted<P extends PlatformData> = LifecycleEvent<P> & {
state: "request.accepted";
};

export type ThreadStarted<P extends PlatformData> = ThreadEvent<P> & {
/** T3 has created the new thread. */
state: "thread.started";
};

export type ThreadStartedAcknowledgement<P extends PlatformData> = ThreadEvent<P> & {
state: "thread.started.acknowledged";
/** the external's platform identification of the acknowledgment message */
acknowledgementMessageId: string;
};

export type ResponseAvailable<P extends PlatformData> = ThreadEvent<P> & {
state: "thread.response.available";
/** the external's platform identification of the acknowledgment message */
acknowledgementMessageId: string;
};

export type ResponsePosted<P extends PlatformData> = ThreadEvent<P> & {
state: "thread.response.posted";
/** the external's platform identification of the acknowledgment message */
acknowledgementMessageId: string;
responseMessageId: string;
};

export type NTBSLifecycle<P extends PlatformData> =
| RequestAccepted<P>
| ThreadStarted<P>
| ThreadStartedAcknowledgement<P>
| ResponseAvailable<P>
| ResponsePosted<P>;
25 changes: 25 additions & 0 deletions docs/planning/fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# NTBS fixes

This document collects the units of work identified by the adversarial review of the NTBS design.

## 1. Remove the pre-thread lifecycle state

`RequestAccepted` exists to recover a request when the server stops before recording `ThreadStarted`. Supporting that narrow failure window requires planned thread IDs, searches for unfinished requests, startup retries, and rules for resuming duplicates.

Do not add that machinery in the first implementation. Remove `RequestAccepted` and make `ThreadStarted` the first stored lifecycle state. Record it as soon as the basic T3 thread exists, before slower worktree preparation or project setup begins.

This deliberately accepts one limitation: if the server stops before `ThreadStarted` is saved, the request may be lost. The user receives no acknowledgement and can send the request again. If this becomes a real problem, each adapter can later inspect recent platform messages and recover missing requests using the capabilities of that platform.

## 2. Make acknowledgements independent from the shared lifecycle

The acknowledgement is a platform message such as "working on it." It improves feedback for the user, but the current types make its message ID mandatory for `ResponseAvailable` and `ResponsePosted`. If posting the acknowledgement fails, the processor cannot represent or post the final response even though T3 work can continue.

After creating the T3 thread, the processor records `ThreadStarted`. Starting the T3 work and attempting to post the acknowledgement are then independent operations. A failed acknowledgement must not prevent the work from starting, completing, or returning its final response.

Remove `ThreadStartedAcknowledgement` from the shared lifecycle and remove `acknowledgementMessageId` from later lifecycle states. An adapter may retain the acknowledgement ID in its own storage and retry posting when appropriate, but final-response processing must not depend on it.

## 3. Resolve the response for the correct T3 message

The processor currently stores only the T3 thread ID and reads the latest turn when looking for the final response. If someone continues that thread from a native T3 client, the latest turn may belong to different work and its answer could be posted back to the original external request.

Store the ID of the first T3 user message with `ThreadStarted`. Resolve the final response for that specific message instead of reading the latest turn in the thread. Record the corresponding turn ID later when T3 provides it.
21 changes: 21 additions & 0 deletions docs/planning/ideas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# NTBS ideas

## Keep the shared lifecycle small

There is a tradeoff between recovering every possible interruption and keeping the first implementation simple. A saved `RequestAccepted` state could recover the rare case where the server receives a request but stops before creating its T3 thread. Doing that safely would also require planned thread IDs, startup searches, retries, and duplicate handling.

For now, the shared lifecycle should begin with `ThreadStarted`. The processor should save it as soon as the basic T3 thread exists, before slower preparation begins. A request can be lost if the server stops before that point, but the missing acknowledgement makes the failure visible and the user can send the request again.

A stronger recovery system can be added later if real usage requires it. Each adapter could inspect recent messages on its platform, find requests that have no corresponding T3 thread, and submit them again. This belongs to the adapter because Jira, GitHub, Discord, and Teams provide different ways to read their recent messages.

## Remove acknowledgement from the shared lifecycle

The acknowledgement is platform feedback, such as a "working on it" message. It should not be a required stage in the shared NTBS lifecycle because failing to post it, or failing to save its message ID, must not prevent the processor from representing and posting the final response.

Remove `ThreadStartedAcknowledgement` from the lifecycle and remove `acknowledgementMessageId` from `ResponseAvailable` and `ResponsePosted`. The adapter may still post an acknowledgement and retain its identifier in its own platform-specific storage when needed. When posting the final response, the adapter can reply to the acknowledgement or fall back to the original source message according to the platform's capabilities.

The shared sequence becomes:

`Create the T3 thread → record ThreadStarted → start the work and attempt the acknowledgement independently`

The processor does not use acknowledgement success as a condition for continuing. Posting may happen alongside the start of T3 work, and an adapter may retry a failed acknowledgement, but the final answer always remains tied to the original response destination. If a platform benefits from replying to the acknowledgement, its adapter can use the identifier stored in its own data without adding that dependency to the shared lifecycle.
Loading
Loading