Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
157 commits
Select commit Hold shift + click to select a range
e15d56e
docs: outline non-turn-based surface problem
omegent-app[bot] Jul 31, 2026
08cf077
docs: develop NTBS event execution proposal
omegent-app[bot] Aug 4, 2026
5e81c76
chore: defined processing
enricopolanski Aug 4, 2026
66fc816
chore: planning of ntsb processing
enricopolanski Aug 4, 2026
2520ce9
chore: update ntsb planning
enricopolanski Aug 4, 2026
fb35e27
chore: planning ntsb output
enricopolanski Aug 5, 2026
b60d8bd
chore: more processing
enricopolanski Aug 5, 2026
f076f9c
feat: finish processing
enricopolanski Aug 5, 2026
0315ca4
chore: kickstart architecture document
enricopolanski Aug 5, 2026
bb095fb
chore: settle on generic definition
enricopolanski Aug 5, 2026
2e8830b
chore: document lifecycle
enricopolanski Aug 5, 2026
a484593
feat: define ntbs architeture
enricopolanski Aug 5, 2026
4f0ac10
feat: rename ntsb -> ntbs
enricopolanski Aug 5, 2026
ce4864d
fix: ntsb -> ntbs
enricopolanski Aug 5, 2026
cacc425
feat: write plan
enricopolanski Aug 6, 2026
c6ff7a6
feat: implement basic lifecycle types
enricopolanski Aug 7, 2026
d8f51d0
feat: implement adapter context service
enricopolanski Aug 7, 2026
e240d6a
feat: work on processor
enricopolanski Aug 7, 2026
06aa6a0
feat: processor types
enricopolanski Aug 7, 2026
b9ddcf0
add inbout and outbound processor function
enricopolanski Aug 7, 2026
3050eae
feat: add makeProcessor declaration
enricopolanski Aug 7, 2026
1cb5aaa
feat: more ntbs processor work
enricopolanski Aug 7, 2026
693acf2
feat: document ntbs processor declarations
enricopolanski Aug 7, 2026
1cc1ce3
fix: ntbs processor and schemas flows
enricopolanski Aug 7, 2026
2cf54d5
fix: docs in ntbs processor
enricopolanski Aug 7, 2026
0f2678f
chore: complete abstract/declaration phase
enricopolanski Aug 8, 2026
7cc3399
chore: fixes part 1
enricopolanski Aug 8, 2026
06d98fd
feat: review removal of the requestaccepted lifecycle event
enricopolanski Aug 8, 2026
c3c22fd
chore: finish reviewing first 4 points of adversarial review
enricopolanski Aug 8, 2026
d04c510
chore: finalize review
enricopolanski Aug 8, 2026
2c03c90
feat: write adversarial review
enricopolanski Aug 9, 2026
7b7e776
chore: document concurrency of NTBSProcessor
enricopolanski Aug 9, 2026
a1dfae8
feat: more updates and refactors of ntbs
enricopolanski Aug 9, 2026
18f012b
feat: more ntbs fixes
enricopolanski Aug 9, 2026
bf9662c
fix #7 of adversarial review
enricopolanski Aug 9, 2026
84a5da4
feat: add or note all fixes
enricopolanski Aug 9, 2026
4b2a545
feat: start working on processor
enricopolanski Aug 9, 2026
5b35c02
chore: remove adapter.accept, not really needed
enricopolanski Aug 9, 2026
69ff904
chore: continue work on processor
enricopolanski Aug 9, 2026
264eac7
feat: processor, implement adversarial review feedback
enricopolanski Aug 10, 2026
ac1c569
feat: added soft concurrency lock
enricopolanski Aug 10, 2026
2bc10c2
feat: create threadstarted in processor
enricopolanski Aug 10, 2026
20716ad
feat: implement start t3 turn
enricopolanski Aug 10, 2026
f381b2c
feat: implement get progression logic
enricopolanski Aug 11, 2026
be64e45
chore: more work on monitor
enricopolanski Aug 11, 2026
fb67c56
docs: monitor
enricopolanski Aug 11, 2026
2b862f6
fix: turn and message consistency
enricopolanski Aug 11, 2026
0011c95
chore: retry retriable stuff
enricopolanski Aug 11, 2026
07c6f15
feat: more work on locks and interrupts
enricopolanski Aug 12, 2026
4a2b3df
chore: processor v1
enricopolanski Aug 12, 2026
5d99cee
feat: start processor testing
enricopolanski Aug 12, 2026
35227a6
chore: more processor tests
enricopolanski Aug 12, 2026
9debc39
chore: first step of processor testing
enricopolanski Aug 13, 2026
3b6e6c6
chore: add deferred idea to ideas.md
enricopolanski Aug 13, 2026
d5e037b
feat: simplify adapter
enricopolanski Aug 13, 2026
509e655
chore: add new idea
enricopolanski Aug 13, 2026
829b9b6
chore: update processor to new api
enricopolanski Aug 13, 2026
75e6521
feat: more work on ntbs test helpers
enricopolanski Aug 13, 2026
f2179ae
chore: sourceUris improvements
enricopolanski Aug 13, 2026
4b71c96
fix: typescript stuff
enricopolanski Aug 14, 2026
d15a33e
docs: review NTBS processor refinements
enricopolanski Aug 14, 2026
8786971
chore: claude code review
enricopolanski Aug 14, 2026
11c8c9d
test: note missing-turn recovery coverage
enricopolanski Aug 14, 2026
ad76111
refactor: simplify NTBS turn monitoring
enricopolanski Aug 14, 2026
012f0ed
chore: remove pointless recordedAt
enricopolanski Aug 14, 2026
4f951ed
feat: simplify turn status
enricopolanski Aug 14, 2026
7136a61
chore: simplify resolveT3Outcome
enricopolanski Aug 14, 2026
7b728e9
chore: simplify acknowledgement
enricopolanski Aug 14, 2026
0e5b676
chore: refactor process
enricopolanski Aug 14, 2026
7014ba1
chore: implement feedback
enricopolanski Aug 15, 2026
dc0f90c
docs: update NTBS processor review
enricopolanski Aug 15, 2026
e67dc48
docs: scope pull request handoff rules
enricopolanski Aug 15, 2026
fa9833b
chore: remove all the monitoring crap
enricopolanski Aug 15, 2026
a70a0aa
chore: refactor
enricopolanski Aug 15, 2026
eeeca85
chore: remove stale/wrong todo
enricopolanski Aug 15, 2026
68ae88f
docs(ntbs): refresh processor reviews
enricopolanski Aug 15, 2026
3976de8
chore: renames and refactors
enricopolanski Aug 16, 2026
a3324f6
feat: continue refactoring
enricopolanski Aug 16, 2026
b245310
feat: refine next phases of work
enricopolanski Aug 16, 2026
87f2489
chore: start refactoring the ntbs state
enricopolanski Aug 16, 2026
9620fd7
chore: bump ntbs todos
enricopolanski Aug 17, 2026
7bb0177
feat: continue refactor of exchange
enricopolanski Aug 17, 2026
5d61152
feat: implement exchange.ts
enricopolanski Aug 18, 2026
3feccf6
feat: complete exchange refactor and its tests
enricopolanski Aug 18, 2026
83945a3
feat: update exchange.ts, implement exchangerepository
enricopolanski Aug 19, 2026
60ec43f
feat: update adapter and exchangerepository
enricopolanski Aug 21, 2026
a8ae9fd
feat: update adapter
enricopolanski Aug 21, 2026
3a298d0
feat: t3gateway core type implementation
enricopolanski Aug 24, 2026
4f64b36
chore: update ntbs docs
enricopolanski Aug 24, 2026
0240582
chore: bump
enricopolanski Aug 24, 2026
b383ec8
bump
enricopolanski Aug 24, 2026
e36e9ae
refactor(ntbs): single adapter tag, rewrite processor docs
enricopolanski Aug 24, 2026
ba2f39b
chore: align tags
enricopolanski Aug 24, 2026
045cdbd
chore: refactor the processor
enricopolanski Aug 24, 2026
efd3468
update
enricopolanski Aug 24, 2026
08b6be4
feat: start work
enricopolanski Aug 24, 2026
4ff48db
chore: bump processor with a lock
enricopolanski Aug 24, 2026
26bb942
process refactor
enricopolanski Aug 24, 2026
c63a1a4
chore: update tests to use baseRefSha
enricopolanski Aug 24, 2026
72c58d6
chore: naming clean up
enricopolanski Aug 24, 2026
af1ed76
chore: update processor
enricopolanski Aug 24, 2026
47802fd
fix: type issue
enricopolanski Aug 24, 2026
46ac2e6
feat: write processor new tests
enricopolanski Aug 24, 2026
7788299
feat: bump todo
enricopolanski Aug 25, 2026
a2ed992
chore: refine t3 gateway apis
enricopolanski Aug 25, 2026
19cb40c
feat: implement planCoordinates
enricopolanski Aug 25, 2026
ee69990
feat: handle edge cases in t3gateway planCoordinates
enricopolanski Aug 26, 2026
ce43854
chore: bump tests
enricopolanski Aug 26, 2026
0c31d23
chore: updates
enricopolanski Aug 27, 2026
a9d6bcc
feat: implement first successful test of t3gateway
enricopolanski Aug 27, 2026
7f315fe
feat: more work on t3 tests
enricopolanski Aug 27, 2026
4f37b55
chore: more invariants on t3 gateway
enricopolanski Aug 27, 2026
89aa76d
chore: more quality refactoring
enricopolanski Aug 27, 2026
8ada944
feat: write remote missing tests
enricopolanski Aug 28, 2026
f63c418
chore: refactor errors in t3gateway
enricopolanski Aug 28, 2026
70f79ba
feat: more tests
enricopolanski Aug 28, 2026
5e0342a
chore: fix t3 gateway tests
enricopolanski Aug 28, 2026
d272b6c
chore: test retryable remoteExists failure
enricopolanski Aug 28, 2026
eb41fb6
feat: complete planCoordinates testing
enricopolanski Aug 28, 2026
ad7e4e5
feat: t3gateway - test and implement getThreadStatus
enricopolanski Aug 28, 2026
e046c3c
fix: remove todo test
enricopolanski Aug 28, 2026
6d70ff4
chore: add todo
enricopolanski Aug 28, 2026
3075820
chore: add docs and shit
enricopolanski Aug 29, 2026
dc72374
chore; partialwork
enricopolanski Aug 29, 2026
5c30ca4
chore: first version of provision thread
enricopolanski Aug 30, 2026
66278d6
chore: add feedback
enricopolanski Aug 30, 2026
8cc3a08
fix: createWorkTree in provisionThread correctly passes the newly min…
enricopolanski Aug 30, 2026
4135907
chore: provisionThread second version with ensureWorktree
enricopolanski Aug 30, 2026
c9a6b0a
feat: refactor error handling in provisionThread
enricopolanski Aug 31, 2026
d139319
chore: provisionThread improvements
enricopolanski Aug 31, 2026
2a91a36
chore: align docs of provisionThread
enricopolanski Aug 31, 2026
88b1fc3
feat: bump provisionThread (v0.01.a xD)
enricopolanski Aug 31, 2026
cb828c8
chore: minor linting stuff
enricopolanski Aug 31, 2026
fd1fdb2
feat: expand happy case test of provisionThread
enricopolanski Aug 31, 2026
21979ad
feat: add second success test, provisionthread
enricopolanski Aug 31, 2026
0dfe706
chore: update docs
enricopolanski Aug 31, 2026
25e9f7a
chore: complete provisionThread tests
enricopolanski Aug 31, 2026
8f74727
chore: minor refactor and bump to docs in t3gateway tests
enricopolanski Aug 31, 2026
d93fe77
chore: more docs bump
enricopolanski Aug 31, 2026
9c05096
chore: outline the tests for getTurnStatus
enricopolanski Aug 31, 2026
f506127
chore: getTurnStatus first implementation
enricopolanski Aug 31, 2026
e242cf7
feat: write tests for getTurnStatus
enricopolanski Aug 31, 2026
dee70a5
chore: define the work and update documentation around
enricopolanski Aug 31, 2026
3ba37a6
feat: implement startTurn and related tests
enricopolanski Aug 31, 2026
cc5b8b4
feat: implement threadActivity stream on t3gateway and related tests
enricopolanski Aug 31, 2026
426063b
chore: cleanup
enricopolanski Sep 1, 2026
d51ae3b
feat: implement the sweeper in the processor
enricopolanski Sep 1, 2026
b51c5b3
feat: rename file
enricopolanski Sep 1, 2026
beb82b9
chore: write the test setup
enricopolanski Sep 1, 2026
94e66cb
feat: processor testing, completed harness and first test
enricopolanski Sep 1, 2026
6393c82
feat: more tests for processor
enricopolanski Sep 1, 2026
d15fb93
chore: final tests
enricopolanski Sep 1, 2026
c80ee1b
chore: remove old test file
enricopolanski Sep 1, 2026
9984c1a
feat: address h1
enricopolanski Sep 2, 2026
de80812
feat: rewrite todos for request accepted flow
enricopolanski Sep 2, 2026
9165e3c
feat: bump
enricopolanski Sep 2, 2026
35567b8
feat: implement timestamps in exchanges
enricopolanski Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
},
"search.exclude": {
".repos/**": true
}
},
"js/ts.experimental.useTsgo": true
}
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,14 @@ whole branching model. See
work dir (`~/.t3/compose-work`, not tmpfs `/tmp`) before install. See
[docs/fork-stack.md](./docs/fork-stack.md) ("Integration overlay compose and lockfiles").

