From 479ccd4a428e826045f5cab07ba23424bc43c71b Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:42:47 +0000 Subject: [PATCH 01/29] docs: outline non-turn-based surface problem --- docs/planning/ntsb.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/planning/ntsb.md diff --git a/docs/planning/ntsb.md b/docs/planning/ntsb.md new file mode 100644 index 00000000000..3fcbc9ef242 --- /dev/null +++ b/docs/planning/ntsb.md @@ -0,0 +1,37 @@ +# 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 T3 event log remains canonical. NTBS support should select and project an explicit subset of existing T3 commands, events, and state, while platform adapters translate between that subset and each platform's native concepts. + +This requires a shared contract that answers several questions consistently across platforms: + +- what an external object corresponds to in T3; +- which T3 commands an external participant may cause; +- which T3 events and projected state an NTBS client may observe; +- how later external changes, multiple participants, retries, and replay affect that state; +- what limited clients render, ignore, or report as unsupported. + +The shared contract should be smaller than the full interactive-client protocol, event-based, and usable through both snapshots and incremental 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 will capture the protocol design one decision at a time. It does not yet prescribe an object-to-thread mapping, a command or event subset, lifecycle semantics, cursor rules, or adapter behavior. Those decisions will be added only after they are discussed and agreed. + +Implementation is out of scope for this planning stage. + +## Agreed decisions + +None yet. From 66580730b290af7df6670b061be75f054e0c915e Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:52:34 +0000 Subject: [PATCH 02/29] docs: develop NTBS event execution proposal --- docs/planning/ntsb.md | 45 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/planning/ntsb.md b/docs/planning/ntsb.md index 3fcbc9ef242..66e0e792131 100644 --- a/docs/planning/ntsb.md +++ b/docs/planning/ntsb.md @@ -32,6 +32,49 @@ This document will capture the protocol design one decision at a time. It does n Implementation is out of scope for this planning stage. +## Proposal: A triggering event creates a new thread + +Each accepted external event that triggers an agent turn creates a new T3 thread. The first user message is constructed from the current authorized snapshot of the external source together with the event that triggered the run. In the existing T3 protocol, this can be expressed with `thread.turn.start` and `bootstrap.createThread`; it does not require a separate NTBS command. + +An external event is not the same as a delivery attempt. Retries and duplicate webhook deliveries must resolve to the same accepted event and must not create additional threads. The identity and idempotency rules needed to guarantee this are still to be designed. + +Triggering events for the same external interaction are processed in order. If an earlier event's T3 thread is still running, a later event waits. It does not run concurrently and does not alter, steer, or continue the active thread. When its turn comes, the later event starts its own T3 thread. + +### Advantages + +- The external source remains the participant-visible context for the run; behavior does not depend on hidden T3 conversation history that external participants cannot inspect. +- Each run has an isolated and auditable input, actor, output, and lifecycle. +- An edit can trigger a new run from the updated source snapshot without rewriting the history of a previous T3 thread. +- Different participants do not implicitly inherit stale or private context accumulated in an earlier agent session. +- Replay can reconstruct what the agent was asked to do from the captured source version and triggering event. +- Closing, reopening, deleting, or moving an external object does not need to masquerade as T3 thread lifecycle. +- The normal path initially needs only the existing bootstrap form of `thread.turn.start`. + +### Costs and limitations + +- Rebuilding the external snapshot for every run may increase prompt size, latency, and model cost. +- T3-only context such as intermediate tool activity or prior instructions is lost unless it is deliberately included in the new prompt. +- A high-volume external discussion may create many short-lived T3 threads, increasing storage and making native T3 discovery noisier. +- Independent threads can still target the same worktree or other mutable resource, so execution needs separate serialization or conflict rules. +- An external reply cannot continue an in-flight approval, user-input request, interrupt, or steering interaction merely by creating another thread; those interactions would need an explicit command targeting the existing thread or be unsupported. +- Reliable replay requires the system to retain or reconstruct the exact authorized source snapshot used for the run, not merely fetch whatever the source contains later. + +## Open questions + +- Which external messages or state changes trigger an agent turn, and which are ignored or recorded without starting work? +- What identifies the same external interaction for sequential processing: a Jira issue, Discord thread, GitHub issue or pull request, nested review discussion, Teams conversation, or another scope? +- Is the source snapshot frozen when an event is accepted or fetched when its queued execution begins? +- If the source is edited or deleted while its event is waiting, does the queued event retain its original snapshot, get replaced, or get cancelled? +- What stable identity distinguishes an accepted external event from duplicate, retried, late, or out-of-order deliveries? +- Does sequential processing apply only within one external interaction, or must executions that share a worktree or another mutable resource also wait for each other? +- Is bootstrap `thread.turn.start` the only command NTBS may issue, or are any commands targeting an existing execution thread supported? +- Which T3 events and projected fields may NTBS clients consume, and which are deliberately omitted or unsupported? +- How do clients obtain an initial snapshot, resume from a cursor, replay missed changes, and recover when their cursor is no longer valid? +- How are completion, failure, timeout, and cancellation represented and rendered on limited external platforms? +- How is an external event correlated with its T3 execution thread and the response rendered back onto the source? +- How long are captured source snapshots, execution threads, and their correlation records retained, and how are they presented in native T3 clients? + ## Agreed decisions -None yet. +- Each accepted external event that triggers an agent turn creates a new T3 thread from the authorized external source snapshot and the triggering event. +- Triggering events for the same external interaction are processed sequentially. A later event waits for the earlier event's thread to finish, then starts a new thread. From 6176db3a8245c265f21b1d5a56da90846aef695b Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Tue, 4 Aug 2026 16:01:23 +0200 Subject: [PATCH 03/29] chore: defined processing --- docs/planning/feedback.md | 60 ++++++++++++++++++++++++++ docs/planning/ntsb-event-processing.md | 60 ++++++++++++++++++++++++++ docs/planning/ntsb.md | 22 +++++----- 3 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 docs/planning/feedback.md create mode 100644 docs/planning/ntsb-event-processing.md diff --git a/docs/planning/feedback.md b/docs/planning/feedback.md new file mode 100644 index 00000000000..b465d3ffd3c --- /dev/null +++ b/docs/planning/feedback.md @@ -0,0 +1,60 @@ +In ntsb-event-processing.md: + +- “authorized request” +- “enabled interaction” +- “accepted request/event” +- “qualifying event” +- “request for the agent” +- “new explicit invocation” +- “source snapshot permitted by the access check” +- “pending turn record” — especially wrong now that we decided not to + queue NTBS work + +- “stable source event identity” — this is appropriately a TODO, but + should be described consistently + +- “external interaction” +- “response destination” +- “correlation record” +- “shared-resource coordination” +- “provider execution” + +The most distracting ones are qualifying, authorized, accepted, and +enabled. I’d replace them with concrete language such as: + +- “an event that matches one of the triggers below” +- “an event accepted after webhook/authentication checks” +- “the external object or conversation that contains the event” +- “the exact comment or message to which T3 posts the answer” + +In ntsb.md: + +- “canonical” event log +- “explicit subset” of commands/events/state +- “projected state” +- “limited clients” +- “source-event translation” +- “accepted external event” +- “agent turn” +- “captured source snapshot” +- “response target” +- “correlation record” +- “T3-only context” +- “external interaction” +- “lifecycle semantics” +- “deliberately omitted or unsupported” + +There are also two concrete leftovers: + +- The open question at line 64 still says events may be “recorded + without starting work,” even though we moved that out of scope. + +- Line 70 is a decision—“NTBS does not target existing execution + threads”—but it is sitting among open questions and should not be + phrased as one. + +The biggest cleanup would be to remove qualifying, authorized, and +accepted wherever they are not carrying a distinct security or lifecycle +meaning, then define the few terms we actually need: external event, +external interaction, captured snapshot, T3 thread, and response +destination. diff --git a/docs/planning/ntsb-event-processing.md b/docs/planning/ntsb-event-processing.md new file mode 100644 index 00000000000..bb0254979c3 --- /dev/null +++ b/docs/planning/ntsb-event-processing.md @@ -0,0 +1,60 @@ +# NTSB 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. + +## 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. + +## 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. + +## 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. diff --git a/docs/planning/ntsb.md b/docs/planning/ntsb.md index 66e0e792131..2e18792a4bc 100644 --- a/docs/planning/ntsb.md +++ b/docs/planning/ntsb.md @@ -34,11 +34,11 @@ Implementation is out of scope for this planning stage. ## Proposal: A triggering event creates a new thread -Each accepted external event that triggers an agent turn creates a new T3 thread. The first user message is constructed from the current authorized snapshot of the external source together with the event that triggered the run. In the existing T3 protocol, this can be expressed with `thread.turn.start` and `bootstrap.createThread`; it does not require a separate NTBS command. +Each accepted external event that triggers an agent turn creates a new T3 thread. The first user message is constructed from the captured source snapshot of the external source together with the event that triggered the run. Authorization to access the source is checked separately. In the existing T3 protocol, this can be expressed with `thread.turn.start` and `bootstrap.createThread`; it does not require a separate NTBS command. An external event is not the same as a delivery attempt. Retries and duplicate webhook deliveries must resolve to the same accepted event and must not create additional threads. The identity and idempotency rules needed to guarantee this are still to be designed. -Triggering events for the same external interaction are processed in order. If an earlier event's T3 thread is still running, a later event waits. It does not run concurrently and does not alter, steer, or continue the active thread. When its turn comes, the later event starts its own T3 thread. +Triggering events for the same external interaction may start T3 threads concurrently. Each thread is correlated with the external event that created it, and its output is projected to that event's response target. A later event does not alter, steer, or continue an earlier thread merely because both belong to the same external interaction. ### Advantages @@ -57,17 +57,16 @@ Triggering events for the same external interaction are processed in order. If a - A high-volume external discussion may create many short-lived T3 threads, increasing storage and making native T3 discovery noisier. - Independent threads can still target the same worktree or other mutable resource, so execution needs separate serialization or conflict rules. - An external reply cannot continue an in-flight approval, user-input request, interrupt, or steering interaction merely by creating another thread; those interactions would need an explicit command targeting the existing thread or be unsupported. -- Reliable replay requires the system to retain or reconstruct the exact authorized source snapshot used for the run, not merely fetch whatever the source contains later. +- Reliable replay requires the system to retain or reconstruct the exact captured source snapshot used for the run, not merely fetch whatever the source contains later. ## Open questions -- Which external messages or state changes trigger an agent turn, and which are ignored or recorded without starting work? -- What identifies the same external interaction for sequential processing: a Jira issue, Discord thread, GitHub issue or pull request, nested review discussion, Teams conversation, or another scope? -- Is the source snapshot frozen when an event is accepted or fetched when its queued execution begins? -- If the source is edited or deleted while its event is waiting, does the queued event retain its original snapshot, get replaced, or get cancelled? +- What identifies the same external interaction for correlation and projection: a Jira issue, Discord thread, GitHub issue or pull request, nested review discussion, Teams conversation, or another scope? +- Is the source snapshot frozen when an event is accepted or fetched immediately before its thread starts? +- If resource coordination delays a thread after its event is accepted, does it retain its original snapshot, or may the snapshot be refreshed? - What stable identity distinguishes an accepted external event from duplicate, retried, late, or out-of-order deliveries? -- Does sequential processing apply only within one external interaction, or must executions that share a worktree or another mutable resource also wait for each other? -- Is bootstrap `thread.turn.start` the only command NTBS may issue, or are any commands targeting an existing execution thread supported? +- How are concurrent executions that share a worktree or another mutable resource coordinated without imposing an event queue? +- NTBS does not target existing execution threads; each accepted event uses the new-thread form of `thread.turn.start`. - Which T3 events and projected fields may NTBS clients consume, and which are deliberately omitted or unsupported? - How do clients obtain an initial snapshot, resume from a cursor, replay missed changes, and recover when their cursor is no longer valid? - How are completion, failure, timeout, and cancellation represented and rendered on limited external platforms? @@ -76,5 +75,6 @@ Triggering events for the same external interaction are processed in order. If a ## Agreed decisions -- Each accepted external event that triggers an agent turn creates a new T3 thread from the authorized external source snapshot and the triggering event. -- Triggering events for the same external interaction are processed sequentially. A later event waits for the earlier event's thread to finish, then starts a new thread. +### 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 [ntsb-event-processing.md](./ntsb-event-processing.md). From 068d2f5fac3dc709373c51a7bdf8da4c3c2d3b5b Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Tue, 4 Aug 2026 16:19:41 +0200 Subject: [PATCH 04/29] chore: planning of ntsb processing --- docs/planning/ntsb-event-processing.md | 7 +++++ docs/planning/ntsb.md | 41 ++++++-------------------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/docs/planning/ntsb-event-processing.md b/docs/planning/ntsb-event-processing.md index bb0254979c3..e8a7126bfec 100644 --- a/docs/planning/ntsb-event-processing.md +++ b/docs/planning/ntsb-event-processing.md @@ -52,6 +52,13 @@ Events that do not match one of the triggers above are ignored. Whether adapters 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. diff --git a/docs/planning/ntsb.md b/docs/planning/ntsb.md index 2e18792a4bc..734916fa938 100644 --- a/docs/planning/ntsb.md +++ b/docs/planning/ntsb.md @@ -14,50 +14,27 @@ Non-turn-based surfaces (NTBS) such as Discord, Jira, Teams, GitHub issues, and - 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 T3 event log remains canonical. NTBS support should select and project an explicit subset of existing T3 commands, events, and state, while platform adapters translate between that subset and each platform's native concepts. +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: -- what an external object corresponds to in T3; -- which T3 commands an external participant may cause; -- which T3 events and projected state an NTBS client may observe; -- how later external changes, multiple participants, retries, and replay affect that state; -- what limited clients render, ignore, or report as unsupported. +- 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 shared contract should be smaller than the full interactive-client protocol, event-based, and usable through both snapshots and incremental changes. Platform adapters should remain responsible for authentication, source-event translation, transport, and rendering—not for defining their own conversation semantics. +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 will capture the protocol design one decision at a time. It does not yet prescribe an object-to-thread mapping, a command or event subset, lifecycle semantics, cursor rules, or adapter behavior. Those decisions will be added only after they are discussed and agreed. +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 accepted external event that triggers an agent turn creates a new T3 thread. The first user message is constructed from the captured source snapshot of the external source together with the event that triggered the run. Authorization to access the source is checked separately. In the existing T3 protocol, this can be expressed with `thread.turn.start` and `bootstrap.createThread`; it does not require a separate NTBS command. - -An external event is not the same as a delivery attempt. Retries and duplicate webhook deliveries must resolve to the same accepted event and must not create additional threads. The identity and idempotency rules needed to guarantee this are still to be designed. - -Triggering events for the same external interaction may start T3 threads concurrently. Each thread is correlated with the external event that created it, and its output is projected to that event's response target. A later event does not alter, steer, or continue an earlier thread merely because both belong to the same external interaction. - -### Advantages - -- The external source remains the participant-visible context for the run; behavior does not depend on hidden T3 conversation history that external participants cannot inspect. -- Each run has an isolated and auditable input, actor, output, and lifecycle. -- An edit can trigger a new run from the updated source snapshot without rewriting the history of a previous T3 thread. -- Different participants do not implicitly inherit stale or private context accumulated in an earlier agent session. -- Replay can reconstruct what the agent was asked to do from the captured source version and triggering event. -- Closing, reopening, deleting, or moving an external object does not need to masquerade as T3 thread lifecycle. -- The normal path initially needs only the existing bootstrap form of `thread.turn.start`. - -### Costs and limitations - -- Rebuilding the external snapshot for every run may increase prompt size, latency, and model cost. -- T3-only context such as intermediate tool activity or prior instructions is lost unless it is deliberately included in the new prompt. -- A high-volume external discussion may create many short-lived T3 threads, increasing storage and making native T3 discovery noisier. -- Independent threads can still target the same worktree or other mutable resource, so execution needs separate serialization or conflict rules. -- An external reply cannot continue an in-flight approval, user-input request, interrupt, or steering interaction merely by creating another thread; those interactions would need an explicit command targeting the existing thread or be unsupported. -- Reliable replay requires the system to retain or reconstruct the exact captured source snapshot used for the run, not merely fetch whatever the source contains later. +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 [ntsb-event-processing.md](./ntsb-event-processing.md). ## Open questions From 6d7069d0e42bf3cf3c6e3fa52dd98bad0bcdaf3b Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Tue, 4 Aug 2026 22:43:07 +0200 Subject: [PATCH 05/29] chore: update ntsb planning --- docs/planning/ntsb.md | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/planning/ntsb.md b/docs/planning/ntsb.md index 734916fa938..0ecbb15f381 100644 --- a/docs/planning/ntsb.md +++ b/docs/planning/ntsb.md @@ -36,22 +36,35 @@ Implementation is out of scope for this planning stage. 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 [ntsb-event-processing.md](./ntsb-event-processing.md). -## Open questions - -- What identifies the same external interaction for correlation and projection: a Jira issue, Discord thread, GitHub issue or pull request, nested review discussion, Teams conversation, or another scope? -- Is the source snapshot frozen when an event is accepted or fetched immediately before its thread starts? -- If resource coordination delays a thread after its event is accepted, does it retain its original snapshot, or may the snapshot be refreshed? -- What stable identity distinguishes an accepted external event from duplicate, retried, late, or out-of-order deliveries? -- How are concurrent executions that share a worktree or another mutable resource coordinated without imposing an event queue? -- NTBS does not target existing execution threads; each accepted event uses the new-thread form of `thread.turn.start`. -- Which T3 events and projected fields may NTBS clients consume, and which are deliberately omitted or unsupported? -- How do clients obtain an initial snapshot, resume from a cursor, replay missed changes, and recover when their cursor is no longer valid? -- How are completion, failure, timeout, and cancellation represented and rendered on limited external platforms? -- How is an external event correlated with its T3 execution thread and the response rendered back onto the source? -- How long are captured source snapshots, execution threads, and their correlation records retained, and how are they presented in native T3 clients? - ## 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 [ntsb-event-processing.md](./ntsb-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. From 77b38e7e4de2feee65d0716155a0ca4b12d2f861 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Wed, 5 Aug 2026 11:53:51 +0200 Subject: [PATCH 06/29] chore: planning ntsb output --- docs/planning/ntsb-event-processing.md | 33 ++++++++++++++++++-------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/planning/ntsb-event-processing.md b/docs/planning/ntsb-event-processing.md index e8a7126bfec..ba3a3ef1244 100644 --- a/docs/planning/ntsb-event-processing.md +++ b/docs/planning/ntsb-event-processing.md @@ -4,64 +4,77 @@ 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. -## Core rule +## 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. -## Platform triggers +### Platform triggers The following source interactions start a new thread: -### Jira +#### 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 +#### 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 +#### 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 +### 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 +### 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 +### 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 +### 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 +### 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 + +The following questions remain open for the T3-to-NTBS path: + +- Which T3 outputs are rendered on the external platform: only the final answer, or also intermediate updates, tool results, attachments, and generated artifacts? +- Does the adapter post the result as a reply, create a new comment or message, or update a message created earlier for the same turn? +- How does each adapter translate T3 formatting and content into the external platform's supported format and size limits? +- What happens when a response cannot be posted, is posted only partially, or must be retried? +- How are responses to concurrent turns ordered or labeled when they appear in the same external interaction? +- Which T3 events are intentionally kept inside T3 rather than rendered externally? From d9630dec63f75bb3706effe221af8ff642fd3ec2 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Wed, 5 Aug 2026 13:53:33 +0200 Subject: [PATCH 07/29] chore: more processing --- docs/planning/ntsb-event-processing.md | 47 ++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/planning/ntsb-event-processing.md b/docs/planning/ntsb-event-processing.md index ba3a3ef1244..a4c269104b9 100644 --- a/docs/planning/ntsb-event-processing.md +++ b/docs/planning/ntsb-event-processing.md @@ -4,6 +4,12 @@ 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 @@ -12,6 +18,15 @@ An external event starts a new T3 thread when the adapter recognizes it as one o 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: @@ -70,10 +85,36 @@ Multiple events from the same external interaction may create T3 threads at the ## Outbound response processing -The following questions remain open for the T3-to-NTBS path: +Outbound processing adds the acknowledgement and final-outcome message IDs, together with whether each message was posted. + +### Agreed decisions + +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. + +#### 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. + +### Open questions + +#### Shared questions -- Which T3 outputs are rendered on the external platform: only the final answer, or also intermediate updates, tool results, attachments, and generated artifacts? -- Does the adapter post the result as a reply, create a new comment or message, or update a message created earlier for the same turn? - How does each adapter translate T3 formatting and content into the external platform's supported format and size limits? - What happens when a response cannot be posted, is posted only partially, or must be retried? - How are responses to concurrent turns ordered or labeled when they appear in the same external interaction? From aa2dc1f9c081922ff834b36e221d6dfb5a515219 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Wed, 5 Aug 2026 14:06:38 +0200 Subject: [PATCH 08/29] feat: finish processing --- docs/planning/ntsb-event-processing.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/planning/ntsb-event-processing.md b/docs/planning/ntsb-event-processing.md index a4c269104b9..52b19b372d5 100644 --- a/docs/planning/ntsb-event-processing.md +++ b/docs/planning/ntsb-event-processing.md @@ -89,8 +89,20 @@ Outbound processing adds the acknowledgement and final-outcome message IDs, toge ### 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: @@ -110,12 +122,3 @@ The adapter retains the thread or channel ID, invoking message ID, acknowledgeme ##### 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. - -### Open questions - -#### Shared questions - -- How does each adapter translate T3 formatting and content into the external platform's supported format and size limits? -- What happens when a response cannot be posted, is posted only partially, or must be retried? -- How are responses to concurrent turns ordered or labeled when they appear in the same external interaction? -- Which T3 events are intentionally kept inside T3 rather than rendered externally? From 15e309dd14b98e9a8d484593eda54c2f033bb547 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Wed, 5 Aug 2026 14:49:31 +0200 Subject: [PATCH 09/29] chore: kickstart architecture document --- docs/planning/ntsb-architecture.md | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/planning/ntsb-architecture.md diff --git a/docs/planning/ntsb-architecture.md b/docs/planning/ntsb-architecture.md new file mode 100644 index 00000000000..2043ed93408 --- /dev/null +++ b/docs/planning/ntsb-architecture.md @@ -0,0 +1,54 @@ +# NTBS architecture + +**Status:** exploratory planning + +This document defines the boundary between T3 and adapters for non-turn-based surfaces such as Jira, GitHub, Discord, and Teams. It explains which system retains which information and the shared path from an external event to a T3 result and back to the external platform. + +## Problem + +T3 clients are built around T3 data views such as threads, diffs, and projects. External platforms know none of those concepts. They only know their own messages, comments, conversations, and identifiers. + +An adapter therefore cannot rely on an external platform to retain T3 state, and T3 cannot infer where a later result belongs from its own thread data alone. The adapter must retain the link between its platform's event and the T3 work created from it. + +## Shared model + +An adapter receives a platform event, applies the trigger rules, captures the source snapshot, and creates a new T3 thread. It retains the platform identifiers and the T3 identifiers created from that event. + +The adapter sends an acknowledgement to the external platform. When T3 reports the thread's final outcome, the adapter uses its retained record to post the final answer, failure, timeout, or cancellation in the correct place. + +T3 remains independent of the platform that produced the event. It owns its threads, messages, turns, execution state, worktrees, and branches. The adapter owns platform authentication, event delivery, source snapshots, platform identifiers, response placement, and platform-specific rendering. + +## Adapter record + +For each event that starts T3 work, the adapter needs a durable record containing: + +- the platform's source event ID and version; +- the source context and message or comment identifiers; +- the captured source snapshot; +- the T3 thread, user-message, and turn identifiers created from the event; +- the acknowledgement and final-message identifiers, when they have been posted; +- the delivery state for both outbound messages. + +This record lets the adapter avoid creating duplicate threads, resume after a restart, and deliver a later T3 outcome to the correct external location. + +## Shared flow + +1. The adapter receives an external event and decides whether it starts T3 work. +2. The adapter creates or reuses its durable record and captures the source snapshot. +3. The adapter asks T3 to create a new thread and retains the resulting T3 identifiers. +4. The adapter posts the acknowledgement and records its message identifier. +5. T3 reports the thread's final outcome. +6. The adapter finds the corresponding record, posts the outcome, and records the result of that delivery. + +## Decisions still needed + +- Define the request from an adapter to T3: the source snapshot, target project, starting revision, and execution settings. +- Define how an adapter receives thread outcomes from T3, including replay after an adapter restart. +- Define the exact idempotency rules for source events, edits, retries, late delivery, and out-of-order delivery. +- Choose the durable storage implementation and retention policy for adapter records. +- Define what happens when the source object is deleted, closed, archived, or otherwise changes while T3 work is running. + +## Related documents + +- [ntsb.md](./ntsb.md) records the overall scope and agreed decisions. +- [ntsb-event-processing.md](./ntsb-event-processing.md) defines inbound triggers and outbound messages on each platform. From 5308496901e1841ba392bfa5db8390442c72fadf Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Wed, 5 Aug 2026 16:25:32 +0200 Subject: [PATCH 10/29] chore: settle on generic definition --- docs/planning/ntsb-architecture.md | 103 ++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 16 deletions(-) diff --git a/docs/planning/ntsb-architecture.md b/docs/planning/ntsb-architecture.md index 2043ed93408..387158ecefc 100644 --- a/docs/planning/ntsb-architecture.md +++ b/docs/planning/ntsb-architecture.md @@ -18,27 +18,97 @@ The adapter sends an acknowledgement to the external platform. When T3 reports t T3 remains independent of the platform that produced the event. It owns its threads, messages, turns, execution state, worktrees, and branches. The adapter owns platform authentication, event delivery, source snapshots, platform identifiers, response placement, and platform-specific rendering. -## Adapter record +The adapter keeps the full record for its platform. T3 does not receive or interpret the adapter's source-event data or response destination. The adapter detects repeated deliveries, creates the snapshot, and asks T3 to start work. T3 receives the snapshot, returns the new thread, message, and turn IDs, and later reports the final outcome. The adapter adds those T3 values to its own record and posts the result on its platform. -For each event that starts T3 work, the adapter needs a durable record containing: +## Event lifecycle -- the platform's source event ID and version; -- the source context and message or comment identifiers; -- the captured source snapshot; -- the T3 thread, user-message, and turn identifiers created from the event; -- the acknowledgement and final-message identifiers, when they have been posted; -- the delivery state for both outbound messages. +Starting from an external event, this happens: -This record lets the adapter avoid creating duplicate threads, resume after a restart, and deliver a later T3 outcome to the correct external location. +1. The adapter accepts an external event that matches a trigger. It creates an adapter record containing the source identifiers, response destination, and captured snapshot. +2. The adapter asks T3 to create a new thread from that snapshot. +3. T3 creates the thread, user message, and turn. The adapter adds those IDs to its record. +4. The adapter posts the acknowledgement and adds its message ID to the record. +5. T3 produces the final answer, failure, timeout, or cancellation for that turn. +6. The adapter finds the record from the T3 IDs, posts the final message at its stored response destination, and adds the final-message ID to the record. -## Shared flow +## Adapter record -1. The adapter receives an external event and decides whether it starts T3 work. -2. The adapter creates or reuses its durable record and captures the source snapshot. -3. The adapter asks T3 to create a new thread and retains the resulting T3 identifiers. -4. The adapter posts the acknowledgement and records its message identifier. -5. T3 reports the thread's final outcome. -6. The adapter finds the corresponding record, posts the outcome, and records the result of that delivery. +Before it asks T3 to create a thread, the adapter record contains: + +- the adapter's own source-event data; +- the adapter's own response destination; +- the captured source snapshot as a string; + +After T3 creates the thread, the adapter adds the T3 thread, user-message, and turn IDs. + +After it posts the acknowledgement and final response, the adapter adds their message IDs. + +The record retains the accepted source event, the new T3 thread it starts, and the messages the adapter sends for that thread. + +`NtsbEventRecord` is a TypeScript pattern for adapter code, not a shared storage format. Each adapter defines, validates, and stores its own source-event data and response destination. + +```ts +/** + * Tracks the lifecycle of external inbound events, such as comments or messages, that trigger T3 work. + * External applications have no relationship to T3, and vice versa. The adapter relates events in one to the other. + */ +type NtsbEventRecord = { + /** Adapter-defined information about the inbound event. */ + source: SourceEvent; + /** Adapter-defined information about where replies belong. */ + responseDestination: ResponseDestination; + /** The captured source text used to create T3's first user message. */ + snapshot: string; + /** 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; + }; + /** The external acknowledgement message posted by the adapter. */ + acknowledgementMessageId?: string; + /** The external final message posted by the adapter. */ + finalMessageId?: string; +}; +``` + +The optional fields in this initial type represent different points in the event lifecycle. They must be replaced with separate record shapes once those lifecycle transitions have been fully defined. + +## 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 +{ + source: { + platform: "jira", + 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 +t3: { + threadId: "thread-1", + userMessageId: "message-1", + turnId: "turn-1", +} +``` + +The adapter posts an acknowledgement as a reply to Jira comment `10401` and adds `acknowledgementMessageId: "10402"`. When T3 produces the final result for `turn-1`, the adapter finds this record, posts another reply to comment `10401`, and adds `finalMessageId: "10403"`. ## Decisions still needed @@ -46,6 +116,7 @@ This record lets the adapter avoid creating duplicate threads, resume after a re - Define how an adapter receives thread outcomes from T3, including replay after an adapter restart. - Define the exact idempotency rules for source events, edits, retries, late delivery, and out-of-order delivery. - Choose the durable storage implementation and retention policy for adapter records. +- Define separate record shapes for each lifecycle transition, replacing the optional fields in `NtsbEventRecord`. - Define what happens when the source object is deleted, closed, archived, or otherwise changes while T3 work is running. ## Related documents From fb65536da0a607ea2dbe7bef838c30547f9bc04b Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Wed, 5 Aug 2026 17:05:50 +0200 Subject: [PATCH 11/29] chore: document lifecycle --- docs/planning/ntsb-architecture.md | 105 +++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 28 deletions(-) diff --git a/docs/planning/ntsb-architecture.md b/docs/planning/ntsb-architecture.md index 387158ecefc..036f36b6613 100644 --- a/docs/planning/ntsb-architecture.md +++ b/docs/planning/ntsb-architecture.md @@ -18,7 +18,7 @@ The adapter sends an acknowledgement to the external platform. When T3 reports t T3 remains independent of the platform that produced the event. It owns its threads, messages, turns, execution state, worktrees, and branches. The adapter owns platform authentication, event delivery, source snapshots, platform identifiers, response placement, and platform-specific rendering. -The adapter keeps the full record for its platform. T3 does not receive or interpret the adapter's source-event data or response destination. The adapter detects repeated deliveries, creates the snapshot, and asks T3 to start work. T3 receives the snapshot, returns the new thread, message, and turn IDs, and later reports the final outcome. The adapter adds those T3 values to its own record and posts the result on its platform. +The adapter keeps the full record for its platform. T3 does not receive or interpret platform data. The adapter detects repeated deliveries, creates the snapshot, and asks T3 to start work. T3 receives the snapshot, returns the new thread, message, and turn IDs, and later reports the final outcome. The adapter adds those T3 values to its own record and posts the result on its platform. ## Event lifecycle @@ -35,32 +35,52 @@ Starting from an external event, this happens: Before it asks T3 to create a thread, the adapter record contains: -- the adapter's own source-event data; -- the adapter's own response destination; +- the adapter's platform data; - the captured source snapshot as a string; After T3 creates the thread, the adapter adds the T3 thread, user-message, and turn IDs. After it posts the acknowledgement and final response, the adapter adds their message IDs. -The record retains the accepted source event, the new T3 thread it starts, and the messages the adapter sends for that thread. +The event retains the accepted source event, the new T3 thread it starts, and the messages the adapter sends for that thread. -`NtsbEventRecord` is a TypeScript pattern for adapter code, not a shared storage format. Each adapter defines, validates, and stores its own source-event data and response destination. +`NtsbEvent` is a TypeScript pattern for adapter code, not a shared storage format. Each adapter defines, validates, and stores its own platform data. ```ts +/** All data that is specific to the external platform. */ +type PlatformData = { + /** Information about the inbound event. */ + source: Source; + /** Information about where replies belong. */ + responseDestination: ResponseDestination; +}; + /** * Tracks the lifecycle of external inbound events, such as comments or messages, that trigger T3 work. * External applications have no relationship to T3, and vice versa. The adapter relates events in one to the other. */ -type NtsbEventRecord = { - /** Adapter-defined information about the inbound event. */ - source: SourceEvent; - /** Adapter-defined information about where replies belong. */ - responseDestination: ResponseDestination; +type NtsbEvent

