diff --git a/apps/server/src/ntbs/adapter.ts b/apps/server/src/ntbs/adapter.ts new file mode 100644 index 00000000000..70b3d2fa144 --- /dev/null +++ b/apps/server/src/ntbs/adapter.ts @@ -0,0 +1,77 @@ +import type { ThreadId } from "@t3tools/contracts"; +import * as NTBS from "./lifecycle.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; +}> {} + +export type NTBSResponse = { + readonly type: "answer" | "failure" | "timeout" | "cancellation"; + readonly text: 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. + * + * The adapter owns its storage and retention policy. A stored snapshot may + * outlive the original platform message. E.g. a message on Discord gets deleted + * but its still persisted in the original snapshot. + * Verify retention policies. + * + * It does not create T3 threads or interpret T3 events. + */ +export interface NTBSAdapter
{ + /** + * Stores the request before any T3 work begins. + * + * Returns `"duplicate"` if the same platform request was already stored. + */ + readonly accept: ( + event: NTBS.NTBSInput
, + ) => Effect.Effect<"accepted" | "duplicate", AdapterError>; + /** + * Stores a lifecycle state. Does not perform any other business logic. + */ + readonly save: (lifecycleEvent: NTBS.NTBSLifecycle
) => Effect.Effect ,
+ ) => Effect.Effect ,
+ response: NTBSResponse,
+ ) => Effect.Effect (key: string) =>
+ Context.Service = {
+ /**
+ * Each NTBSEvent carries the adapter-defined external data.
+ * T3 never inspects it. Only the adapter deals with it.
+ */
+ platformData: P;
+ /**
+ * The captured source text sent as the first T3 user message.
+ * Platform independent.
+ * Must not exceed T3's 120,000-character input limit.
+ */
+ snapshot: string;
+ /**
+ * References to attachments stored by T3 and sent with the first user message.
+ * The processor creates them from attachment data provided by the adapter.
+ */
+ attachments: ReadonlyArray = NTBSInput & {
+ t3Data: {
+ /** The T3 thread created by the lifecycle event */
+ threadId: ThreadId;
+ };
+};
+
+export type ThreadStarted = ThreadEvent & {
+ /** T3 has created the new thread. */
+ state: "thread.started";
+};
+export type ResponsePosted = ThreadEvent & {
+ state: "thread.response.posted";
+ responseMessageId: string;
+};
+
+export type NTBSLifecycle = ThreadStarted | ResponsePosted ;
diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts
new file mode 100644
index 00000000000..f9e16769f76
--- /dev/null
+++ b/apps/server/src/ntbs/processor.ts
@@ -0,0 +1,180 @@
+import {
+ type ChatAttachment,
+ type OrchestrationEvent,
+ type ProjectId,
+ type ThreadId,
+} from "@t3tools/contracts";
+import type * as NTBS from "./lifecycle.ts";
+import { Context, Crypto, Data, Effect } from "effect";
+import type { NTBSAdapter, NTBSResponse } 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 architecture:
+
+ 1. Generic NTBS processor:
+ - Runs the shared workflow for every platform.
+ - Asks the adapter to detect duplicate input.
+ - Creates a fresh worktree and T3 thread.
+ - Saves `ThreadStarted`.
+ - Starts the first turn with the snapshot and attachments.
+ - Attempts to post the acknowledgement independently.
+ - Watches T3 events for completed work.
+ - Posts the final result through the adapter and saves `ResponsePosted`.
+
+ 2. Platform-specific inbound code:
+ - Receives raw platform data from Jira, Discord, GitHub, or Teams.
+ - Applies platform trigger and actor checks.
+ - Builds `NTBSInput ` and `T3Context`.
+ - Calls the processor.
+
+ 3. Adapter
+ - Owns platform storage, duplicate detection, and platform API calls.
+ - Posts acknowledgements 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 =
+ | {
+ readonly source: "adapter";
+ readonly event: NTBS.NTBSInput ;
+ readonly t3Context: T3Context;
+ }
+ | {
+ readonly source: "t3";
+ readonly event: OrchestrationEvent;
+ };
+
+export class NTBSProcessorError extends Data.TaggedError("NTBSProcessorError")<{
+ reason: string;
+}> {}
+
+export interface NTBSProcessor {
+ /**
+ * Routes adapter requests and T3 events through the shared NTBS workflow.
+ *
+ * Platform requests must already have passed their platform-specific trigger
+ * and actor checks. The processor does not perform those.
+ *
+ * Accepts concurrent requests and applies no queue, concurrency cap
+ * or backpressure for the time being. This choice can be reviewed later.
+ */
+ readonly process: (event: ProcessorEvent ) => Effect.Effect (key: string) =>
+ Context.Service (
+ request: NTBS.NTBSInput ,
+ t3Context: T3Context,
+) => Effect.Effect (
+ adapter: NTBSAdapter ,
+) => Effect.Effect > =
+ | NtbsEventAccepted
+ | NtbsEventThreadStarted
+ | NtbsEventAcknowledgementPosted
+ | NtbsEventOutcomeAvailable
+ | NtbsEventResponsePosted ;
+
+type NtbsEventBase > = {
+ /** Adapter-defined data for the external platform. T3 does not inspect it. */
+ platformData: P;
+ /** The captured source text used to create T3's first user message. */
+ snapshot: string;
+};
+
+type NtbsEventAccepted > = NtbsEventBase & {
+ /** The adapter has accepted the inbound event but has not started T3 work. */
+ state: "accepted";
+};
+
+type NtbsEventWithThread > = NtbsEventBase & {
+ /** The T3 IDs created after the adapter starts work. */
+ t3: {
+ /** The T3 thread created from the source event. */
+ threadId: string;
+ /** The first T3 user message created from the snapshot. */
+ userMessageId: string;
+ /** The T3 turn started from that message. */
+ turnId: string;
+ };
+};
+
+type NtbsEventThreadStarted > = NtbsEventWithThread & {
+ /** T3 has created the new thread from the source snapshot. */
+ state: "threadStarted";
+};
+
+type NtbsEventWithAcknowledgement > =
+ NtbsEventWithThread & {
+ /** The external acknowledgement message posted by the adapter. */
+ acknowledgementMessageId: string;
+ };
+
+type NtbsEventAcknowledgementPosted > =
+ NtbsEventWithAcknowledgement & {
+ /** The adapter has posted the acknowledgement. */
+ state: "acknowledgementPosted";
+ };
+
+type NtbsEventOutcomeAvailable > =
+ NtbsEventWithAcknowledgement & {
+ /** T3 has produced a final outcome for the turn. */
+ state: "outcomeAvailable";
+ };
+
+type NtbsEventResponsePosted > =
+ NtbsEventWithAcknowledgement & {
+ /** The adapter has posted T3's final response. */
+ state: "responsePosted";
+ /** The external final message posted by the adapter. */
+ finalMessageId: string;
+ };
+```
+
+TODO: Define error and retry lifecycle states when adapter behaviour is tested.
+
+## Jira example
+
+A user adds top-level Jira comment `10401` on issue `T3-123`: `@agent investigate the failed build`. The adapter accepts source event `jira-event-1`, version `1`, and stores this record before asking T3 to do anything:
+
+```ts
+{
+ state: "accepted",
+ platformData: {
+ source: {
+ eventId: "jira-event-1",
+ version: "1",
+ contextId: "T3-123",
+ messageId: "10401",
+ },
+ responseDestination: {
+ contextId: "T3-123",
+ parentMessageId: "10401",
+ },
+ },
+ snapshot: "@agent investigate the failed build",
+}
+```
+
+When T3 creates the work, the adapter adds its IDs:
+
+```ts
+state: "threadStarted",
+t3: {
+ threadId: "thread-1",
+ userMessageId: "message-1",
+ turnId: "turn-1",
+}
+```
+
+The adapter posts an acknowledgement as a reply to Jira comment `10401`, changes the state to `"acknowledgementPosted"`, and adds `acknowledgementMessageId: "10402"`. When T3 produces the final result for `turn-1`, the state becomes `"outcomeAvailable"`. The adapter then posts another reply to comment `10401`, changes the state to `"responsePosted"`, and adds `finalMessageId: "10403"`.
+
+## Related documents
+
+- [ntbs.md](./ntbs.md) records the overall scope and agreed decisions.
+- [ntbs-event-processing.md](./ntbs-event-processing.md) defines inbound triggers and outbound messages on each platform.
diff --git a/docs/planning/ntbs-event-processing.md b/docs/planning/ntbs-event-processing.md
new file mode 100644
index 00000000000..22391a340d9
--- /dev/null
+++ b/docs/planning/ntbs-event-processing.md
@@ -0,0 +1,124 @@
+# NTBS event processing
+
+**Status:** exploratory planning
+
+This document defines how events arriving from non-turn-based surfaces are classified and converted into T3 work. It focuses on which events start new T3 threads, which events are ignored, and how independently started threads are correlated with their external events and responses.
+
+T3 clients are built around T3 data views (projections): threads, diffs, and projects.
+
+External NTBSs like Jira, Discord, or GitHub know nothing about that: they have only limited capabilities for sending and receiving messages.
+
+The UX on these platforms has to be thoroughly scoped, and adapters to these platforms have to be extended to retain the information needed to connect T3 events to Jira, Discord, GitHub, or Teams events.
+
+## Inbound event processing
+
+### Core rule
+
+An external event starts a new T3 thread when the adapter recognizes it as one of the trigger forms defined below. Each triggering event creates a new T3 thread. NTBS does not explicitly target, continue, steer, or modify an existing T3 thread.
+
+The event is captured together with the source snapshot used to construct the first user message. The adapter must distinguish a source event from delivery attempts. Retrying or redelivering the same source event must not create another T3 thread.
+
+### Adapter storage
+
+For each inbound event, the adapter retains:
+
+- the source event ID and version, to avoid handling the same event twice;
+- the source context and message or comment IDs, so it knows where the event came from;
+- the captured source snapshot;
+- the T3 thread, user-message, and turn IDs created from the event.
+
+### Platform triggers
+
+The following source interactions start a new thread:
+
+#### Jira
+
+- A top-level comment mentioning the agent.
+- A reply mentioning the agent.
+- A comment edit that adds the agent mention to a comment that previously did not invoke the agent.
+- An edit to a comment that already invoked the agent does not trigger a new thread merely because its content changed. A new turn requires a new explicit invocation under the edited comment.
+
+#### GitHub
+
+- An issue or pull request comment mentioning the agent.
+- A pull-request review comment or reply mentioning the agent.
+- A comment edit that adds the agent mention to a comment that previously did not invoke the agent.
+- An edit to a comment that already invoked the agent does not trigger a new thread merely because its content changed. A new turn requires a new explicit invocation under the edited comment.
+
+#### Discord
+
+- A human message mentioning the configured agent user.
+- A human reply to an agent-authored message.
+- A message edit that adds the configured agent mention to a message that previously did not invoke the agent.
+- Editing a message that already invoked the agent does not start another thread merely because its content changed; a new turn requires a new explicit invocation.
+
+### Processing a trigger
+
+When a source interaction matches one of the triggers above, the adapter deduplicates the source event and captures the source snapshot used for the new thread. TODO: define the source event identity and idempotency rules, including late and out-of-order deliveries.
+
+T3 then starts a new thread from that event and snapshot. A thread already running for the same external interaction does not delay, absorb, continue, or modify the new thread.
+
+### Events that do not trigger work
+
+- Edits to a comment that already invoked the agent, including edits that change its content, unless the edited comment contains a new explicit invocation.
+- Duplicate or already-accepted deliveries do not create another thread. TODO: define the stable event identity and idempotency rules, including late and out-of-order deliveries.
+
+Events that do not match one of the triggers above are ignored. Whether adapters retain them for deduplication, audit, or external-state projection is a separate concern.
+
+### Concurrent turns
+
+Multiple events from the same external interaction may create T3 threads at the same time. Each thread produces an answer for its own triggering event and sends that answer to the exact response destination associated with that event. Threads may finish in any order; completion order does not change where their answers are sent.
+
+### Consequences
+
+- Each event has isolated T3 context; a thread does not inherit the conversation history of another event.
+- Each response must retain the exact destination associated with its triggering event.
+- Capturing the source snapshot supports reproducibility, but may increase input size, latency, and model cost.
+- High-volume external interactions may create many T3 threads and increase storage and discovery noise.
+
+### Summary
+
+- An invocation creates an independent T3 thread; it does not target or continue an existing thread.
+- Multiple events from the same external interaction may create concurrent threads.
+- Each thread produces its own answer, routed to the exact response destination associated with its originating event.
+- Duplicate or already-accepted deliveries do not create another thread. TODO: define stable event identity and idempotency rules.
+
+## Outbound response processing
+
+Outbound processing adds the acknowledgement and final-outcome message IDs, together with whether each message was posted.
+
+### Agreed decisions
+
+Only the acknowledgement and the final answer, failure, timeout, or cancellation are rendered on the external platform. All other T3 events remain internal.
+
+Each inbound event creates an immediate outbound acknowledgement. When its T3 thread ends, the adapter sends the final answer, failure, timeout, or cancellation as a new message after that acknowledgement, in the platform's native conversation scope.
+
+#### Response format
+
+Acknowledgements and final messages are text. Adapters use the platform's Markdown-like formatting, including fenced code snippets when useful.
+
+T3 does not use interactive controls, permission requests, or multiple-choice prompts on external platforms. Any question is written as ordinary text.
+
+#### Delivery failures
+
+The adapter posts the result or error as the final message. If delivery fails for a recoverable reason, it retries; otherwise the original working message remains without a follow-up, and the user may start a new request.
+
+#### Message identifiers and placement
+
+Each adapter defines how these identifiers and message relationships map to its platform:
+
+##### Jira
+
+The adapter retains the issue ID or key, invoking comment ID, root comment ID, acknowledgement comment ID, and outcome comment ID. It posts the acknowledgement and outcome as separate replies to the same root comment.
+
+##### GitHub
+
+The adapter retains the repository, pull-request number, invoking comment ID, root review-comment ID when the invocation is in a review thread, acknowledgement message ID, and outcome message ID. In a review thread, the acknowledgement and outcome both reply to the root review comment. For ordinary issue or pull-request comments, they are separate timeline comments on the pull request.
+
+##### Discord
+
+The adapter retains the thread or channel ID, invoking message ID, acknowledgement message ID, and outcome message ID. The acknowledgement replies to the invoking message, and the outcome replies to the acknowledgement.
+
+##### Teams
+
+The adapter retains the team and channel or chat ID, root conversation-message ID, invoking message ID, acknowledgement message ID, and outcome message ID. The acknowledgement and outcome are separate replies in the same root conversation.
diff --git a/docs/planning/ntbs-plan.md b/docs/planning/ntbs-plan.md
new file mode 100644
index 00000000000..802de7c5bde
--- /dev/null
+++ b/docs/planning/ntbs-plan.md
@@ -0,0 +1,63 @@
+# NTBS implementation plan
+
+**Status:** exploratory planning
+
+## 1. Understand the existing mechanics
+
+Read the orchestration command definitions, the orchestration engine service, and the WebSocket turn-start handling to understand how T3 creates threads, prepares worktrees, starts turns, persists events, and exposes those events to consumers.
+
+Then follow the current Jira path from the webhook route and payload parser through the Jira bridge, delivery store, and Jira API client. This provides concrete examples of inbound event handling, platform-owned persistence, T3 command dispatch, acknowledgement delivery, outcome detection, and outbound response placement.
+
+The current Jira bridge is a reference, not the desired architecture. It contains platform-independent behavior that should move into the shared NTBS implementation, and it currently reuses existing threads instead of creating a new thread for every accepted event.
+
+## 2. Build the platform-agnostic NTBS implementation
+
+Create `apps/server/src/ntbs` for the shared lifecycle model, adapter contract, and workflow service.
+
+First, extract the existing create-thread, prepare-worktree, and start-turn mechanic from the WebSocket handler into a reusable orchestration service. Both native T3 clients and NTBS workflows should call this service so thread creation behaves consistently regardless of where the request originated.
+
+Define an adapter contract that leaves platform data opaque to the shared workflow. Each adapter supplies persistence, duplicate prevention, acknowledgement delivery, final-response delivery, and the platform-specific data needed to place those messages.
+
+Implement the shared workflow:
+
+1. Accept the snapshot, T3 context, and opaque platform data from an adapter.
+2. Persist the accepted lifecycle state before starting T3 work.
+3. Create a new T3 thread and worktree, start its first turn, and retain the resulting T3 identifiers.
+4. Ask the adapter to post the acknowledgement and retain its platform message identifier.
+5. Consume T3 events, including replay after a restart, and identify the final outcome for the recorded work.
+6. Load the final assistant text or failure information and ask the adapter to post the final message.
+7. Persist every lifecycle transition so interrupted processing can resume safely.
+
+Confirm when the T3 turn ID becomes available during this work. The current command path knows the thread and user-message IDs immediately but discovers the turn ID later. The implementation and lifecycle types must represent that sequence accurately.
+
+Test the shared workflow with an in-memory adapter implementation before connecting it to a real platform. The tests should cover successful completion, failure, duplicate delivery, restart recovery, and concurrent events.
+
+## 3. Port Jira onto the shared implementation
+
+Keep Jira webhook verification, payload parsing, trigger recognition, Jira identifiers, and Jira API calls inside the Jira adapter.
+
+Replace the shared workflow currently embedded in the Jira bridge with an implementation of the NTBS adapter contract. Adapt the Jira delivery store to persist the NTBS lifecycle together with Jira-specific source and response-destination data.
+
+Change Jira processing so every accepted event creates a new T3 thread. Preserve the agreed outbound behavior: post an acknowledgement for the invoking comment, then post the final answer, failure, timeout, or cancellation as a separate reply in the same Jira comment scope.
+
+Update the Jira tests to prove trigger handling, duplicate prevention, lifecycle recovery, new-thread creation, acknowledgement placement, final-response placement, and concurrent invocations.
+
+# Notes
+
+In `packages/contracts/src/orchestration.ts` we can find the schema `ThreadTurnStartBootstrapCreateThread`.
+
+The schema wants:
+
+- `projectId` (project should be inferred by discord/jira/etc)
+- `title` (generated somewhere)
+- `modelSelection` (some model)
+- `runtimeMode` (permissions)
+- `interactionMode` (apparently default vs plan)
+- `branch` (git branch?)
+- `worktreePath` (where is it on filesystem)
+
+It is then used by the
+
+`ThreadTurnStartBootstrap` which has some optional data for running setup script, preparing worktrees which is then used by
+
+`ThreadTurnStartCommand` and `ClientThreadTurnStartCommand` (essentially the same type)
diff --git a/docs/planning/ntbs.md b/docs/planning/ntbs.md
new file mode 100644
index 00000000000..67b2f3f2298
--- /dev/null
+++ b/docs/planning/ntbs.md
@@ -0,0 +1,70 @@
+# Non-turn-based surfaces
+
+**Status:** exploratory planning
+
+## Overview
+
+T3 currently models interaction primarily as a conversation between one user and an agent. A user submits a message, the agent runs a turn, and T3 presents the resulting conversation and runtime state through clients that understand the full T3 model.
+
+Non-turn-based surfaces (NTBS) such as Discord, Jira, Teams, GitHub issues, and pull requests do not share those assumptions. They are independently owned collaboration systems where:
+
+- several people may interact with the same external object;
+- messages, comments, and object state may be edited or deleted after T3 first observes them;
+- objects may be closed, reopened, moved, locked, or otherwise changed outside T3;
+- events may arrive late, more than once, or after T3 has been offline;
+- the platform can render only a small part of the state and activity available in a native T3 client.
+
+The problem is to define how these surfaces participate in T3 without creating a second domain model beside T3's existing one. The existing T3 event log remains the source of truth for T3 state. NTBS support should reuse existing T3 commands, events, and state where possible, while platform adapters translate between T3 and each platform's native concepts.
+
+This requires a shared contract that answers several questions consistently across platforms:
+
+- how an external interaction is identified and related to its T3 threads;
+- which T3 commands an adapter may issue in response to an external event;
+- which T3 events and state an adapter may use to render a response on the external platform;
+- how later edits, deletions, multiple participants, retries, and replay affect event handling and response rendering;
+- what an adapter does when the external platform cannot represent a T3 event or response;
+
+The integration protocol should expose only the T3 commands and state needed by these adapters. Adapters should be able to obtain an initial state and then receive subsequent changes. Platform adapters should remain responsible for authentication, source-event translation, transport, and rendering—not for defining their own conversation semantics.
+
+## Scope of this document
+
+This document defines the protocol-level relationship between T3 and non-turn-based surfaces. It covers event processing and trigger rules, thread creation, interaction identity, lifecycle, client state, cursors, and adapter behavior. Detailed decisions may be developed in companion planning documents, but remain part of this document's scope.
+
+Implementation is out of scope for this planning stage.
+
+## Proposal: A triggering event creates a new thread
+
+Each event that matches a trigger creates a new T3 thread from the event and its captured source snapshot; the detailed trigger, processing, concurrency, and response-routing rules are defined in [ntbs-event-processing.md](./ntbs-event-processing.md).
+
+## Agreed decisions
+
+### Which external messages or state changes trigger an agent turn, and which are ignored or recorded without starting work?
+
+The platform-specific trigger forms, ignored events, thread creation, and response routing are defined in [ntbs-event-processing.md](./ntbs-event-processing.md).
+
+### What identifies the same external interaction for correlation and projection?
+
+- Jira: the issue key or immutable issue ID. Comments and replies are events within that issue.
+- Discord: the thread ID. The thread is the interaction.
+- GitHub: the repository and pull-request number. Issue comments, review comments, and replies are events within that pull request; the triggering comment and any diff context belong to the individual event.
+- Teams: unresolved. The likely scope is the conversation or reply-chain ID, with each message as its own event.
+
+### When does the adapter capture the source snapshot relative to receiving a trigger and creating the T3 thread?
+
+The adapter captures the source snapshot while processing the trigger, before creating the T3 thread. The new thread uses that captured snapshot.
+
+### How does T3 prevent repeated delivery of the same source event from creating multiple threads?
+
+Each adapter derives an idempotency key from the platform’s source-event identity and version. The adapter stores that key with the T3 thread created for the event. If the same key is delivered again, the adapter reuses the existing record and does not create another thread. A later edit or distinct source event receives a different key and may create a new thread. The exact event identity, versioning, and retention rules are platform-specific and remain to be defined.
+
+### How are concurrent NTBS threads isolated without an event queue?
+
+Each NTBS-triggered T3 thread receives its own worktree and branch before provider execution begins. Threads from the same external interaction can therefore run concurrently without sharing a mutable checkout or requiring an event queue.
+
+### How are completion, failure, timeout, and cancellation reported for an external event?
+
+They use the same response destination as the triggering event. Normal completion returns the agent’s answer; failure, timeout, or cancellation returns a response that explicitly reports the outcome and, where available, its reason. These outcomes do not create a separate external lifecycle or target a different thread.
+
+### How does T3 associate a thread's outcome with the external event that created it, and where does the adapter post that outcome?
+
+Each source event has a unique event ID. T3 stores a correlation record linking that event ID to the T3 thread, user message or turn, and exact response destination. When the turn ends, the adapter uses that record to post the answer or outcome back to the originating source.