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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions apps/server/src/identity/applyThreadAttribution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,34 @@ describe("applyStoredThreadAttribution", () => {
expect(enriched.originSource?.username).toBe("enricopolanski");
expect(enriched.participantSummaries?.[0]?.personId).toBe("enricopolanski");
});

it("recovers origin from the Discord overlay when bootstrap stored no SourceRef", () => {
const dir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-identity-"));
const mapPath = NodePath.join(dir, "identity-map.yaml");
NodeFS.writeFileSync(
mapPath,
`people:
"147977704522645504":
username: enricopolanski
name: Enrico Polanski
`,
);
process.env.T3_IDENTITY_MAP_PATH = mapPath;

const enriched = applyStoredThreadAttribution({
originSource: null,
participantSummaries: [],
createdAt: "2026-09-25T09:18:14.418Z",
messages: [
{
role: "user",
createdAt: "2026-09-25T09:18:14.418Z",
text: "req: 147977704522645504@enricopolanski\n## User request\nHi",
},
],
});

expect(enriched.originSource?.personId).toBe("enricopolanski");
expect(enriched.participantSummaries?.[0]?.personId).toBe("enricopolanski");
});
});
1 change: 1 addition & 0 deletions apps/server/src/identity/applyThreadAttribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function applyStoredThreadAttribution(input: {
| ReadonlyArray<{
readonly role: string;
readonly createdAt: string;
readonly text?: string | undefined;
readonly source?: SourceRef | undefined;
}>
| undefined;
Expand Down
12 changes: 9 additions & 3 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
ThreadId,
} from "@t3tools/contracts";
import { compareDateTimeStrings } from "@t3tools/shared/dateTime";
import { withMappedPerson } from "@t3tools/shared/sourceAttribution";
import { parseDiscordConversationActor, withMappedPerson } from "@t3tools/shared/sourceAttribution";
import { readIdentityMapPeopleFromEnv } from "../../identity/IdentityService.ts";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
Expand Down Expand Up @@ -626,10 +626,16 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
? withMappedPerson(existingRow.value.originSource, identityPeople)
: null;
for (const message of messages) {
if (message.role !== "user" || message.source === undefined) {
if (message.role !== "user") {
continue;
}
const messageSource = withMappedPerson(message.source, identityPeople);
let rawSource = message.source;
if (rawSource === undefined) {
const actor = parseDiscordConversationActor(message.text);
if (actor === null) continue;
rawSource = { channel: "discord", actor };
}
const messageSource = withMappedPerson(rawSource, identityPeople);
if (rebuiltOrigin === null || rebuiltOrigin === undefined) {
rebuiltOrigin = messageSource;
}
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
...(command.message.context !== undefined ? { context: command.message.context } : {}),
turnId: null,
streaming: false,
...(command.source !== undefined ? { source: command.source } : {}),
createdAt: command.createdAt,
updatedAt: command.createdAt,
},
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/orchestration/decider.userMessageAppend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { expect, it } from "@effect/vitest";
import {
CommandId,
EventId,
IdentityUsername,
MessageId,
PersonId,
ProjectId,
ProviderInstanceId,
ThreadId,
Expand Down Expand Up @@ -96,6 +98,32 @@ it.layer(NodeServices.layer)("thread.message.user.append", (it) => {
}),
);

it.effect("copies the bootstrap source onto the persisted user message", () =>
Effect.gen(function* () {
const readModel = yield* readModelWithThread;
const planned = yield* decideOrchestrationCommand({
command: {
...appendCommand,
source: {
channel: "discord" as const,
personId: PersonId.make("enricopolanski"),
username: IdentityUsername.make("enricopolanski"),
actor: { platformId: "147977704522645504", displayName: "enricopolanski" },
},
},
readModel,
});
const events = Array.isArray(planned) ? planned : [planned];
expect(events[0]?.payload).toMatchObject({
source: {
channel: "discord",
personId: "enricopolanski",
username: "enricopolanski",
},
});
}),
);

it.effect("rejects a message id that already exists on the thread", () =>
Effect.gen(function* () {
const readModel = yield* readModelWithThread;
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1689,6 +1689,7 @@ const makeWsRpcLayer = (
? { context: command.message.context }
: {}),
},
...(command.source !== undefined ? { source: command.source } : {}),
createdAt: command.createdAt,
});
if (tracked) {
Expand Down
5 changes: 5 additions & 0 deletions apps/server/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export default mergeConfig(
"src/assets/AssetAccess.test.ts",
"src/bootstrap.test.ts",
"src/cli/app.test.ts",
"src/orchestration/Layers/CheckpointReactor.test.ts",
"src/provider/Layers/ClaudeCapabilitiesProbe.test.ts",
"src/provider/Layers/GrokAdapter.test.ts",
"src/provider/Layers/ProviderRegistry.test.ts",
Expand All @@ -176,6 +177,10 @@ export default mergeConfig(
// real os module and the mock never applies — the CLI then
// connects to the live Linux desktop socket.
"src/cli/app.test.ts",
// Real git in a temp cwd. Under isolate:false a sibling can leave
// a .git / unparseable HEAD and `git: false` / init-between-turns
// fail on CI (`existsSync(.git)` true, `could not parse HEAD`).
"src/orchestration/Layers/CheckpointReactor.test.ts",
// xAI prompt-complete can land after the assertion under
// isolate:false CI load (`hello from ` vs `hello from mock`).
"src/provider/Layers/GrokAdapter.test.ts",
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1677,6 +1677,8 @@ const ThreadMessageUserAppendCommand = Schema.Struct({
attachments: Schema.Array(ChatAttachment),
context: Schema.optional(OrchestrationMessageContext),
}),
/** Copied from the bootstrap turn.start stamp. Worktree creates skip message-sent. */
source: Schema.optional(SourceRef),
createdAt: IsoDateTime,
});

Expand Down
31 changes: 31 additions & 0 deletions packages/shared/src/sourceAttribution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
enrichThreadAttribution,
mergeParticipantSummaries,
nextOriginSource,
parseDiscordConversationActor,
resolveSourceChannel,
sourceChannelFromDeviceType,
withMappedPerson,
Expand Down Expand Up @@ -72,6 +73,14 @@ describe("nextOriginSource", () => {
});
});

describe("parseDiscordConversationActor", () => {
it("reads req: snowflake@handle from the Discord overlay", () => {
expect(
parseDiscordConversationActor("req: 147977704522645504@enricopolanski\npr: name=x uid=1"),
).toEqual({ platformId: "147977704522645504", displayName: "enricopolanski" });
});
});

describe("withMappedPerson", () => {
const people = parseIdentityMapDocument({
people: {
Expand Down Expand Up @@ -128,6 +137,28 @@ describe("enrichThreadAttribution", () => {
expect(enriched.participantSummaries[0]?.firstChannel).toBe("discord");
});

it("recovers origin from the Discord overlay when the bootstrap message has no SourceRef", () => {
const enriched = enrichThreadAttribution({
originSource: null,
messages: [
{
role: "user",
createdAt: "2026-09-25T09:18:14.418Z",
text: `## Discord conversation context
req: 147977704522645504@enricopolanski
pr: name=enricopolanski uid=147977704522645504 g=1 c=2 m=3 title=Discord

## User request
Announce it once.`,
},
],
createdAt: "2026-09-25T09:18:14.418Z",
people,
});
expect(enriched.originSource?.personId).toBe("enricopolanski");
expect(enriched.participantSummaries[0]?.personId).toBe("enricopolanski");
});

it("recovers origin from the first user message when the thread row has none", () => {
const enriched = enrichThreadAttribution({
originSource: null,
Expand Down
35 changes: 31 additions & 4 deletions packages/shared/src/sourceAttribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,34 @@ export function withMappedPerson(
};
}

/**
* Discord bot prefixes the first user message with an overlay:
* `req: 147977704522645504@enricopolanski`
* Worktree bootstrap used to persist that message without SourceRef, so
* origin stayed empty. Recover the actor from the overlay for backfill.
*/
export function parseDiscordConversationActor(
text: string,
): { readonly platformId: string; readonly displayName: string } | null {
const req = text.match(/^req:\s*(\d{5,32})@([a-z0-9][a-z0-9._-]*)/im);
if (req?.[1] !== undefined && req[2] !== undefined) {
return { platformId: req[1], displayName: req[2] };
}
const uid = text.match(/\buid=(\d{5,32})\b/i);
const name = text.match(/\bname=([a-z0-9][a-z0-9._-]*)\b/i);
if (uid?.[1] !== undefined) {
return { platformId: uid[1], displayName: name?.[1] ?? uid[1] };
}
return null;
}

export function enrichThreadAttribution(input: {
readonly originSource?: SourceRefLike | null | undefined;
readonly participantSummaries?: ReadonlyArray<ParticipantSummaryLike> | null | undefined;
readonly messages?: ReadonlyArray<{
readonly role: string;
readonly createdAt: string;
readonly text?: string | undefined;
readonly source?: SourceRefLike | undefined;
}>;
readonly createdAt: string;
Expand All @@ -258,10 +280,15 @@ export function enrichThreadAttribution(input: {
const people = input.people;
let origin = input.originSource ?? null;
if (origin === null || origin === undefined) {
const firstUser = input.messages?.find(
(message) => message.role === "user" && message.source !== undefined,
);
origin = firstUser?.source ?? null;
const firstUser = input.messages?.find((message) => message.role === "user");
if (firstUser?.source !== undefined) {
origin = firstUser.source;
} else if (firstUser?.text !== undefined) {
const actor = parseDiscordConversationActor(firstUser.text);
if (actor !== null) {
origin = { channel: "discord", actor };
}
}
}
origin = origin === null || origin === undefined ? origin : withMappedPerson(origin, people);

Expand Down