## Pull requests (required handoff)
## Pull requests (when publishing)

When implementation work for a user request is done (code, docs, config — not pure Q&A):
Do not commit, rebase, push, or open/update a PR merely because an edit is complete. Do those
things only when the user explicitly requests publication or the specific version-control action,
or when another workflow in this file explicitly requires it (for example, Discord-originated
work). A request to change code, docs, or config does not by itself authorize publication.

When publication or a PR handoff is in scope:

1. **Commit** the changes on a feature branch cut from `fork/dev`.
2. **Open or update a PR against `fork/dev`** before handing off — for every kind of work, including
Expand Down
260 changes: 260 additions & 0 deletions apps/server/src/ntbs/ExchangeRepository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit } from "effect";
import { MessageId, ProjectId, ThreadId, TurnId } from "@t3tools/contracts";
import {
ExchangeRepositoryError,
ExchangeRepository,
inMemoryExchangeRepository,
} from "./ExchangeRepository.ts";
import {
makeRequestAccepted,
toReplyPending,
toReplyPosted,
toThreadCreated,
toUndeliverable,
toWorkPlanned,
type Reply,
} from "./exchange.ts";

const now = 1_700_000_000_000;

const makeAccepted = (sourceUri: string) =>
makeRequestAccepted(
{ sourceUri, snapshot: "request", attachments: [] },
{ projectId: ProjectId.make("project"), startBranchName: "main" },
now,
);