> = + | NtsbEventAccepted

+ | NtsbEventThreadStarted

+ | NtsbEventAcknowledgementPosted

+ | NtsbEventOutcomeAvailable

+ | NtsbEventResponsePosted

; + +type NtsbEventBase

> = { + /** 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 NtsbEventAccepted

> = NtsbEventBase

& { + /** The adapter has accepted the inbound event but has not started T3 work. */ + state: "accepted"; +}; + +type NtsbEventWithThread

> = NtsbEventBase

& { /** The T3 IDs created after the adapter starts work. */ - t3?: { + t3: { /** The T3 thread created from the source event. */ threadId: string; /** The first T3 user message created from the snapshot. */ @@ -68,14 +88,41 @@ type NtsbEventRecord = { /** The T3 turn started from that message. */ turnId: string; }; - /** The external acknowledgement message posted by the adapter. */ - acknowledgementMessageId?: string; - /** The external final message posted by the adapter. */ - finalMessageId?: string; }; + +type NtsbEventThreadStarted

> = NtsbEventWithThread

& { + /** T3 has created the new thread from the source snapshot. */ + state: "threadStarted"; +}; + +type NtsbEventWithAcknowledgement

> = + NtsbEventWithThread

& { + /** The external acknowledgement message posted by the adapter. */ + acknowledgementMessageId: string; + }; + +type NtsbEventAcknowledgementPosted

> = + NtsbEventWithAcknowledgement

& { + /** The adapter has posted the acknowledgement. */ + state: "acknowledgementPosted"; + }; + +type NtsbEventOutcomeAvailable

> = + NtsbEventWithAcknowledgement

& { + /** T3 has produced a final outcome for the turn. */ + state: "outcomeAvailable"; + }; + +type NtsbEventResponsePosted

> = + NtsbEventWithAcknowledgement

& { + /** The adapter has posted T3's final response. */ + state: "responsePosted"; + /** The external final message posted by the adapter. */ + finalMessageId: string; + }; ``` -The optional fields in this initial type represent different points in the event lifecycle. They must be replaced with separate record shapes once those lifecycle transitions have been fully defined. +TODO: Define error and retry lifecycle states when adapter behaviour is tested. ## Jira example @@ -83,16 +130,18 @@ A user adds top-level Jira comment `10401` on issue `T3-123`: `@agent investigat ```ts { - source: { - platform: "jira", - eventId: "jira-event-1", - version: "1", - contextId: "T3-123", - messageId: "10401", - }, - responseDestination: { - contextId: "T3-123", - parentMessageId: "10401", + 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", } @@ -101,6 +150,7 @@ A user adds top-level Jira comment `10401` on issue `T3-123`: `@agent investigat When T3 creates the work, the adapter adds its IDs: ```ts +state: "threadStarted", t3: { threadId: "thread-1", userMessageId: "message-1", @@ -108,7 +158,7 @@ t3: { } ``` -The adapter posts an acknowledgement as a reply to Jira comment `10401` and adds `acknowledgementMessageId: "10402"`. When T3 produces the final result for `turn-1`, the adapter finds this record, posts another reply to comment `10401`, and adds `finalMessageId: "10403"`. +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"`. ## Decisions still needed @@ -116,7 +166,6 @@ The adapter posts an acknowledgement as a reply to Jira comment `10401` and adds - Define how an adapter receives thread outcomes from T3, including replay after an adapter restart. - Define the exact idempotency rules for source events, edits, retries, late delivery, and out-of-order delivery. - Choose the durable storage implementation and retention policy for adapter records. -- Define separate record shapes for each lifecycle transition, replacing the optional fields in `NtsbEventRecord`. - Define what happens when the source object is deleted, closed, archived, or otherwise changes while T3 work is running. ## Related documents From 865dc6c37073dd483e1785cc71998fdd4e5ac270 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Wed, 5 Aug 2026 17:51:25 +0200 Subject: [PATCH 12/29] feat: define ntbs architeture --- docs/planning/ntsb-architecture.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/planning/ntsb-architecture.md b/docs/planning/ntsb-architecture.md index 036f36b6613..20d231f8b61 100644 --- a/docs/planning/ntsb-architecture.md +++ b/docs/planning/ntsb-architecture.md @@ -18,7 +18,19 @@ The adapter sends an acknowledgement to the external platform. When T3 reports t T3 remains independent of the platform that produced the event. It owns its threads, messages, turns, execution state, worktrees, and branches. The adapter owns platform authentication, event delivery, source snapshots, platform identifiers, response placement, and platform-specific rendering. -The adapter keeps the full record for its platform. T3 does not receive or interpret platform data. The adapter detects repeated deliveries, creates the snapshot, and asks T3 to start work. T3 receives the snapshot, returns the new thread, message, and turn IDs, and later reports the final outcome. The adapter adds those T3 values to its own record and posts the result on its platform. +The adapter keeps the full record for its platform. T3 does not receive or interpret platform data. The adapter makes sure the same platform message does not start T3 work twice, creates the snapshot, and asks T3 to start work. T3 receives the snapshot, returns the new thread, message, and turn IDs, and later reports the final outcome. The adapter adds those T3 values to its own record and posts the result on its platform. + +Storage and retention are adapter implementation details, not architecture decisions. Platform-specific edge cases, such as a source item being deleted or closed while T3 is working, also belong to the adapter implementation phase. + +## Passing T3 context + +An incoming platform event carries both platform data and the T3 context needed to start work, such as the project, revision, and execution context. The adapter forwards that T3 context to T3 when it creates the new thread. + +`NtsbEvent` does not retain the project, revision, or execution context as lifecycle data. Once T3 creates the thread, T3 owns that information. Keeping copies in `NtsbEvent` would require the adapter to keep them in sync with T3. + +## Receiving T3 outcomes + +Adapters subscribe to T3's event log, like other T3 consumers. After T3 starts a thread, `NtsbEvent` contains its turn ID. When the adapter receives the final outcome for that turn from the event log, it uses the same `NtsbEvent` to post the result on the external platform. ## Event lifecycle @@ -160,14 +172,6 @@ t3: { 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"`. -## Decisions still needed - -- Define the request from an adapter to T3: the source snapshot, target project, starting revision, and execution settings. -- Define how an adapter receives thread outcomes from T3, including replay after an adapter restart. -- Define the exact idempotency rules for source events, edits, retries, late delivery, and out-of-order delivery. -- Choose the durable storage implementation and retention policy for adapter records. -- Define what happens when the source object is deleted, closed, archived, or otherwise changes while T3 work is running. - ## Related documents - [ntsb.md](./ntsb.md) records the overall scope and agreed decisions. From eb9e1111fc7ebfb0522901b498f66dc1f620942a Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Wed, 5 Aug 2026 17:52:13 +0200 Subject: [PATCH 13/29] feat: rename ntsb -> ntbs --- docs/planning/feedback.md | 60 ------------------- ...b-architecture.md => ntbs-architecture.md} | 0 ...processing.md => ntbs-event-processing.md} | 0 docs/planning/{ntsb.md => ntbs.md} | 0 4 files changed, 60 deletions(-) delete mode 100644 docs/planning/feedback.md rename docs/planning/{ntsb-architecture.md => ntbs-architecture.md} (100%) rename docs/planning/{ntsb-event-processing.md => ntbs-event-processing.md} (100%) rename docs/planning/{ntsb.md => ntbs.md} (100%) diff --git a/docs/planning/feedback.md b/docs/planning/feedback.md deleted file mode 100644 index b465d3ffd3c..00000000000 --- a/docs/planning/feedback.md +++ /dev/null @@ -1,60 +0,0 @@ -In ntsb-event-processing.md: - -- “authorized request” -- “enabled interaction” -- “accepted request/event” -- “qualifying event” -- “request for the agent” -- “new explicit invocation” -- “source snapshot permitted by the access check” -- “pending turn record” — especially wrong now that we decided not to - queue NTBS work - -- “stable source event identity” — this is appropriately a TODO, but - should be described consistently - -- “external interaction” -- “response destination” -- “correlation record” -- “shared-resource coordination” -- “provider execution” - -The most distracting ones are qualifying, authorized, accepted, and -enabled. I’d replace them with concrete language such as: - -- “an event that matches one of the triggers below” -- “an event accepted after webhook/authentication checks” -- “the external object or conversation that contains the event” -- “the exact comment or message to which T3 posts the answer” - -In ntsb.md: - -- “canonical” event log -- “explicit subset” of commands/events/state -- “projected state” -- “limited clients” -- “source-event translation” -- “accepted external event” -- “agent turn” -- “captured source snapshot” -- “response target” -- “correlation record” -- “T3-only context” -- “external interaction” -- “lifecycle semantics” -- “deliberately omitted or unsupported” - -There are also two concrete leftovers: - -- The open question at line 64 still says events may be “recorded - without starting work,” even though we moved that out of scope. - -- Line 70 is a decision—“NTBS does not target existing execution - threads”—but it is sitting among open questions and should not be - phrased as one. - -The biggest cleanup would be to remove qualifying, authorized, and -accepted wherever they are not carrying a distinct security or lifecycle -meaning, then define the few terms we actually need: external event, -external interaction, captured snapshot, T3 thread, and response -destination. diff --git a/docs/planning/ntsb-architecture.md b/docs/planning/ntbs-architecture.md similarity index 100% rename from docs/planning/ntsb-architecture.md rename to docs/planning/ntbs-architecture.md diff --git a/docs/planning/ntsb-event-processing.md b/docs/planning/ntbs-event-processing.md similarity index 100% rename from docs/planning/ntsb-event-processing.md rename to docs/planning/ntbs-event-processing.md diff --git a/docs/planning/ntsb.md b/docs/planning/ntbs.md similarity index 100% rename from docs/planning/ntsb.md rename to docs/planning/ntbs.md From ea73116b37501d7bb27937f0f21cc9f2c37e4fa1 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Wed, 5 Aug 2026 17:54:29 +0200 Subject: [PATCH 14/29] fix: ntsb -> ntbs --- docs/planning/ntbs-architecture.md | 48 +++++++++++++------------- docs/planning/ntbs-event-processing.md | 2 +- docs/planning/ntbs.md | 4 +-- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/planning/ntbs-architecture.md b/docs/planning/ntbs-architecture.md index 20d231f8b61..b47cf90e0bd 100644 --- a/docs/planning/ntbs-architecture.md +++ b/docs/planning/ntbs-architecture.md @@ -26,11 +26,11 @@ Storage and retention are adapter implementation details, not architecture decis An incoming platform event carries both platform data and the T3 context needed to start work, such as the project, revision, and execution context. The adapter forwards that T3 context to T3 when it creates the new thread. -`NtsbEvent` does not retain the project, revision, or execution context as lifecycle data. Once T3 creates the thread, T3 owns that information. Keeping copies in `NtsbEvent` would require the adapter to keep them in sync with T3. +`NtbsEvent` does not retain the project, revision, or execution context as lifecycle data. Once T3 creates the thread, T3 owns that information. Keeping copies in `NtbsEvent` would require the adapter to keep them in sync with T3. ## Receiving T3 outcomes -Adapters subscribe to T3's event log, like other T3 consumers. After T3 starts a thread, `NtsbEvent` contains its turn ID. When the adapter receives the final outcome for that turn from the event log, it uses the same `NtsbEvent` to post the result on the external platform. +Adapters subscribe to T3's event log, like other T3 consumers. After T3 starts a thread, `NtbsEvent` contains its turn ID. When the adapter receives the final outcome for that turn from the event log, it uses the same `NtbsEvent` to post the result on the external platform. ## Event lifecycle @@ -56,7 +56,7 @@ After it posts the acknowledgement and final response, the adapter adds their me The event retains the accepted source event, the new T3 thread it starts, and the messages the adapter sends for that thread. -`NtsbEvent` is a TypeScript pattern for adapter code, not a shared storage format. Each adapter defines, validates, and stores its own platform data. +`NtbsEvent` is a TypeScript pattern for adapter code, not a shared storage format. Each adapter defines, validates, and stores its own platform data. ```ts /** All data that is specific to the external platform. */ @@ -71,26 +71,26 @@ type PlatformData = { * Tracks the lifecycle of external inbound events, such as comments or messages, that trigger T3 work. * External applications have no relationship to T3, and vice versa. The adapter relates events in one to the other. */ -type NtsbEvent

> = - | NtsbEventAccepted

- | NtsbEventThreadStarted

- | NtsbEventAcknowledgementPosted

- | NtsbEventOutcomeAvailable

- | NtsbEventResponsePosted

; - -type NtsbEventBase

> = { +type NtbsEvent

> = + | 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 NtsbEventAccepted

> = NtsbEventBase

& { +type NtbsEventAccepted

> = NtbsEventBase

& { /** The adapter has accepted the inbound event but has not started T3 work. */ state: "accepted"; }; -type NtsbEventWithThread

> = NtsbEventBase

& { +type NtbsEventWithThread

> = NtbsEventBase

& { /** The T3 IDs created after the adapter starts work. */ t3: { /** The T3 thread created from the source event. */ @@ -102,31 +102,31 @@ type NtsbEventWithThread

> = NtsbEventBa }; }; -type NtsbEventThreadStarted

> = NtsbEventWithThread

& { +type NtbsEventThreadStarted

> = NtbsEventWithThread

& { /** T3 has created the new thread from the source snapshot. */ state: "threadStarted"; }; -type NtsbEventWithAcknowledgement

> = - NtsbEventWithThread

& { +type NtbsEventWithAcknowledgement

> = + NtbsEventWithThread

& { /** The external acknowledgement message posted by the adapter. */ acknowledgementMessageId: string; }; -type NtsbEventAcknowledgementPosted

> = - NtsbEventWithAcknowledgement

& { +type NtbsEventAcknowledgementPosted

> = + NtbsEventWithAcknowledgement

& { /** The adapter has posted the acknowledgement. */ state: "acknowledgementPosted"; }; -type NtsbEventOutcomeAvailable

> = - NtsbEventWithAcknowledgement

& { +type NtbsEventOutcomeAvailable

> = + NtbsEventWithAcknowledgement

& { /** T3 has produced a final outcome for the turn. */ state: "outcomeAvailable"; }; -type NtsbEventResponsePosted

> = - NtsbEventWithAcknowledgement

& { +type NtbsEventResponsePosted

> = + NtbsEventWithAcknowledgement

& { /** The adapter has posted T3's final response. */ state: "responsePosted"; /** The external final message posted by the adapter. */ @@ -174,5 +174,5 @@ The adapter posts an acknowledgement as a reply to Jira comment `10401`, changes ## Related documents -- [ntsb.md](./ntsb.md) records the overall scope and agreed decisions. -- [ntsb-event-processing.md](./ntsb-event-processing.md) defines inbound triggers and outbound messages on each platform. +- [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 index 52b19b372d5..22391a340d9 100644 --- a/docs/planning/ntbs-event-processing.md +++ b/docs/planning/ntbs-event-processing.md @@ -1,4 +1,4 @@ -# NTSB event processing +# NTBS event processing **Status:** exploratory planning diff --git a/docs/planning/ntbs.md b/docs/planning/ntbs.md index 0ecbb15f381..67b2f3f2298 100644 --- a/docs/planning/ntbs.md +++ b/docs/planning/ntbs.md @@ -34,13 +34,13 @@ 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 [ntsb-event-processing.md](./ntsb-event-processing.md). +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 [ntsb-event-processing.md](./ntsb-event-processing.md). +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? From 5d051b5badeb06067a4098ae2f4ed93b3f29e82f Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Thu, 6 Aug 2026 15:57:27 +0200 Subject: [PATCH 15/29] feat: write plan --- docs/planning/ntbs-plan.md | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/planning/ntbs-plan.md 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) From 18997466c25ef6beef554c00e7fd3e89789c6d3a Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Fri, 7 Aug 2026 16:32:59 +0200 Subject: [PATCH 16/29] feat: implement basic lifecycle types --- apps/server/src/ntbs/schemas.ts | 68 +++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 apps/server/src/ntbs/schemas.ts diff --git a/apps/server/src/ntbs/schemas.ts b/apps/server/src/ntbs/schemas.ts new file mode 100644 index 00000000000..3f9b84d538c --- /dev/null +++ b/apps/server/src/ntbs/schemas.ts @@ -0,0 +1,68 @@ +/** + * 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. + */ +type PlatformData = { + source: Source; + responseDestination: ResponseDestination; +}; + +type LifecycleEvent

= { + /** + * 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; +}; + +type ThreadEvent

= LifecycleEvent

& { + /** The T3 IDs created by the adapter */ + t3Data: { + /** The T3 thread created by the lifecycle event */ + threadId: string; + }; +}; + +type RequestAccepted

= LifecycleEvent

& { + state: "request.accepted"; +}; + +type ThreadStarted

= ThreadEvent

& { + /** T3 has created the new thread from the source snapshot */ + state: "thread.started"; +}; + +type ThreadStartedAcknowledgement

= ThreadEvent

& { + state: "thread.started.acknowledged"; + /** the external's platform identification of the acknowledgment message */ + acknowledgementMessageId: string; +}; + +type ResponseAvailable

= ThreadEvent

& { + state: "thread.response.available"; + /** the external's platform identification of the acknowledgment message */ + acknowledgementMessageId: string; +}; + +type ResponsePosted

= ThreadEvent

& { + state: "thread.response.posted"; + /** the external's platform identification of the acknowledgment message */ + acknowledgementMessageId: string; + responseMessageId: string; +}; + +type NTBSLifecycle

= + | RequestAccepted

+ | ThreadStarted

+ | ThreadStartedAcknowledgement

+ | ResponseAvailable

+ | ResponsePosted

; From 5b9658c9fba0a8762181b538a8cd41e331695f1f Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Fri, 7 Aug 2026 17:23:44 +0200 Subject: [PATCH 17/29] feat: implement adapter context service --- apps/server/src/ntbs/adapter.ts | 34 +++++++++++++++++++++++++++++++++ apps/server/src/ntbs/schemas.ts | 19 +++++++++--------- 2 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 apps/server/src/ntbs/adapter.ts diff --git a/apps/server/src/ntbs/adapter.ts b/apps/server/src/ntbs/adapter.ts new file mode 100644 index 00000000000..393d43f1547 --- /dev/null +++ b/apps/server/src/ntbs/adapter.ts @@ -0,0 +1,34 @@ +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; +}> {} + +export interface NTBSAdapter

{ + readonly accept: ( + event: NTBS.RequestAccepted

, + ) => Effect.Effect<"accepted" | "duplicate", AdapterError>; + readonly save: (lifecycleEvent: NTBS.NTBSLifecycle

) => Effect.Effect; + readonly postAcknowledgement: ( + event: NTBS.ThreadStarted

, + ) => Effect.Effect; + readonly postResponse: ( + event: NTBS.ResponseAvailable

, + text: string, + ) => Effect.Effect; + readonly findByThreadId: ( + threadId: string, + ) => Effect.Effect< + Exclude, NTBS.RequestAccepted

>, + ThreadNotFound | AdapterError + >; +} + +export const makeNTBSAdapter =

(key: string) => + Context.Service>(key); diff --git a/apps/server/src/ntbs/schemas.ts b/apps/server/src/ntbs/schemas.ts index 3f9b84d538c..c409874d464 100644 --- a/apps/server/src/ntbs/schemas.ts +++ b/apps/server/src/ntbs/schemas.ts @@ -6,12 +6,12 @@ * a bot, etc) `source` and `responseDestination` hold the details * necessary to process the what and why. */ -type PlatformData = { +export type PlatformData = { source: Source; responseDestination: ResponseDestination; }; -type LifecycleEvent

= { +export type LifecycleEvent

= { /** * Each NTBSEvent carries the adapter-defined external data. * T3 never inspects it. Only the adapter deals with it. @@ -24,43 +24,42 @@ type LifecycleEvent

= { snapshot: string; }; -type ThreadEvent

= LifecycleEvent

& { - /** The T3 IDs created by the adapter */ +export type ThreadEvent

= LifecycleEvent

& { t3Data: { /** The T3 thread created by the lifecycle event */ threadId: string; }; }; -type RequestAccepted

= LifecycleEvent

& { +export type RequestAccepted

= LifecycleEvent

& { state: "request.accepted"; }; -type ThreadStarted

= ThreadEvent

& { +export type ThreadStarted

= ThreadEvent

& { /** T3 has created the new thread from the source snapshot */ state: "thread.started"; }; -type ThreadStartedAcknowledgement

= ThreadEvent

& { +export type ThreadStartedAcknowledgement

= ThreadEvent

& { state: "thread.started.acknowledged"; /** the external's platform identification of the acknowledgment message */ acknowledgementMessageId: string; }; -type ResponseAvailable

= ThreadEvent

& { +export type ResponseAvailable

= ThreadEvent

& { state: "thread.response.available"; /** the external's platform identification of the acknowledgment message */ acknowledgementMessageId: string; }; -type ResponsePosted

= ThreadEvent

& { +export type ResponsePosted

= ThreadEvent

& { state: "thread.response.posted"; /** the external's platform identification of the acknowledgment message */ acknowledgementMessageId: string; responseMessageId: string; }; -type NTBSLifecycle

= +export type NTBSLifecycle

= | RequestAccepted

| ThreadStarted

| ThreadStartedAcknowledgement

From fd92f5b3ea2221ff4386716ab71f397265020457 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Fri, 7 Aug 2026 18:23:21 +0200 Subject: [PATCH 18/29] feat: work on processor --- apps/server/src/ntbs/processor.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 apps/server/src/ntbs/processor.ts diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts new file mode 100644 index 00000000000..4462eebbbd3 --- /dev/null +++ b/apps/server/src/ntbs/processor.ts @@ -0,0 +1,29 @@ +import type { OrchestrationEvent, ProjectId } from "@t3tools/contracts"; +import type * as NTBS from "./schemas.ts"; +import { Data, Effect, Scope } from "effect"; + +export type T3Context = { + readonly projectId: ProjectId; + readonly revision: string; +}; + +export type ProcessorEvent

= + | { + readonly source: "adapter"; + readonly event: NTBS.LifecycleEvent

; + readonly t3Context: T3Context; + } + | { + readonly source: "t3"; + readonly event: OrchestrationEvent; + }; + +export class NTBSProcessorError extends Data.TaggedError("NTBSProcessorError")<{ + reason: string; +}> {} + +export interface NTBSProcessor

{ + readonly process: (event: ProcessorEvent

) => Effect.Effect; + + readonly start: () => Effect.Effect; +} From 432e8398f55478aa9f12fc94e89dd3ccf01e2004 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Fri, 7 Aug 2026 20:29:16 +0200 Subject: [PATCH 19/29] feat: processor types --- apps/server/src/ntbs/processor.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts index 4462eebbbd3..50ff4ad11ff 100644 --- a/apps/server/src/ntbs/processor.ts +++ b/apps/server/src/ntbs/processor.ts @@ -1,6 +1,6 @@ import type { OrchestrationEvent, ProjectId } from "@t3tools/contracts"; import type * as NTBS from "./schemas.ts"; -import { Data, Effect, Scope } from "effect"; +import { Context, Data, Effect, Scope } from "effect"; export type T3Context = { readonly projectId: ProjectId; @@ -27,3 +27,6 @@ export interface NTBSProcessor

{ readonly start: () => Effect.Effect; } + +export const makeNTBSProcessor =

(key: string) => + Context.Service>(key); From b307399d202d994b404c9cac17bbb56fc7661a42 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Fri, 7 Aug 2026 21:57:30 +0200 Subject: [PATCH 20/29] add inbout and outbound processor function --- apps/server/src/ntbs/processor.ts | 42 ++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts index 50ff4ad11ff..a41bcc6d372 100644 --- a/apps/server/src/ntbs/processor.ts +++ b/apps/server/src/ntbs/processor.ts @@ -1,6 +1,31 @@ -import type { OrchestrationEvent, ProjectId } from "@t3tools/contracts"; +import { type OrchestrationEvent, type ProjectId } from "@t3tools/contracts"; import type * as NTBS from "./schemas.ts"; -import { Context, Data, Effect, Scope } from "effect"; +import { Context, Data, Effect } from "effect"; + +/* + 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 thread and worktree, then starts the turn with `snapshot` + - Saves `ThreadStarted` + - Posts to the NTBS platform through the adapter and saves `ThreadStartAcknowledgment` + + - 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

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; @@ -10,7 +35,7 @@ export type T3Context = { export type ProcessorEvent

= | { readonly source: "adapter"; - readonly event: NTBS.LifecycleEvent

; + readonly event: NTBS.RequestAccepted

; readonly t3Context: T3Context; } | { @@ -25,8 +50,17 @@ export class NTBSProcessorError extends Data.TaggedError("NTBSProcessorError")<{ export interface NTBSProcessor

{ readonly process: (event: ProcessorEvent

) => Effect.Effect; - readonly start: () => Effect.Effect; + readonly subscribeToT3Events: () => Effect.Effect; } export const makeNTBSProcessor =

(key: string) => Context.Service>(key); + +declare const processAcceptedRequest:

( + request: NTBS.RequestAccepted

, + t3Context: T3Context, +) => Effect.Effect; + +declare const processT3Event:

( + event: OrchestrationEvent, +) => Effect.Effect; From ccbd7493e90dd2299a3aeb9177e3bd9fa53efa89 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Fri, 7 Aug 2026 22:08:46 +0200 Subject: [PATCH 21/29] feat: add makeProcessor declaration --- apps/server/src/ntbs/processor.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts index a41bcc6d372..e35da4dcc6c 100644 --- a/apps/server/src/ntbs/processor.ts +++ b/apps/server/src/ntbs/processor.ts @@ -1,6 +1,7 @@ import { type OrchestrationEvent, type ProjectId } from "@t3tools/contracts"; import type * as NTBS from "./schemas.ts"; import { Context, Data, Effect } from "effect"; +import type { NTBSAdapter } from "./adapter.ts"; /* NTBS architectural description: @@ -50,7 +51,7 @@ export class NTBSProcessorError extends Data.TaggedError("NTBSProcessorError")<{ export interface NTBSProcessor

{ readonly process: (event: ProcessorEvent

) => Effect.Effect; - readonly subscribeToT3Events: () => Effect.Effect; + readonly subscribeToT3Events: Effect.Effect; } export const makeNTBSProcessor =

(key: string) => @@ -64,3 +65,7 @@ declare const processAcceptedRequest:

( declare const processT3Event:

( event: OrchestrationEvent, ) => Effect.Effect; + +declare const makeProcessor:

( + adapter: NTBSAdapter

, +) => Effect.Effect, never, NTBSProcessorRequirements>; From db363ec7bedcdc2a60470f05ff473b84dfd0fd2a Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Fri, 7 Aug 2026 23:23:10 +0200 Subject: [PATCH 22/29] feat: more ntbs processor work --- apps/server/src/ntbs/processor.ts | 37 +++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts index e35da4dcc6c..2bcf8676b83 100644 --- a/apps/server/src/ntbs/processor.ts +++ b/apps/server/src/ntbs/processor.ts @@ -1,7 +1,11 @@ -import { type OrchestrationEvent, type ProjectId } from "@t3tools/contracts"; +import { ThreadId, type OrchestrationEvent, type ProjectId } from "@t3tools/contracts"; import type * as NTBS from "./schemas.ts"; -import { Context, Data, Effect } from "effect"; +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: @@ -57,6 +61,30 @@ export interface NTBSProcessor

{ export const makeNTBSProcessor =

(key: string) => Context.Service>(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; + declare const processAcceptedRequest:

( request: NTBS.RequestAccepted

, t3Context: T3Context, @@ -66,6 +94,11 @@ declare const processT3Event:

( event: OrchestrationEvent, ) => Effect.Effect; +declare const startT3thread: ( + snapshot: string, + t3Context: T3Context, +) => Effect.Effect; + declare const makeProcessor:

( adapter: NTBSAdapter

, ) => Effect.Effect, never, NTBSProcessorRequirements>; From 49c91aa642b63513be18b233ff30007e4c40b6ed Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Sat, 8 Aug 2026 00:29:44 +0200 Subject: [PATCH 23/29] feat: document ntbs processor declarations --- apps/server/src/ntbs/processor.ts | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts index 2bcf8676b83..e7db068dda0 100644 --- a/apps/server/src/ntbs/processor.ts +++ b/apps/server/src/ntbs/processor.ts @@ -85,20 +85,66 @@ type NTBSProcessorRequirements = */ | Crypto.Crypto; +/** + * Creates a new T3 thread for an external request. + * + * Does nothing if the adapter has already handled the request. + * Otherwise starts the thread, records it, posts an acknowledgement, and records that message. + */ declare const processAcceptedRequest:

( request: NTBS.RequestAccepted

, t3Context: T3Context, ) => Effect.Effect; +/** + * Provider runtimes (like Claude Code) emit `turn.completed` but + * T3 consumes those internally and represents the result externally emitting only a `thread.session-set` event. + * This works for non-NTBS surfaces as they are notified to simply + * rerender the latest projection. + * + * But it does not work for NTBS ones that do not consume projections. + * + * Thus, we need to listen for re-emitted `thread.session-set` events and manually check the state of the thread. + * + * We read the project thread identified by the session event. + * - return `null` if the thread isn't done. + * - return final assistant text or plain error text otherwise. + * Reads the projected thread identified by the session event. + */ +declare const resolveT3Outcome: ( + event: Extract, +) => Effect.Effect< + { readonly threadId: ThreadId; readonly text: string } | null, + NTBSProcessorError +>; + +/** + * Handles T3 events that may indicate that a turn has ended. + * + * Ignores other events, threads with no adapter record, and responses that have been already posted. + * + * When a turn has ended, reads its result, posts it through the adapter and updates the lifecycle. + */ declare const processT3Event:

( event: OrchestrationEvent, ) => Effect.Effect; +/** + * Creates an isolated worktree and a new T3 thread from the source snapshot. + * + * Starts the first turn and returns the new thread ID. + * Does not read platform data or call the adapter. + */ declare const startT3thread: ( snapshot: string, t3Context: T3Context, ) => Effect.Effect; +/** + * Creates an NTBS processor for one adapter. + * + * Resolves the required T3 services and returns processor operations with no remaining requirements. + */ declare const makeProcessor:

( adapter: NTBSAdapter

, ) => Effect.Effect, never, NTBSProcessorRequirements>; From f7b70d7aa1c3d892c1c988205cb29ec60322d65c Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Sat, 8 Aug 2026 01:12:25 +0200 Subject: [PATCH 24/29] fix: ntbs processor and schemas flows --- apps/server/src/ntbs/processor.ts | 38 +++++++++++++++++++------------ apps/server/src/ntbs/schemas.ts | 2 +- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts index e7db068dda0..cfd038fc699 100644 --- a/apps/server/src/ntbs/processor.ts +++ b/apps/server/src/ntbs/processor.ts @@ -86,10 +86,31 @@ type NTBSProcessorRequirements = | Crypto.Crypto; /** - * Creates a new T3 thread for an external request. + * Creates an isolated worktree and a new T3 thread. * - * Does nothing if the adapter has already handled the request. - * Otherwise starts the thread, records it, posts an acknowledgement, and records that message. + * Does not start a turn, read platform data or call the adapter. + */ +declare const createT3Thread: ( + snapshot: string, + t3Context: T3Context, +) => Effect.Effect; + +/** + * Stars the first turn in an existing T3 thread. + */ +declare const startT3Turn: ( + threadId: ThreadId, + snapshot: string, +) => Effect.Effect; + +/** + * 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:

( request: NTBS.RequestAccepted

, @@ -129,17 +150,6 @@ declare const processT3Event:

( event: OrchestrationEvent, ) => Effect.Effect; -/** - * Creates an isolated worktree and a new T3 thread from the source snapshot. - * - * Starts the first turn and returns the new thread ID. - * Does not read platform data or call the adapter. - */ -declare const startT3thread: ( - snapshot: string, - t3Context: T3Context, -) => Effect.Effect; - /** * Creates an NTBS processor for one adapter. * diff --git a/apps/server/src/ntbs/schemas.ts b/apps/server/src/ntbs/schemas.ts index c409874d464..0d0533fde03 100644 --- a/apps/server/src/ntbs/schemas.ts +++ b/apps/server/src/ntbs/schemas.ts @@ -36,7 +36,7 @@ export type RequestAccepted

= LifecycleEvent

& { }; export type ThreadStarted

= ThreadEvent

& { - /** T3 has created the new thread from the source snapshot */ + /** T3 has created the new thread. */ state: "thread.started"; }; From cd68ff9e528942a95020608cc9d4b65ba1a64958 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Sat, 8 Aug 2026 01:54:57 +0200 Subject: [PATCH 25/29] fix: docs in ntbs processor --- apps/server/src/ntbs/processor.ts | 56 ++++++++++++++++++------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts index cfd038fc699..7b6381dd148 100644 --- a/apps/server/src/ntbs/processor.ts +++ b/apps/server/src/ntbs/processor.ts @@ -12,9 +12,10 @@ import type { ProjectSetupScriptRunner } from "../project/ProjectSetupScriptRunn 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 thread and worktree, then starts the turn with `snapshot` + - Uses private T3-specific effect to create a fresh worktree and T3 thread - Saves `ThreadStarted` - - Posts to the NTBS platform through the adapter and saves `ThreadStartAcknowledgment` + - 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 @@ -53,12 +54,21 @@ export class NTBSProcessorError extends Data.TaggedError("NTBSProcessorError")<{ }> {} export interface NTBSProcessor

{ + /** + * Routes adapter requests and T3 events through the shared NTBS workflow. + */ readonly process: (event: ProcessorEvent

) => Effect.Effect; + /** + * 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; } -export const makeNTBSProcessor =

(key: string) => +export const makeNTBSProcessorTag =

(key: string) => Context.Service>(key); type NTBSProcessorRequirements = @@ -89,14 +99,13 @@ type NTBSProcessorRequirements = * 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: ( - snapshot: string, - t3Context: T3Context, -) => Effect.Effect; +declare const createT3Thread: (t3Context: T3Context) => Effect.Effect; /** - * Stars the first turn in an existing T3 thread. + * Starts the first turn in an existing T3 thread. */ declare const startT3Turn: ( threadId: ThreadId, @@ -118,19 +127,15 @@ declare const processAcceptedRequest:

( ) => Effect.Effect; /** - * Provider runtimes (like Claude Code) emit `turn.completed` but - * T3 consumes those internally and represents the result externally emitting only a `thread.session-set` event. - * This works for non-NTBS surfaces as they are notified to simply - * rerender the latest projection. - * - * But it does not work for NTBS ones that do not consume projections. + * 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. * - * Thus, we need to listen for re-emitted `thread.session-set` events and manually check the state of the thread. + * 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. * - * We read the project thread identified by the session event. - * - return `null` if the thread isn't done. - * - return final assistant text or plain error text otherwise. - * Reads the projected thread identified by the session event. + * 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, @@ -142,11 +147,14 @@ declare const resolveT3Outcome: ( /** * Handles T3 events that may indicate that a turn has ended. * - * Ignores other events, threads with no adapter record, and responses that have been already posted. - * - * When a turn has ended, reads its result, posts it through the adapter and updates the lifecycle. + * 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:

( +declare const processT3Event: ( event: OrchestrationEvent, ) => Effect.Effect; @@ -155,6 +163,6 @@ declare const processT3Event:

( * * Resolves the required T3 services and returns processor operations with no remaining requirements. */ -declare const makeProcessor:

( +declare const makeNTBSProcessor:

( adapter: NTBSAdapter

, ) => Effect.Effect, never, NTBSProcessorRequirements>; From ce8b7aaeaf90b41db556e6997eab64ad9191cf0a Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Sat, 8 Aug 2026 02:30:00 +0200 Subject: [PATCH 26/29] chore: complete abstract/declaration phase --- apps/server/src/ntbs/adapter.ts | 44 ++++++++++++++++++++++-- apps/server/src/ntbs/platform-handler.ts | 26 ++++++++++++++ apps/server/src/ntbs/processor.ts | 2 +- apps/server/src/ntbs/schemas.ts | 4 ++- 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 apps/server/src/ntbs/platform-handler.ts diff --git a/apps/server/src/ntbs/adapter.ts b/apps/server/src/ntbs/adapter.ts index 393d43f1547..fdcbdf6ae2f 100644 --- a/apps/server/src/ntbs/adapter.ts +++ b/apps/server/src/ntbs/adapter.ts @@ -1,3 +1,4 @@ +import type { ThreadId } from "@t3tools/contracts"; import * as NTBS from "./schemas.ts"; import { Context, Data, Effect } from "effect"; @@ -10,25 +11,64 @@ 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

{ + /** + * 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

, ) => Effect.Effect<"accepted" | "duplicate", AdapterError>; + /** + * Stores a lifecycle state. Does not perform any other business logic. + */ readonly save: (lifecycleEvent: NTBS.NTBSLifecycle

) => Effect.Effect; + /** + * 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

, ) => Effect.Effect; + /** + * 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

, text: string, ) => Effect.Effect; + /** + * 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: string, + threadId: ThreadId, ) => Effect.Effect< Exclude, NTBS.RequestAccepted

>, ThreadNotFound | AdapterError >; } -export const makeNTBSAdapter =

(key: string) => +export const makeNTBSAdapterTag =

(key: string) => Context.Service>(key); diff --git a/apps/server/src/ntbs/platform-handler.ts b/apps/server/src/ntbs/platform-handler.ts new file mode 100644 index 00000000000..8206baf3108 --- /dev/null +++ b/apps/server/src/ntbs/platform-handler.ts @@ -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 { + readonly handle: (input: Input) => Effect.Effect; +} + +/** + * 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 = (key: string) => + Context.Service>(key); diff --git a/apps/server/src/ntbs/processor.ts b/apps/server/src/ntbs/processor.ts index 7b6381dd148..d7e5e746e42 100644 --- a/apps/server/src/ntbs/processor.ts +++ b/apps/server/src/ntbs/processor.ts @@ -163,6 +163,6 @@ declare const processT3Event: ( * * Resolves the required T3 services and returns processor operations with no remaining requirements. */ -declare const makeNTBSProcessor:

( +export declare const makeNTBSProcessor:

( adapter: NTBSAdapter

, ) => Effect.Effect, never, NTBSProcessorRequirements>; diff --git a/apps/server/src/ntbs/schemas.ts b/apps/server/src/ntbs/schemas.ts index 0d0533fde03..3dd3fc4225e 100644 --- a/apps/server/src/ntbs/schemas.ts +++ b/apps/server/src/ntbs/schemas.ts @@ -1,3 +1,5 @@ +import type { ThreadId } from "@t3tools/contracts"; + /** * Describes the platform-specific data of a * Non-Turn-Based-Surface. @@ -27,7 +29,7 @@ export type LifecycleEvent

= { export type ThreadEvent

= LifecycleEvent

& { t3Data: { /** The T3 thread created by the lifecycle event */ - threadId: string; + threadId: ThreadId; }; }; From d991699fe808324f6bfc7c1b103962b08a740227 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Sat, 8 Aug 2026 15:44:57 +0200 Subject: [PATCH 27/29] chore: fixes part 1 --- docs/planning/fixes.md | 11 ++ docs/planning/ntbs-adversarial-review.md | 156 +++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 docs/planning/fixes.md create mode 100644 docs/planning/ntbs-adversarial-review.md diff --git a/docs/planning/fixes.md b/docs/planning/fixes.md new file mode 100644 index 00000000000..7705f6a6ae6 --- /dev/null +++ b/docs/planning/fixes.md @@ -0,0 +1,11 @@ +# NTBS fixes + +This document collects the units of work identified by the adversarial review of the NTBS design. + +## 1. Recover accepted requests whose T3 thread was not recorded as started + +The adapter records `RequestAccepted` before the processor creates the T3 thread. If the server stops after accepting the request but before recording `ThreadStarted`, the request remains unfinished. A repeated delivery cannot safely solve this by starting fresh because it is treated as a duplicate, and the previous attempt may already have created a T3 thread. + +The request must retain a planned T3 thread ID before thread creation begins. Every creation attempt for that request must use the same thread ID, making a retry safe even if the previous attempt created the thread but failed before recording `ThreadStarted`. + +The adapter must expose accepted requests that have no recorded `ThreadStarted`. When the processor starts, it must find those requests and retry thread creation using their stored thread IDs. A duplicate delivery must not create another thread; it may resume the existing unfinished request. diff --git a/docs/planning/ntbs-adversarial-review.md b/docs/planning/ntbs-adversarial-review.md new file mode 100644 index 00000000000..66e2356ead2 --- /dev/null +++ b/docs/planning/ntbs-adversarial-review.md @@ -0,0 +1,156 @@ +# NTBS skeleton adversarial review + +**Status:** review of the declaration-phase skeleton in `apps/server/src/ntbs` + +**Scope:** `schemas.ts`, `processor.ts`, `platform-handler.ts`, `adapter.ts`, reviewed against [ntbs.md](./ntbs.md), [ntbs-architecture.md](./ntbs-architecture.md), [ntbs-event-processing.md](./ntbs-event-processing.md), [ntbs-plan.md](./ntbs-plan.md), and the existing implementation (`apps/server/src/jira`, `apps/server/src/github`, `apps/server/src/ws.ts`, `apps/server/src/orchestration`, `packages/contracts`). + +## Verdict + +The core seam is right — a shared lifecycle processor with per-platform adapters is exactly what `JiraIssueBridge` and `GitHubPrBridge` already share informally (they import each other's outcome-resolution helpers), and thread-per-event kills the hairiest logic in the current bridges (thread reuse, turn targeting against a shared thread). But the skeleton as declared has **two liveness holes that make it unimplementable as specified**, quietly **regresses five capabilities the current bridges already have**, **re-declares a mechanic the codebase already ships** (turn-start bootstrap), and carries at least three abstractions that can be deleted. + +Findings are ordered by severity within each section and numbered globally for reference. + +## A. Contract holes — these produce stuck/wrong external state if implemented as declared + +### 1. A crash after `accept` loses the event forever, by construction + +`adapter.accept` persists `RequestAccepted` _before any T3 work_ and returns `"duplicate"` on redelivery (`adapter.ts:22-33`). But: + +- `findByThreadId` structurally excludes `RequestAccepted` (`adapter.ts:65-70`) — no thread exists yet, so the record is unreachable; +- the processor interface has only `process` and `subscribeToT3Events` (`processor.ts:56-69`) — no recovery entry point; +- Jira/GitHub webhooks cannot save you, because `jira/http.ts` responds 202 before processing and fork-detaches, so platforms do not redeliver. + +Crash between `accept` and thread creation → idempotency key consumed, event never processed, no path ever revisits it. The current Jira bridge solves exactly this with a startup `restore` sweep over `status: "processing"` deliveries (`JiraIssueBridge.ts:867-875`). The plan doc's own step 7 ("persist every lifecycle transition so interrupted processing can resume") is unsatisfiable with this interface. + +**Fix:** add `listIncomplete` (or similar) to the adapter contract plus a processor startup-recovery pass, and consider `accept` returning the existing lifecycle state instead of a bare `"duplicate"` so redeliveries can resume half-done work. + +### 2. The state machine has a typed dead-end when acknowledgement posting fails + +`ResponseAvailable` and `ResponsePosted` both _require_ `acknowledgementMessageId` (`schemas.ts:51-62`). If `postAcknowledgement` fails permanently — or the process dies between saving `ThreadStarted` and posting the ack — the turn still runs and completes, the outcome event arrives, `findByThreadId` returns `ThreadStarted`… and step 5 of `processT3Event` ("Record `ResponseAvailable`", `processor.ts:150-159`) is unconstructible. The answer exists and can never be posted. + +The architecture doc defers "error and retry lifecycle states" to a TODO, but this is not an error state — it is the happy path after one failed platform call. + +**Fix:** either make `acknowledgementMessageId` optional in the response states, or model ack-retry explicitly. Note the constraint is real for Discord (the outcome must reply to the ack, per ntbs-event-processing.md §Discord), so "post outcome without ack" needs a per-platform answer, which argues for the adapter receiving the whole record and deciding. + +### 3. Outcome detection is anchored on "latest turn", which is wrong the moment anyone touches the thread + +`t3Data` keeps only `threadId` (`schemas.ts:29-34`) and `resolveT3Outcome` reads "the latest turn" from the projection (`processor.ts:140-145`). NTBS threads are ordinary threads — visible in native clients, no lock. If a human opens one and sends a follow-up (or a queued message drains), `latestTurn` now describes _their_ turn: the processor will either post nothing or post the second turn's answer to the Jira comment. + +This regresses against both the NTBS docs (ntbs-architecture.md's record keeps `threadId`/`userMessageId`/`turnId`; ntbs-plan.md explicitly says "the lifecycle types must represent that sequence accurately") and the current implementation (`JiraDeliveryStore` keeps `userMessageId`, `previousTurnId`, `targetTurnId` and the bridges do targeted turn discovery). + +`userMessageId` is free — the processor generates it at dispatch. `turnId` genuinely arrives later (the provider adapter mints it; the decider emits `thread.turn-start-requested` with `turnId: null`). + +**Fix:** store `userMessageId` at `ThreadStarted`, resolve the outcome for the turn that answered _that message_, and record the discovered `turnId` when it appears. + +### 4. No timeout anywhere — a silently hung provider leaves an ack dangling forever + +Orchestration has no turn timeout (only 5s/45s provider-_control_ timeouts in `ProviderCommandReactor`), and if an ACP connection drops silently there is no `thread.session-set` until a server restart triggers `OrphanSessionRecovery`. The current Jira bridge covers this with a configurable 30-minute poll deadline plus a "still working" fallback message (`JiraIssueBridge.ts:504-559`). The skeleton has no deadline concept, yet ntbs.md promises "failure, timeout, or cancellation returns a response that explicitly reports the outcome." + +**Fix:** the processor (platform-independent) owns a per-record deadline; on expiry it posts a timeout outcome and marks the record posted — and document that a late real answer is then dropped, or define a supersede rule. + +### 5. The event subscription cannot survive a restart, and nothing compensates + +`subscribeToT3Events` necessarily rides `OrchestrationEngine.streamDomainEvents`, which is an in-memory PubSub — "hot runtime stream (new events only)" (`orchestration/Services/OrchestrationEngine.ts:52-57`). Any turn that completes while the server (or just the processor fiber) is down emits into the void. `readEvents(fromSequenceExclusive)` exists, but the skeleton stores no cursor anywhere, and the engine's own docstring warns against stale-cursor replay. + +**No cursor is needed:** since `resolveT3Outcome` already treats the projection as the source of truth, the startup-recovery pass from finding 1 — re-check the projection for every incomplete record — also closes this hole. Treat the live stream purely as a wake-up signal. + +This is also the honest framing of a half-made design choice: the current bridges _poll_ per-delivery, which gets recovery and timeouts almost for free at the cost of 1s ticks. Event-wakeup + projection-truth + startup sweep is a fine steady state, but say so explicitly — the mechanism is currently smeared across `subscribeToT3Events`' docstring. + +### 6. Double-posting is possible and undocumented + +Crash between a successful `postResponse` and `save(ResponsePosted)` → recovery re-posts. Platforms have no idempotent comment-create, so this is inherent at-least-once delivery. The current bridge has the same window; that is acceptable — but the adapter contract should state which semantics adapters must expect, because it changes what `save` failures mean. + +## B. The codebase already has things the skeleton re-declares or ignores + +### 7. The turn-start bootstrap already exists as a single command — do not rebuild it from Git primitives + +`ThreadTurnStartCommand.bootstrap` covers createThread + prepareWorktree + runSetupScript + first message + turn start (`packages/contracts/src/orchestration.ts:744-803`). Its server-side executor is currently inlined in `ws.ts:816-1168` (`dispatchBootstrapTurnStart`), including subtleties the skeleton would otherwise re-learn the hard way: `thread.meta.update` after worktree creation, polling the projection until the worktree is _visible_ before provider start (`ws.ts:953`), setup-script activity records, `startFromOrigin` fetch/resolve. + +The skeleton instead declares its own `createT3Thread`/`startT3Turn` split depending directly on `GitWorkflowService` + `ProjectSetupScriptRunner` + `Crypto` (`processor.ts:74-113`) — i.e., a third copy of the mechanic beside `ws.ts` and `JiraIssueBridge.createThreadForIssue` (which the plan itself calls "a reference, not the desired architecture"). The plan's step 2 — extract the ws.ts mechanic into a service both native clients and NTBS call — is the right move and the skeleton silently dropped it. + +**Fix:** do the extraction; the processor's dependency list shrinks to OrchestrationEngine + ProjectionSnapshotQuery + the extracted service. One nuance to encode there: ws.ts _continues_ on worktree-prep failure (`ws.ts:879`), but NTBS must _fail_ the event instead — isolation is an agreed hard requirement (ntbs.md §concurrency). The shared service needs a strictness knob. + +### 8. Provenance is first-class in T3 and the skeleton cannot carry it + +`SourceChannel` already enumerates `discord`/`github`/`jira`/`slack`/`teams` (`packages/contracts/src/identity.ts:60-73`), `SourceRef`/`SourceLocation` carry issueKey/owner/repo/number/guildId (`identity.ts:75-116`), `ThreadTurnStartCommand` has `source`/`sourceHint` seats (`orchestration.ts:796-801`), threads have `originSource` (`orchestration.ts:443`), and the current bridges stamp it via `buildIntegrationSourceRef` (`identity/stampSource.ts:175`). + +`T3Context = { projectId, revision }` (`processor.ts:36-39`) has no seat for any of this, so NTBS-created threads would lose origin badges, participant attribution, and identity-map resolution that native clients already render — a visible regression vs. today's Jira bridge. It also falsifies the architecture doc's "T3 does not receive or interpret platform data" absolutism: T3 already _stores and renders_ platform provenance; what it does not do is interpret it for routing. + +**Fix:** add `source`/`sourceHint` to `T3Context` (or the processor's dispatch), and reword the doc's opacity principle to "T3 never interprets platform data for lifecycle/routing decisions." + +### 9. `T3Context` is missing everything else a thread needs, with no stated defaulting policy + +Thread creation requires `title`, `modelSelection`, `runtimeMode`, `interactionMode` (`orchestration.ts:627-641`). The current bridge resolves model as `project.defaultModelSelection ?? getAutoBootstrapDefaultModelSelection()`, title from the issue summary, runtimeMode `"full-access"` (`JiraIssueBridge.ts:396-399`). Either `T3Context` grows optional overrides (platforms will eventually want them — e.g. a Jira label selecting a model) or the processor owns the defaulting policy; right now neither is written down. Same for `revision` — is it a branch name or pinned SHA, resolved at accept-time or worktree-time? The current bridge resolves `origin/` at worktree time. + +### 10. `snapshot: string` will hit the 120k input cap and silently forecloses attachments + +`PROVIDER_SEND_TURN_MAX_INPUT_CHARS = 120_000` (`orchestration.ts:145`) — a captured Jira issue + comment history or a PR diff snapshot can exceed it, and then the dispatch fails and the lifecycle wedges (see finding 1). The processor must own a truncation policy. + +Separately, turn messages support image attachments (`ChatAttachment`), and Jira/GitHub/Discord invocations routinely include screenshots; `snapshot: string` forecloses them. Punting is fine — write the exclusion into the lifecycle module so it is a decision, not an accident. + +### 11. Actor trust has no home + +`classifyJiraActorTrust`/`githubActorTrust` gate who may trigger work, with a "context-only" fallback that _posts a note instead of starting work_. The platform-handler docstring ("determines whether the input should start work", `platform-handler.ts:8-14`) implicitly absorbs trust but never names it, and the context-only path — an outbound platform post with no lifecycle — fits neither handler nor adapter contract as written. It is fine to keep it platform-side and outside the lifecycle; say so explicitly, because it is a security boundary. + +## C. Simplifications — things to delete or merge + +### 12. `platform-handler.ts` is a vacuous abstraction — delete it + +`NTBSPlatformHandler` is `handle: Input => Effect` (`platform-handler.ts:15-17`). Nothing consumes it polymorphically and nothing ever will — each platform's HTTP route calls its own handler with its own `Input` type; a generic interface over an existential `Input` has no call sites by construction. It is a function type with a ceremony tag factory. The real architecture is two-piece (processor + adapter); the "handler" is just each adapter package's inbound edge, and a comment in `processor.ts` already documents that convention adequately. + +### 13. Collapse `ProcessorEvent` — the union wraps two statically-known callers + +`{ source: "adapter" } | { source: "t3" }` (`processor.ts:41-50`) has exactly one producer per arm, and the `"t3"` arm's only legitimate producer is the processor's _own_ subscription. Exposing it invites outsiders to inject synthetic T3 events, and `"adapter"` is a misnomer anyway (the platform handler builds it, not the adapter). Replace with `handleRequest(request, t3Context)` plus the internal subscription; tests can fake the engine's stream through the service dependency instead of injecting events through the front door. + +Also reconsider exposing `subscribeToT3Events` at all — if callers must wire it, name it for what it is (`run`/`daemon`); its current docstring leaks a private function name (`processT3Event`). + +### 14. `RequestAccepted` lies about its own state + +The handler constructs `state: "request.accepted"` _before_ the adapter has accepted anything, then `processAcceptedRequest` step 1 "asks the adapter to accept it" (`processor.ts:124-127`) — a value asserting a persisted state that does not exist yet, named "accepted" while acceptance is pending. Pass the base `{ platformData, snapshot }` into `accept` and let the adapter mint the accepted state. This fixes the semantics and removes a footgun for adapter authors. + +### 15. Justify each of the five states with a distinct recovery action, or cut to three + +`ResponseAvailable` as a _persisted_ state buys nothing today: recovery must re-derive the outcome from the projection anyway (finding 5), and dedup only needs "posted". The current stores run on effectively three states (`received`/`processing`/`completed`) plus nullable message IDs. Keep five states only if each maps to a distinct crash-recovery behavior — writing that table down (state → recovery action) is the cheapest way to force findings 1/2/5 to resolution. If a state has no recovery action of its own, it is a log line, not a state. + +### 16. Drop the tag factories until something resolves them from context + +Adapter → processor → handler is a straight constructor chain assembled once at bootstrap; only the HTTP route plausibly needs a context tag. Three string-keyed generic tag factories (`makeNTBS*Tag(key)`) also deviate from the codebase convention (`class X extends Context.Service()("t3/…")` with namespaced IDs) and nothing type-checks that two call sites will not collide on a bare key like `"jira"`. If kept, namespace the keys. (`Context.Service(key)` is a legitimate effect-smol form, so the factories are _sound_, just probably unnecessary.) + +### 17. Design the error taxonomy around retryability, not strings + +`AdapterError { reason: string }` / `NTBSProcessorError { reason: string }` erase the one distinction the whole outbox pattern turns on: transient (429, network) vs. permanent (403, deleted comment). The current `JiraAppClient` already encodes retry-vs-fallback decisions (400/404 → try next parent). "Will be refined later" is noted in `adapter.ts` — but retryability is the _first_ refinement, and it changes method signatures, so decide it before adapters exist. + +### 18. Do not collapse the outcome to `text` before the adapter sees it + +`resolveT3Outcome` returns bare text and `postResponse(event, text)` posts it (`processor.ts:140-145`, `adapter.ts:55-58`). The docs distinguish answer/failure/timeout/cancellation and adapters own platform rendering — yet the one place rendering matters (a failure vs. an answer) arrives pre-flattened, while ack copy is fully adapter-owned. Asymmetric. Pass a small tagged outcome (`{ kind: "answer" | "failure" | "interrupted" | "timeout"; text }`) and let the adapter format. + +### 19. Naming/file nits, worth fixing while it is cheap + +- `schemas.ts` contains no `effect/Schema` schemas — misleading in a repo where "schema" means that specifically (`packages/contracts` is "schema-only"); call it `lifecycle.ts`. +- Three different things are called "event" in one file (platform events, T3 events, and these persisted _states_ with event-shaped names like `"thread.started"`); the base type `LifecycleEvent` is a state/record, and its docstring still says "NTBSEvent" (`schemas.ts:18`) while the docs say `NtbsEvent` and the state names diverge from the doc's (`acknowledgementPosted` vs `thread.started.acknowledged`). +- Typos: "response tex" (`processor.ts:83`), "idenitifier" (`adapter.ts:52`). +- Casing: `NTBSAdapter` vs the docs' `Ntbs`. + +## D. Decisions to make now (cheap in planning, expensive later) + +- **Post-response thread policy.** Thread-per-event with no terminal action means worktrees and inbox noise accumulate unboundedly — `WorktreeLifecycle` only cleans on archive, and nobody archives NTBS threads. The docs acknowledge the noise but propose nothing. Decide: auto-settle (or archive) after `ResponsePosted`, keeping worktree-retention rules in one place. +- **Concurrency caps.** A chatty Jira issue or Discord thread can fork-bomb worktrees + provider sessions. The cap/queue/reject policy is platform-independent and belongs in the processor; the current bridge only bounds _recovery_ concurrency (4). +- **In-process vs. remote adapters.** The skeleton is in-process Effect services; Jira/GitHub webhooks fit, but today's Discord integration is an external bot speaking WS with `sourceHint` (`identity/stampSource.ts:2-4`). ntbs.md's own framing ("adapters should be able to obtain an initial state and then receive subsequent changes") describes a _protocol_, not an in-process interface. Building in-process first is fine — but state that the Discord port means either moving the bot in-server or exposing the processor over a transport, so nobody bakes in-process assumptions into the lifecycle store. +- **Ack-before-turn ordering.** The skeleton posts the ack before starting the turn (`processor.ts:117-123`); the plan doc ordered turn-start first. Ack-first is currently _forced_ by finding 2's type constraint and costs a platform round-trip of agent latency on every event. If `acknowledgementMessageId` becomes optional in response states, the ordering becomes free — choose it deliberately rather than inheriting it from the type shape. +- **Snapshot retention.** Adapters persist external user content (snapshots) indefinitely; platforms let users delete messages. The docs punt to adapters — fine, but record it as a known compliance question, and note the current store's ~2000-record cap as prior art. + +## What is right (keep it) + +- Thread-per-event genuinely deletes the worst code in the current bridges (`resolveLinkedThreadId`, ambiguous-link handling, target-turn discovery against shared threads). +- The adapter surface (`accept`/`save`/`find`/`post*`) is small, in-memory-fakeable, and matches the plan's testing strategy. +- Keeping platform data opaque-generic (`PlatformData`) while the processor owns sequencing is the correct division — every platform-independent behavior identified in the Jira bridge analysis fits it once findings 1–5 are fixed. + +## Summary + +The skeleton is a good shape wrapped around an incomplete failure model: + +1. Fix the recovery story (findings 1, 5), the ack dead-end (2), turn anchoring (3), and timeouts (4) in the contract now. +2. Reuse the bootstrap command and provenance plumbing instead of re-declaring them (7, 8). +3. Delete the platform-handler layer (12). + +Update [ntbs-architecture.md](./ntbs-architecture.md) alongside — several findings (3, 8) are places where the skeleton diverged from decisions the docs already got right. From afdbb5ecb4fa8ecff7d64fb84d384398c8c906b4 Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Sat, 8 Aug 2026 17:07:36 +0200 Subject: [PATCH 28/29] feat: review removal of the requestaccepted lifecycle event --- docs/planning/fixes.md | 16 +++++-- docs/planning/ideas.md | 21 +++++++++ docs/planning/ntbs-adversarial-review.md | 56 +++++++----------------- 3 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 docs/planning/ideas.md diff --git a/docs/planning/fixes.md b/docs/planning/fixes.md index 7705f6a6ae6..2e70003758d 100644 --- a/docs/planning/fixes.md +++ b/docs/planning/fixes.md @@ -2,10 +2,18 @@ This document collects the units of work identified by the adversarial review of the NTBS design. -## 1. Recover accepted requests whose T3 thread was not recorded as started +## 1. Remove the pre-thread lifecycle state -The adapter records `RequestAccepted` before the processor creates the T3 thread. If the server stops after accepting the request but before recording `ThreadStarted`, the request remains unfinished. A repeated delivery cannot safely solve this by starting fresh because it is treated as a duplicate, and the previous attempt may already have created a T3 thread. +`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. -The request must retain a planned T3 thread ID before thread creation begins. Every creation attempt for that request must use the same thread ID, making a retry safe even if the previous attempt created the thread but failed before recording `ThreadStarted`. +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. -The adapter must expose accepted requests that have no recorded `ThreadStarted`. When the processor starts, it must find those requests and retry thread creation using their stored thread IDs. A duplicate delivery must not create another thread; it may resume the existing unfinished request. +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. diff --git a/docs/planning/ideas.md b/docs/planning/ideas.md new file mode 100644 index 00000000000..4edd50d3f44 --- /dev/null +++ b/docs/planning/ideas.md @@ -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. diff --git a/docs/planning/ntbs-adversarial-review.md b/docs/planning/ntbs-adversarial-review.md index 66e2356ead2..78e6f5127d5 100644 --- a/docs/planning/ntbs-adversarial-review.md +++ b/docs/planning/ntbs-adversarial-review.md @@ -12,27 +12,7 @@ Findings are ordered by severity within each section and numbered globally for r ## A. Contract holes — these produce stuck/wrong external state if implemented as declared -### 1. A crash after `accept` loses the event forever, by construction - -`adapter.accept` persists `RequestAccepted` _before any T3 work_ and returns `"duplicate"` on redelivery (`adapter.ts:22-33`). But: - -- `findByThreadId` structurally excludes `RequestAccepted` (`adapter.ts:65-70`) — no thread exists yet, so the record is unreachable; -- the processor interface has only `process` and `subscribeToT3Events` (`processor.ts:56-69`) — no recovery entry point; -- Jira/GitHub webhooks cannot save you, because `jira/http.ts` responds 202 before processing and fork-detaches, so platforms do not redeliver. - -Crash between `accept` and thread creation → idempotency key consumed, event never processed, no path ever revisits it. The current Jira bridge solves exactly this with a startup `restore` sweep over `status: "processing"` deliveries (`JiraIssueBridge.ts:867-875`). The plan doc's own step 7 ("persist every lifecycle transition so interrupted processing can resume") is unsatisfiable with this interface. - -**Fix:** add `listIncomplete` (or similar) to the adapter contract plus a processor startup-recovery pass, and consider `accept` returning the existing lifecycle state instead of a bare `"duplicate"` so redeliveries can resume half-done work. - -### 2. The state machine has a typed dead-end when acknowledgement posting fails - -`ResponseAvailable` and `ResponsePosted` both _require_ `acknowledgementMessageId` (`schemas.ts:51-62`). If `postAcknowledgement` fails permanently — or the process dies between saving `ThreadStarted` and posting the ack — the turn still runs and completes, the outcome event arrives, `findByThreadId` returns `ThreadStarted`… and step 5 of `processT3Event` ("Record `ResponseAvailable`", `processor.ts:150-159`) is unconstructible. The answer exists and can never be posted. - -The architecture doc defers "error and retry lifecycle states" to a TODO, but this is not an error state — it is the happy path after one failed platform call. - -**Fix:** either make `acknowledgementMessageId` optional in the response states, or model ack-retry explicitly. Note the constraint is real for Discord (the outcome must reply to the ack, per ntbs-event-processing.md §Discord), so "post outcome without ack" needs a per-platform answer, which argues for the adapter receiving the whole record and deciding. - -### 3. Outcome detection is anchored on "latest turn", which is wrong the moment anyone touches the thread +### 1. Outcome detection is anchored on "latest turn", which is wrong the moment anyone touches the thread `t3Data` keeps only `threadId` (`schemas.ts:29-34`) and `resolveT3Outcome` reads "the latest turn" from the projection (`processor.ts:140-145`). NTBS threads are ordinary threads — visible in native clients, no lock. If a human opens one and sends a follow-up (or a queued message drains), `latestTurn` now describes _their_ turn: the processor will either post nothing or post the second turn's answer to the Jira comment. @@ -42,13 +22,13 @@ This regresses against both the NTBS docs (ntbs-architecture.md's record keeps ` **Fix:** store `userMessageId` at `ThreadStarted`, resolve the outcome for the turn that answered _that message_, and record the discovered `turnId` when it appears. -### 4. No timeout anywhere — a silently hung provider leaves an ack dangling forever +### 2. No timeout anywhere — a silently hung provider leaves an ack dangling forever Orchestration has no turn timeout (only 5s/45s provider-_control_ timeouts in `ProviderCommandReactor`), and if an ACP connection drops silently there is no `thread.session-set` until a server restart triggers `OrphanSessionRecovery`. The current Jira bridge covers this with a configurable 30-minute poll deadline plus a "still working" fallback message (`JiraIssueBridge.ts:504-559`). The skeleton has no deadline concept, yet ntbs.md promises "failure, timeout, or cancellation returns a response that explicitly reports the outcome." **Fix:** the processor (platform-independent) owns a per-record deadline; on expiry it posts a timeout outcome and marks the record posted — and document that a late real answer is then dropped, or define a supersede rule. -### 5. The event subscription cannot survive a restart, and nothing compensates +### 3. The event subscription cannot survive a restart, and nothing compensates `subscribeToT3Events` necessarily rides `OrchestrationEngine.streamDomainEvents`, which is an in-memory PubSub — "hot runtime stream (new events only)" (`orchestration/Services/OrchestrationEngine.ts:52-57`). Any turn that completes while the server (or just the processor fiber) is down emits into the void. `readEvents(fromSequenceExclusive)` exists, but the skeleton stores no cursor anywhere, and the engine's own docstring warns against stale-cursor replay. @@ -56,13 +36,13 @@ Orchestration has no turn timeout (only 5s/45s provider-_control_ timeouts in `P This is also the honest framing of a half-made design choice: the current bridges _poll_ per-delivery, which gets recovery and timeouts almost for free at the cost of 1s ticks. Event-wakeup + projection-truth + startup sweep is a fine steady state, but say so explicitly — the mechanism is currently smeared across `subscribeToT3Events`' docstring. -### 6. Double-posting is possible and undocumented +### 4. Double-posting is possible and undocumented Crash between a successful `postResponse` and `save(ResponsePosted)` → recovery re-posts. Platforms have no idempotent comment-create, so this is inherent at-least-once delivery. The current bridge has the same window; that is acceptable — but the adapter contract should state which semantics adapters must expect, because it changes what `save` failures mean. ## B. The codebase already has things the skeleton re-declares or ignores -### 7. The turn-start bootstrap already exists as a single command — do not rebuild it from Git primitives +### 5. The turn-start bootstrap already exists as a single command — do not rebuild it from Git primitives `ThreadTurnStartCommand.bootstrap` covers createThread + prepareWorktree + runSetupScript + first message + turn start (`packages/contracts/src/orchestration.ts:744-803`). Its server-side executor is currently inlined in `ws.ts:816-1168` (`dispatchBootstrapTurnStart`), including subtleties the skeleton would otherwise re-learn the hard way: `thread.meta.update` after worktree creation, polling the projection until the worktree is _visible_ before provider start (`ws.ts:953`), setup-script activity records, `startFromOrigin` fetch/resolve. @@ -70,7 +50,7 @@ The skeleton instead declares its own `createT3Thread`/`startT3Turn` split depen **Fix:** do the extraction; the processor's dependency list shrinks to OrchestrationEngine + ProjectionSnapshotQuery + the extracted service. One nuance to encode there: ws.ts _continues_ on worktree-prep failure (`ws.ts:879`), but NTBS must _fail_ the event instead — isolation is an agreed hard requirement (ntbs.md §concurrency). The shared service needs a strictness knob. -### 8. Provenance is first-class in T3 and the skeleton cannot carry it +### 6. Provenance is first-class in T3 and the skeleton cannot carry it `SourceChannel` already enumerates `discord`/`github`/`jira`/`slack`/`teams` (`packages/contracts/src/identity.ts:60-73`), `SourceRef`/`SourceLocation` carry issueKey/owner/repo/number/guildId (`identity.ts:75-116`), `ThreadTurnStartCommand` has `source`/`sourceHint` seats (`orchestration.ts:796-801`), threads have `originSource` (`orchestration.ts:443`), and the current bridges stamp it via `buildIntegrationSourceRef` (`identity/stampSource.ts:175`). @@ -78,53 +58,49 @@ The skeleton instead declares its own `createT3Thread`/`startT3Turn` split depen **Fix:** add `source`/`sourceHint` to `T3Context` (or the processor's dispatch), and reword the doc's opacity principle to "T3 never interprets platform data for lifecycle/routing decisions." -### 9. `T3Context` is missing everything else a thread needs, with no stated defaulting policy +### 7. `T3Context` is missing everything else a thread needs, with no stated defaulting policy Thread creation requires `title`, `modelSelection`, `runtimeMode`, `interactionMode` (`orchestration.ts:627-641`). The current bridge resolves model as `project.defaultModelSelection ?? getAutoBootstrapDefaultModelSelection()`, title from the issue summary, runtimeMode `"full-access"` (`JiraIssueBridge.ts:396-399`). Either `T3Context` grows optional overrides (platforms will eventually want them — e.g. a Jira label selecting a model) or the processor owns the defaulting policy; right now neither is written down. Same for `revision` — is it a branch name or pinned SHA, resolved at accept-time or worktree-time? The current bridge resolves `origin/` at worktree time. -### 10. `snapshot: string` will hit the 120k input cap and silently forecloses attachments +### 8. `snapshot: string` will hit the 120k input cap and silently forecloses attachments `PROVIDER_SEND_TURN_MAX_INPUT_CHARS = 120_000` (`orchestration.ts:145`) — a captured Jira issue + comment history or a PR diff snapshot can exceed it, and then the dispatch fails and the lifecycle wedges (see finding 1). The processor must own a truncation policy. Separately, turn messages support image attachments (`ChatAttachment`), and Jira/GitHub/Discord invocations routinely include screenshots; `snapshot: string` forecloses them. Punting is fine — write the exclusion into the lifecycle module so it is a decision, not an accident. -### 11. Actor trust has no home +### 9. Actor trust has no home `classifyJiraActorTrust`/`githubActorTrust` gate who may trigger work, with a "context-only" fallback that _posts a note instead of starting work_. The platform-handler docstring ("determines whether the input should start work", `platform-handler.ts:8-14`) implicitly absorbs trust but never names it, and the context-only path — an outbound platform post with no lifecycle — fits neither handler nor adapter contract as written. It is fine to keep it platform-side and outside the lifecycle; say so explicitly, because it is a security boundary. ## C. Simplifications — things to delete or merge -### 12. `platform-handler.ts` is a vacuous abstraction — delete it +### 10. `platform-handler.ts` is a vacuous abstraction — delete it `NTBSPlatformHandler` is `handle: Input => Effect` (`platform-handler.ts:15-17`). Nothing consumes it polymorphically and nothing ever will — each platform's HTTP route calls its own handler with its own `Input` type; a generic interface over an existential `Input` has no call sites by construction. It is a function type with a ceremony tag factory. The real architecture is two-piece (processor + adapter); the "handler" is just each adapter package's inbound edge, and a comment in `processor.ts` already documents that convention adequately. -### 13. Collapse `ProcessorEvent` — the union wraps two statically-known callers +### 11. Collapse `ProcessorEvent` — the union wraps two statically-known callers `{ source: "adapter" } | { source: "t3" }` (`processor.ts:41-50`) has exactly one producer per arm, and the `"t3"` arm's only legitimate producer is the processor's _own_ subscription. Exposing it invites outsiders to inject synthetic T3 events, and `"adapter"` is a misnomer anyway (the platform handler builds it, not the adapter). Replace with `handleRequest(request, t3Context)` plus the internal subscription; tests can fake the engine's stream through the service dependency instead of injecting events through the front door. Also reconsider exposing `subscribeToT3Events` at all — if callers must wire it, name it for what it is (`run`/`daemon`); its current docstring leaks a private function name (`processT3Event`). -### 14. `RequestAccepted` lies about its own state - -The handler constructs `state: "request.accepted"` _before_ the adapter has accepted anything, then `processAcceptedRequest` step 1 "asks the adapter to accept it" (`processor.ts:124-127`) — a value asserting a persisted state that does not exist yet, named "accepted" while acceptance is pending. Pass the base `{ platformData, snapshot }` into `accept` and let the adapter mint the accepted state. This fixes the semantics and removes a footgun for adapter authors. - -### 15. Justify each of the five states with a distinct recovery action, or cut to three +### 12. Justify each of the five states with a distinct recovery action, or cut to three `ResponseAvailable` as a _persisted_ state buys nothing today: recovery must re-derive the outcome from the projection anyway (finding 5), and dedup only needs "posted". The current stores run on effectively three states (`received`/`processing`/`completed`) plus nullable message IDs. Keep five states only if each maps to a distinct crash-recovery behavior — writing that table down (state → recovery action) is the cheapest way to force findings 1/2/5 to resolution. If a state has no recovery action of its own, it is a log line, not a state. -### 16. Drop the tag factories until something resolves them from context +### 13. Drop the tag factories until something resolves them from context Adapter → processor → handler is a straight constructor chain assembled once at bootstrap; only the HTTP route plausibly needs a context tag. Three string-keyed generic tag factories (`makeNTBS*Tag(key)`) also deviate from the codebase convention (`class X extends Context.Service()("t3/…")` with namespaced IDs) and nothing type-checks that two call sites will not collide on a bare key like `"jira"`. If kept, namespace the keys. (`Context.Service(key)` is a legitimate effect-smol form, so the factories are _sound_, just probably unnecessary.) -### 17. Design the error taxonomy around retryability, not strings +### 14. Design the error taxonomy around retryability, not strings `AdapterError { reason: string }` / `NTBSProcessorError { reason: string }` erase the one distinction the whole outbox pattern turns on: transient (429, network) vs. permanent (403, deleted comment). The current `JiraAppClient` already encodes retry-vs-fallback decisions (400/404 → try next parent). "Will be refined later" is noted in `adapter.ts` — but retryability is the _first_ refinement, and it changes method signatures, so decide it before adapters exist. -### 18. Do not collapse the outcome to `text` before the adapter sees it +### 15. Do not collapse the outcome to `text` before the adapter sees it `resolveT3Outcome` returns bare text and `postResponse(event, text)` posts it (`processor.ts:140-145`, `adapter.ts:55-58`). The docs distinguish answer/failure/timeout/cancellation and adapters own platform rendering — yet the one place rendering matters (a failure vs. an answer) arrives pre-flattened, while ack copy is fully adapter-owned. Asymmetric. Pass a small tagged outcome (`{ kind: "answer" | "failure" | "interrupted" | "timeout"; text }`) and let the adapter format. -### 19. Naming/file nits, worth fixing while it is cheap +### 16. Naming/file nits, worth fixing while it is cheap - `schemas.ts` contains no `effect/Schema` schemas — misleading in a repo where "schema" means that specifically (`packages/contracts` is "schema-only"); call it `lifecycle.ts`. - Three different things are called "event" in one file (platform events, T3 events, and these persisted _states_ with event-shaped names like `"thread.started"`); the base type `LifecycleEvent` is a state/record, and its docstring still says "NTBSEvent" (`schemas.ts:18`) while the docs say `NtbsEvent` and the state names diverge from the doc's (`acknowledgementPosted` vs `thread.started.acknowledged`). From 73121f72724d5042379aa9f0a3b3cc104eefc6ca Mon Sep 17 00:00:00 2001 From: enricopolanski Date: Sat, 8 Aug 2026 17:17:14 +0200 Subject: [PATCH 29/29] chore: finish reviewing first 4 points of adversarial review --- docs/planning/fixes.md | 6 ++++ docs/planning/ntbs-adversarial-review.md | 40 +++++++++--------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/docs/planning/fixes.md b/docs/planning/fixes.md index 2e70003758d..9b985eabe41 100644 --- a/docs/planning/fixes.md +++ b/docs/planning/fixes.md @@ -17,3 +17,9 @@ The acknowledgement is a platform message such as "working on it." It improves f 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. diff --git a/docs/planning/ntbs-adversarial-review.md b/docs/planning/ntbs-adversarial-review.md index 78e6f5127d5..7124bffea9c 100644 --- a/docs/planning/ntbs-adversarial-review.md +++ b/docs/planning/ntbs-adversarial-review.md @@ -12,23 +12,13 @@ Findings are ordered by severity within each section and numbered globally for r ## A. Contract holes — these produce stuck/wrong external state if implemented as declared -### 1. Outcome detection is anchored on "latest turn", which is wrong the moment anyone touches the thread - -`t3Data` keeps only `threadId` (`schemas.ts:29-34`) and `resolveT3Outcome` reads "the latest turn" from the projection (`processor.ts:140-145`). NTBS threads are ordinary threads — visible in native clients, no lock. If a human opens one and sends a follow-up (or a queued message drains), `latestTurn` now describes _their_ turn: the processor will either post nothing or post the second turn's answer to the Jira comment. - -This regresses against both the NTBS docs (ntbs-architecture.md's record keeps `threadId`/`userMessageId`/`turnId`; ntbs-plan.md explicitly says "the lifecycle types must represent that sequence accurately") and the current implementation (`JiraDeliveryStore` keeps `userMessageId`, `previousTurnId`, `targetTurnId` and the bridges do targeted turn discovery). - -`userMessageId` is free — the processor generates it at dispatch. `turnId` genuinely arrives later (the provider adapter mints it; the decider emits `thread.turn-start-requested` with `turnId: null`). - -**Fix:** store `userMessageId` at `ThreadStarted`, resolve the outcome for the turn that answered _that message_, and record the discovered `turnId` when it appears. - -### 2. No timeout anywhere — a silently hung provider leaves an ack dangling forever +### 1. No timeout anywhere — a silently hung provider leaves an ack dangling forever Orchestration has no turn timeout (only 5s/45s provider-_control_ timeouts in `ProviderCommandReactor`), and if an ACP connection drops silently there is no `thread.session-set` until a server restart triggers `OrphanSessionRecovery`. The current Jira bridge covers this with a configurable 30-minute poll deadline plus a "still working" fallback message (`JiraIssueBridge.ts:504-559`). The skeleton has no deadline concept, yet ntbs.md promises "failure, timeout, or cancellation returns a response that explicitly reports the outcome." **Fix:** the processor (platform-independent) owns a per-record deadline; on expiry it posts a timeout outcome and marks the record posted — and document that a late real answer is then dropped, or define a supersede rule. -### 3. The event subscription cannot survive a restart, and nothing compensates +### 2. The event subscription cannot survive a restart, and nothing compensates `subscribeToT3Events` necessarily rides `OrchestrationEngine.streamDomainEvents`, which is an in-memory PubSub — "hot runtime stream (new events only)" (`orchestration/Services/OrchestrationEngine.ts:52-57`). Any turn that completes while the server (or just the processor fiber) is down emits into the void. `readEvents(fromSequenceExclusive)` exists, but the skeleton stores no cursor anywhere, and the engine's own docstring warns against stale-cursor replay. @@ -36,13 +26,13 @@ Orchestration has no turn timeout (only 5s/45s provider-_control_ timeouts in `P This is also the honest framing of a half-made design choice: the current bridges _poll_ per-delivery, which gets recovery and timeouts almost for free at the cost of 1s ticks. Event-wakeup + projection-truth + startup sweep is a fine steady state, but say so explicitly — the mechanism is currently smeared across `subscribeToT3Events`' docstring. -### 4. Double-posting is possible and undocumented +### 3. Double-posting is possible and undocumented Crash between a successful `postResponse` and `save(ResponsePosted)` → recovery re-posts. Platforms have no idempotent comment-create, so this is inherent at-least-once delivery. The current bridge has the same window; that is acceptable — but the adapter contract should state which semantics adapters must expect, because it changes what `save` failures mean. ## B. The codebase already has things the skeleton re-declares or ignores -### 5. The turn-start bootstrap already exists as a single command — do not rebuild it from Git primitives +### 4. The turn-start bootstrap already exists as a single command — do not rebuild it from Git primitives `ThreadTurnStartCommand.bootstrap` covers createThread + prepareWorktree + runSetupScript + first message + turn start (`packages/contracts/src/orchestration.ts:744-803`). Its server-side executor is currently inlined in `ws.ts:816-1168` (`dispatchBootstrapTurnStart`), including subtleties the skeleton would otherwise re-learn the hard way: `thread.meta.update` after worktree creation, polling the projection until the worktree is _visible_ before provider start (`ws.ts:953`), setup-script activity records, `startFromOrigin` fetch/resolve. @@ -50,7 +40,7 @@ The skeleton instead declares its own `createT3Thread`/`startT3Turn` split depen **Fix:** do the extraction; the processor's dependency list shrinks to OrchestrationEngine + ProjectionSnapshotQuery + the extracted service. One nuance to encode there: ws.ts _continues_ on worktree-prep failure (`ws.ts:879`), but NTBS must _fail_ the event instead — isolation is an agreed hard requirement (ntbs.md §concurrency). The shared service needs a strictness knob. -### 6. Provenance is first-class in T3 and the skeleton cannot carry it +### 5. Provenance is first-class in T3 and the skeleton cannot carry it `SourceChannel` already enumerates `discord`/`github`/`jira`/`slack`/`teams` (`packages/contracts/src/identity.ts:60-73`), `SourceRef`/`SourceLocation` carry issueKey/owner/repo/number/guildId (`identity.ts:75-116`), `ThreadTurnStartCommand` has `source`/`sourceHint` seats (`orchestration.ts:796-801`), threads have `originSource` (`orchestration.ts:443`), and the current bridges stamp it via `buildIntegrationSourceRef` (`identity/stampSource.ts:175`). @@ -58,49 +48,49 @@ The skeleton instead declares its own `createT3Thread`/`startT3Turn` split depen **Fix:** add `source`/`sourceHint` to `T3Context` (or the processor's dispatch), and reword the doc's opacity principle to "T3 never interprets platform data for lifecycle/routing decisions." -### 7. `T3Context` is missing everything else a thread needs, with no stated defaulting policy +### 6. `T3Context` is missing everything else a thread needs, with no stated defaulting policy Thread creation requires `title`, `modelSelection`, `runtimeMode`, `interactionMode` (`orchestration.ts:627-641`). The current bridge resolves model as `project.defaultModelSelection ?? getAutoBootstrapDefaultModelSelection()`, title from the issue summary, runtimeMode `"full-access"` (`JiraIssueBridge.ts:396-399`). Either `T3Context` grows optional overrides (platforms will eventually want them — e.g. a Jira label selecting a model) or the processor owns the defaulting policy; right now neither is written down. Same for `revision` — is it a branch name or pinned SHA, resolved at accept-time or worktree-time? The current bridge resolves `origin/` at worktree time. -### 8. `snapshot: string` will hit the 120k input cap and silently forecloses attachments +### 7. `snapshot: string` will hit the 120k input cap and silently forecloses attachments `PROVIDER_SEND_TURN_MAX_INPUT_CHARS = 120_000` (`orchestration.ts:145`) — a captured Jira issue + comment history or a PR diff snapshot can exceed it, and then the dispatch fails and the lifecycle wedges (see finding 1). The processor must own a truncation policy. Separately, turn messages support image attachments (`ChatAttachment`), and Jira/GitHub/Discord invocations routinely include screenshots; `snapshot: string` forecloses them. Punting is fine — write the exclusion into the lifecycle module so it is a decision, not an accident. -### 9. Actor trust has no home +### 8. Actor trust has no home `classifyJiraActorTrust`/`githubActorTrust` gate who may trigger work, with a "context-only" fallback that _posts a note instead of starting work_. The platform-handler docstring ("determines whether the input should start work", `platform-handler.ts:8-14`) implicitly absorbs trust but never names it, and the context-only path — an outbound platform post with no lifecycle — fits neither handler nor adapter contract as written. It is fine to keep it platform-side and outside the lifecycle; say so explicitly, because it is a security boundary. ## C. Simplifications — things to delete or merge -### 10. `platform-handler.ts` is a vacuous abstraction — delete it +### 9. `platform-handler.ts` is a vacuous abstraction — delete it `NTBSPlatformHandler` is `handle: Input => Effect` (`platform-handler.ts:15-17`). Nothing consumes it polymorphically and nothing ever will — each platform's HTTP route calls its own handler with its own `Input` type; a generic interface over an existential `Input` has no call sites by construction. It is a function type with a ceremony tag factory. The real architecture is two-piece (processor + adapter); the "handler" is just each adapter package's inbound edge, and a comment in `processor.ts` already documents that convention adequately. -### 11. Collapse `ProcessorEvent` — the union wraps two statically-known callers +### 10. Collapse `ProcessorEvent` — the union wraps two statically-known callers `{ source: "adapter" } | { source: "t3" }` (`processor.ts:41-50`) has exactly one producer per arm, and the `"t3"` arm's only legitimate producer is the processor's _own_ subscription. Exposing it invites outsiders to inject synthetic T3 events, and `"adapter"` is a misnomer anyway (the platform handler builds it, not the adapter). Replace with `handleRequest(request, t3Context)` plus the internal subscription; tests can fake the engine's stream through the service dependency instead of injecting events through the front door. Also reconsider exposing `subscribeToT3Events` at all — if callers must wire it, name it for what it is (`run`/`daemon`); its current docstring leaks a private function name (`processT3Event`). -### 12. Justify each of the five states with a distinct recovery action, or cut to three +### 11. Justify each of the five states with a distinct recovery action, or cut to three `ResponseAvailable` as a _persisted_ state buys nothing today: recovery must re-derive the outcome from the projection anyway (finding 5), and dedup only needs "posted". The current stores run on effectively three states (`received`/`processing`/`completed`) plus nullable message IDs. Keep five states only if each maps to a distinct crash-recovery behavior — writing that table down (state → recovery action) is the cheapest way to force findings 1/2/5 to resolution. If a state has no recovery action of its own, it is a log line, not a state. -### 13. Drop the tag factories until something resolves them from context +### 12. Drop the tag factories until something resolves them from context Adapter → processor → handler is a straight constructor chain assembled once at bootstrap; only the HTTP route plausibly needs a context tag. Three string-keyed generic tag factories (`makeNTBS*Tag(key)`) also deviate from the codebase convention (`class X extends Context.Service()("t3/…")` with namespaced IDs) and nothing type-checks that two call sites will not collide on a bare key like `"jira"`. If kept, namespace the keys. (`Context.Service(key)` is a legitimate effect-smol form, so the factories are _sound_, just probably unnecessary.) -### 14. Design the error taxonomy around retryability, not strings +### 13. Design the error taxonomy around retryability, not strings `AdapterError { reason: string }` / `NTBSProcessorError { reason: string }` erase the one distinction the whole outbox pattern turns on: transient (429, network) vs. permanent (403, deleted comment). The current `JiraAppClient` already encodes retry-vs-fallback decisions (400/404 → try next parent). "Will be refined later" is noted in `adapter.ts` — but retryability is the _first_ refinement, and it changes method signatures, so decide it before adapters exist. -### 15. Do not collapse the outcome to `text` before the adapter sees it +### 14. Do not collapse the outcome to `text` before the adapter sees it `resolveT3Outcome` returns bare text and `postResponse(event, text)` posts it (`processor.ts:140-145`, `adapter.ts:55-58`). The docs distinguish answer/failure/timeout/cancellation and adapters own platform rendering — yet the one place rendering matters (a failure vs. an answer) arrives pre-flattened, while ack copy is fully adapter-owned. Asymmetric. Pass a small tagged outcome (`{ kind: "answer" | "failure" | "interrupted" | "timeout"; text }`) and let the adapter format. -### 16. Naming/file nits, worth fixing while it is cheap +### 15. Naming/file nits, worth fixing while it is cheap - `schemas.ts` contains no `effect/Schema` schemas — misleading in a repo where "schema" means that specifically (`packages/contracts` is "schema-only"); call it `lifecycle.ts`. - Three different things are called "event" in one file (platform events, T3 events, and these persisted _states_ with event-shaped names like `"thread.started"`); the base type `LifecycleEvent` is a state/record, and its docstring still says "NTBSEvent" (`schemas.ts:18`) while the docs say `NtbsEvent` and the state names diverge from the doc's (`acknowledgementPosted` vs `thread.started.acknowledged`).