chore: merge dev

This commit is contained in:
Dax Raad 2026-06-04 10:17:17 -04:00
commit d525663171
28 changed files with 3744 additions and 3051 deletions

View file

@ -1,3 +1,4 @@
DELETE FROM `session_message`;--> statement-breakpoint
ALTER TABLE `session_message` ADD `seq` integer NOT NULL;--> statement-breakpoint
DROP INDEX IF EXISTS `session_message_session_time_created_id_idx`;--> statement-breakpoint
DROP INDEX IF EXISTS `session_message_session_type_time_created_id_idx`;--> statement-breakpoint

View file

@ -19,12 +19,14 @@ export const Plugin = PluginV2.define({
const config = yield* Config.Service
const fs = yield* FSUtil.Service
const transform = yield* command.transform()
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
return loadDirectory(fs, entry.path).pipe(
Effect.map((commands) => [{ commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) }]),
)
}).pipe(Effect.map((documents) => documents.flat()))
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
return loadDirectory(fs, entry.path).pipe(
Effect.map((commands) => [
{ commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
]),
)
}).pipe(Effect.map((documents) => documents.flat()))
yield* transform((editor) => {
for (const document of documents) {

View file

@ -23,8 +23,12 @@ export const Plugin = PluginV2.define({
yield* transform((editor) => {
for (const directory of directories) {
editor.source(new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }))
editor.source(new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }))
editor.source(
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
)
editor.source(
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }),
)
}
for (const item of items) {
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {

View file

@ -5,16 +5,10 @@ export default {
id: "20260603040000_session_message_projection_order",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer NOT NULL DEFAULT 0;`)
yield* tx.run(
`UPDATE \`session_message\` SET \`seq\` = COALESCE((SELECT \`seq\` + 1 FROM \`event\` WHERE \`event\`.\`id\` = \`session_message\`.\`id\`), 0);`,
)
const unmatched = yield* tx.get<{ count: number }>(
`SELECT COUNT(*) AS \`count\` FROM \`session_message\` WHERE \`seq\` = 0;`,
)
if ((unmatched?.count ?? 0) > 0)
return yield* Effect.die("Cannot migrate session_message projections without matching durable events")
yield* tx.run(`UPDATE \`session_message\` SET \`seq\` = \`seq\` - 1;`)
// Pre-launch Session projections were written before durable event persistence
// became unconditional, so they cannot be assigned truthful aggregate order.
yield* tx.run(`DELETE FROM \`session_message\`;`)
yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer NOT NULL;`)
yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_time_created_id_idx\`;`)
yield* tx.run(`CREATE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
yield* tx.run(

View file

@ -10,7 +10,9 @@ export const Plugin = PluginV2.define({
effect: Effect.gen(function* () {
const skill = yield* SkillV2.Service
const transform = yield* skill.transform()
const content = yield* Effect.promise(() => Bun.file(new URL("./skill/customize-opencode.md", import.meta.url)).text())
const content = yield* Effect.promise(() =>
Bun.file(new URL("./skill/customize-opencode.md", import.meta.url)).text(),
)
yield* transform((editor) => {
editor.source(

View file

@ -37,7 +37,11 @@ export const Source = Schema.Union([DirectorySource, UrlSource, EmbeddedSource])
return false
},
key: (source: DirectorySource | UrlSource | EmbeddedSource) =>
source.type === "directory" ? `directory:${source.path}` : source.type === "url" ? `url:${source.url}` : `embedded:${source.skill.name}`,
source.type === "directory"
? `directory:${source.path}`
: source.type === "url"
? `url:${source.url}`
: `embedded:${source.skill.name}`,
})),
)
export type Source = typeof Source.Type

View file

@ -79,13 +79,20 @@ describe("DatabaseMigration", () => {
)
})
test("backfills projected Session message order from durable event sequence", async () => {
test("resets incompatible projected Session messages before adding sequence order", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
yield* db.run(
sql`CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL)`,
)
yield* db.run(
sql`CREATE TABLE part (id text PRIMARY KEY, message_id text NOT NULL, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL)`,
)
yield* db.run(sql`CREATE TABLE event (id text PRIMARY KEY, seq integer NOT NULL)`)
yield* db.run(
sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, time_created integer NOT NULL, data text NOT NULL)`,
sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL)`,
)
yield* db.run(
sql`CREATE INDEX session_message_session_time_created_id_idx ON session_message (session_id, time_created, id)`,
@ -93,40 +100,40 @@ describe("DatabaseMigration", () => {
yield* db.run(
sql`CREATE INDEX session_message_session_type_time_created_id_idx ON session_message (session_id, type, time_created, id)`,
)
yield* db.run(sql`INSERT INTO event (id, seq) VALUES ('evt_z', 0), ('evt_a', 1)`)
yield* db.run(sql`INSERT INTO session (id) VALUES ('session')`)
yield* db.run(
sql`INSERT INTO session_message (id, session_id, type, time_created, data) VALUES ('evt_z', 'session', 'user', 0, '{}'), ('evt_a', 'session', 'user', 0, '{}')`,
sql`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES ('legacy_message', 'session', 1, 1, '{"role":"user"}')`,
)
yield* db.run(
sql`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES ('legacy_part', 'legacy_message', 'session', 1, 1, '{"type":"text","text":"hello"}')`,
)
yield* db.run(
sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('stale_projection', 'session', 'user', 1, 1, '{}')`,
)
yield* DatabaseMigration.applyOnly(db, [sessionMessageProjectionOrderMigration])
expect(yield* db.all(sql`SELECT id, seq FROM session_message ORDER BY seq`)).toEqual([
{ id: "evt_z", seq: 0 },
{ id: "evt_a", seq: 1 },
expect(yield* db.all(sql`SELECT id, session_id, data FROM message`)).toEqual([
{ id: "legacy_message", session_id: "session", data: '{"role":"user"}' },
])
expect(yield* db.all(sql`SELECT id, message_id, session_id, data FROM part`)).toEqual([
{
id: "legacy_part",
message_id: "legacy_message",
session_id: "session",
data: '{"type":"text","text":"hello"}',
},
])
expect(yield* db.all(sql`SELECT id FROM session_message`)).toEqual([])
yield* db.run(
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('fresh_projection', 'session', 'user', 7, 2, 2, '{}')`,
)
expect(yield* db.get(sql`SELECT id, seq FROM session_message`)).toEqual({ id: "fresh_projection", seq: 7 })
}),
)
})
test("fails projected Session message order backfill without a durable event", async () => {
await expect(
run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE event (id text PRIMARY KEY, seq integer NOT NULL)`)
yield* db.run(
sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, time_created integer NOT NULL, data text NOT NULL)`,
)
yield* db.run(
sql`INSERT INTO session_message (id, session_id, type, time_created, data) VALUES ('evt_missing', 'session', 'user', 0, '{}')`,
)
yield* DatabaseMigration.applyOnly(db, [sessionMessageProjectionOrderMigration])
}),
),
).rejects.toThrow("Cannot migrate session_message projections without matching durable events")
})
test("runs session usage backfill in order with schema changes", async () => {
await run(
Effect.gen(function* () {

View file

@ -12,10 +12,7 @@ const project = AbsolutePath.make("/repo")
const it = testEffect(
CommandV2.locationLayer.pipe(
Layer.provide(
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory }, { projectDirectory: project })),
),
Layer.succeed(Location.Service, Location.Service.of(location({ directory }, { projectDirectory: project }))),
),
),
)
@ -26,7 +23,10 @@ describe("CommandPlugin.Plugin", () => {
const command = yield* CommandV2.Service
yield* CommandPlugin.Plugin.effect.pipe(
Effect.provideService(CommandV2.Service, command),
Effect.provideService(Location.Service, Location.Service.of(location({ directory }, { projectDirectory: project }))),
Effect.provideService(
Location.Service,
Location.Service.of(location({ directory }, { projectDirectory: project })),
),
)
expect(yield* command.get("init")).toMatchObject({