const makeExchange = (sourceUri: string, threadId: string) =>
toWorkPlanned(
makeAccepted(sourceUri),
{
projectId: ProjectId.make("project"),
startBranchName: "main",
startCommitSha: "start-commit-sha",
threadId: ThreadId.make(threadId),
userMessageId: MessageId.make(`message-${threadId}`),
worktreeBranchName: `branch-${threadId}`,
},
now,
);

const answerFrom = (threadId: string, text: string): Reply => ({
type: "answer",
text,
threadId: ThreadId.make(threadId),
userMessageId: MessageId.make(`message-${threadId}`),
turnId: TurnId.make(`turn-${threadId}`),
});

describe("inMemoryExchangeRepository", () => {
it.layer(inMemoryExchangeRepository)((it) => {
it.effect("allows the same sourceUri to advance its state", () =>
Effect.gen(function* () {
const repository = yield* ExchangeRepository;
const planned = makeExchange("test://request/1", "thread-1");
const threadCreated = toThreadCreated(planned, now);

yield* repository.upsert(planned);
yield* repository.upsert(threadCreated);

expect(yield* repository.findBySourceUri(planned.sourceUri)).toEqual(threadCreated);
}),
);
});

it.layer(inMemoryExchangeRepository)((it) => {
it.effect("allows the same state to be rewritten", () =>
Effect.gen(function* () {
const repository = yield* ExchangeRepository;
const planned = makeExchange("test://request/1", "thread-1");

yield* repository.upsert(planned);
yield* repository.upsert(planned);

expect(yield* repository.findBySourceUri(planned.sourceUri)).toEqual(planned);
}),
);
});

it.layer(inMemoryExchangeRepository)((it) => {
it.effect("refuses a replacement that is not an update of the stored exchange", () =>
Effect.gen(function* () {
const repository = yield* ExchangeRepository;
const planned = makeExchange("test://request/1", "thread-1");
const threadCreated = toThreadCreated(planned, now);
const replyPending = toReplyPending(threadCreated, answerFrom("thread-1", "reply"), now);
const posted = toReplyPosted(replyPending, "test://reply/1", now);

yield* repository.upsert(threadCreated);
const backwards = yield* Effect.flip(repository.upsert(planned));
expect(backwards).toBeInstanceOf(ExchangeRepositoryError);

const skippingAhead = yield* Effect.flip(repository.upsert(posted));
expect(skippingAhead).toBeInstanceOf(ExchangeRepositoryError);

yield* repository.upsert(replyPending);
yield* repository.upsert(posted);
const afterTerminal = yield* Effect.flip(repository.upsert(threadCreated));
expect(afterTerminal).toBeInstanceOf(ExchangeRepositoryError);

expect(yield* repository.findBySourceUri(planned.sourceUri)).toEqual(posted);
}),
);
});

it.layer(inMemoryExchangeRepository)((it) => {
it.effect("rejects a threadId already owned by another sourceUri", () =>
Effect.gen(function* () {
const repository = yield* ExchangeRepository;
const existing = makeExchange("test://request/1", "shared-thread");
const conflicting = makeExchange("test://request/2", "shared-thread");

yield* repository.upsert(existing);
const error = yield* Effect.flip(repository.upsert(conflicting));

expect(error).toBeInstanceOf(ExchangeRepositoryError);
expect(error.reason).toContain(existing.t3.threadId);
expect(yield* repository.findBySourceUri(existing.sourceUri)).toEqual(existing);
expect(yield* repository.findBySourceUri(conflicting.sourceUri)).toBeNull();
}),
);
});

it.layer(inMemoryExchangeRepository)((it) => {
it.effect("keeps a replied exchange's thread out of reach of other sourceUris", () =>
Effect.gen(function* () {
const repository = yield* ExchangeRepository;
const replied = toReplyPending(
toThreadCreated(makeExchange("test://request/1", "shared-thread"), now),
answerFrom("shared-thread", "done"),
now,
);
const conflicting = makeExchange("test://request/2", "shared-thread");

yield* repository.upsert(replied);
const error = yield* Effect.flip(repository.upsert(conflicting));

expect(error).toBeInstanceOf(ExchangeRepositoryError);
expect(yield* repository.findBySourceUri(conflicting.sourceUri)).toBeNull();
}),
);
});

it.layer(inMemoryExchangeRepository)((it) => {
it.effect("finds an exchange by threadId", () =>
Effect.gen(function* () {
const repository = yield* ExchangeRepository;
const exchange = makeExchange("test://request/1", "thread-1");

yield* repository.upsert(exchange);

expect(yield* repository.findByThreadId(exchange.t3.threadId)).toEqual(exchange);
expect(yield* repository.findByThreadId(ThreadId.make("unknown-thread"))).toBeNull();
}),
);
});

it.layer(inMemoryExchangeRepository)((it) => {
it.effect("finds only non-terminal exchanges", () =>
Effect.gen(function* () {
const repository = yield* ExchangeRepository;
const accepted = makeAccepted("test://request/accepted");
const planned = makeExchange("test://request/planned", "thread-planned");
const threadCreated = toThreadCreated(
makeExchange("test://request/thread-created", "thread-created"),
now,
);
const replyPending = toReplyPending(
toThreadCreated(
makeExchange("test://request/reply-pending", "thread-reply-pending"),
now,
),
answerFrom("thread-reply-pending", "pending reply"),
now,
);
const replyPosted = toReplyPosted(
toReplyPending(
toThreadCreated(
makeExchange("test://request/reply-posted", "thread-reply-posted"),
now,
),
answerFrom("thread-reply-posted", "posted reply"),
now,
),
"test://reply/posted",
now,
);
const undeliverable = toUndeliverable(
toReplyPending(
toThreadCreated(
makeExchange("test://request/undeliverable", "thread-undeliverable"),
now,
),
answerFrom("thread-undeliverable", "undeliverable reply"),
now,
),
{ message: "platform rejected the reply" },
now,
);

yield* Effect.forEach(
[accepted, planned, threadCreated, replyPending, replyPosted, undeliverable],
repository.upsert,
);

const results = yield* repository.findNonTerminalExchanges;

expect(results).toHaveLength(4);
expect(results).toEqual(
expect.arrayContaining([accepted, planned, threadCreated, replyPending]),
);
}),
);
});

it.layer(inMemoryExchangeRepository)((it) => {
it.effect("preserves existing records when a replacement has a conflicting threadId", () =>
Effect.gen(function* () {
const repository = yield* ExchangeRepository;
const first = makeExchange("test://request/1", "thread-1");
const second = makeExchange("test://request/2", "thread-2");
const conflictingReplacement = makeExchange("test://request/2", "thread-1");

yield* repository.upsert(first);
yield* repository.upsert(second);
yield* Effect.flip(repository.upsert(conflictingReplacement));

expect(yield* repository.findBySourceUri(first.sourceUri)).toEqual(first);
expect(yield* repository.findBySourceUri(second.sourceUri)).toEqual(second);
expect(yield* repository.findByThreadId(first.t3.threadId)).toEqual(first);
expect(yield* repository.findByThreadId(second.t3.threadId)).toEqual(second);
}),
);
});

it.layer(inMemoryExchangeRepository)((it) => {
it.effect("atomically rejects concurrent upserts with the same threadId", () =>
Effect.gen(function* () {
const repository = yield* ExchangeRepository;
const first = makeExchange("test://request/1", "shared-thread");
const second = makeExchange("test://request/2", "shared-thread");

const outcomes = yield* Effect.all(
[Effect.exit(repository.upsert(first)), Effect.exit(repository.upsert(second))],
{ concurrency: "unbounded" },
);

expect(outcomes.filter(Exit.isSuccess)).toHaveLength(1);
expect(outcomes.filter(Exit.isFailure)).toHaveLength(1);

const stored = yield* Effect.all([
repository.findBySourceUri(first.sourceUri),
repository.findBySourceUri(second.sourceUri),
]);

expect(stored.filter((state) => state !== null)).toHaveLength(1);
}),
);
});
});
Loading
Loading