@hexis-ai/engram-server 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { Session } from "@hexis-ai/engram-core";
2
- import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit } from "@hexis-ai/engram-sdk";
2
+ import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "@hexis-ai/engram-sdk";
3
3
  import { type StorageAdapter } from "../storage";
4
4
  export interface InMemoryAdapterOptions {
5
5
  /** Override for tests. Default: `p_${random}` with 8 chars. */
@@ -24,6 +24,7 @@ export declare class InMemoryAdapter implements StorageAdapter {
24
24
  }): Promise<void>;
25
25
  appendEvents(sessionId: string, events: SessionEvent[]): Promise<void>;
26
26
  getSession(sessionId: string): Promise<Session | null>;
27
+ updateSession(sessionId: string, patch: SessionUpdate): Promise<Session | null>;
27
28
  getSessionEvents(sessionId: string): Promise<SessionEvent[] | null>;
28
29
  listSessions(opts: {
29
30
  limit: number;
@@ -37,6 +37,16 @@ export class InMemoryAdapter {
37
37
  participants,
38
38
  viewable_by,
39
39
  createdAt: init.createdAt,
40
+ // status defaults to 'active' to match the Postgres column default.
41
+ status: init.status ?? "active",
42
+ ...(init.summary != null ? { summary: init.summary } : {}),
43
+ ...(init.model != null ? { model: init.model } : {}),
44
+ ...(init.trigger_conversation_id != null
45
+ ? { trigger_conversation_id: init.trigger_conversation_id }
46
+ : {}),
47
+ ...(init.trigger_event_id != null
48
+ ? { trigger_event_id: init.trigger_event_id }
49
+ : {}),
40
50
  },
41
51
  events: new Map(),
42
52
  });
@@ -66,6 +76,56 @@ export class InMemoryAdapter {
66
76
  return null;
67
77
  return foldEvents(s.row, [...s.events.values()], new Date());
68
78
  }
79
+ async updateSession(sessionId, patch) {
80
+ const s = this.sessions.get(sessionId);
81
+ if (!s)
82
+ return null;
83
+ // Patch semantics: undefined = leave alone; null = clear; value = set.
84
+ // SessionRow optional fields use `string | undefined`, so a "null"
85
+ // request collapses to `undefined` storage-side.
86
+ const next = { ...s.row };
87
+ if (patch.title !== undefined) {
88
+ if (patch.title === null)
89
+ delete next.title;
90
+ else
91
+ next.title = patch.title;
92
+ }
93
+ if (patch.channel !== undefined) {
94
+ if (patch.channel === null)
95
+ delete next.channel;
96
+ else
97
+ next.channel = patch.channel;
98
+ }
99
+ if (patch.status !== undefined) {
100
+ next.status = patch.status;
101
+ }
102
+ if (patch.summary !== undefined) {
103
+ if (patch.summary === null)
104
+ delete next.summary;
105
+ else
106
+ next.summary = patch.summary;
107
+ }
108
+ if (patch.model !== undefined) {
109
+ if (patch.model === null)
110
+ delete next.model;
111
+ else
112
+ next.model = patch.model;
113
+ }
114
+ if (patch.trigger_conversation_id !== undefined) {
115
+ if (patch.trigger_conversation_id === null)
116
+ delete next.trigger_conversation_id;
117
+ else
118
+ next.trigger_conversation_id = patch.trigger_conversation_id;
119
+ }
120
+ if (patch.trigger_event_id !== undefined) {
121
+ if (patch.trigger_event_id === null)
122
+ delete next.trigger_event_id;
123
+ else
124
+ next.trigger_event_id = patch.trigger_event_id;
125
+ }
126
+ s.row = next;
127
+ return foldEvents(s.row, [...s.events.values()], new Date());
128
+ }
69
129
  async getSessionEvents(sessionId) {
70
130
  const s = this.sessions.get(sessionId);
71
131
  if (!s)
@@ -1,5 +1,5 @@
1
1
  import type { Session } from "@hexis-ai/engram-core";
2
- import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit } from "@hexis-ai/engram-sdk";
2
+ import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "@hexis-ai/engram-sdk";
3
3
  import { type StorageAdapter } from "../storage";
4
4
  /**
5
5
  * Minimal subset of `postgres` driver's tagged-template surface that this
@@ -37,6 +37,7 @@ export declare class PostgresAdapter implements StorageAdapter {
37
37
  }): Promise<void>;
38
38
  appendEvents(sessionId: string, events: SessionEvent[]): Promise<void>;
39
39
  getSession(sessionId: string): Promise<Session | null>;
40
+ updateSession(sessionId: string, patch: SessionUpdate): Promise<Session | null>;
40
41
  getSessionEvents(sessionId: string): Promise<SessionEvent[] | null>;
41
42
  listSessions(opts: {
42
43
  limit: number;
@@ -38,8 +38,14 @@ export class PostgresAdapter {
38
38
  const viewableBy = init.viewable_by
39
39
  ? Array.from(new Set([...init.viewable_by, ...participants]))
40
40
  : [...participants];
41
+ // status defaults to 'active' server-side via the column default;
42
+ // a null value here is treated as "use the default" by NULL-fallback.
41
43
  await this.sql `
42
- INSERT INTO engram_sessions (workspace_id, id, title, channel, participants, viewable_by, created_at)
44
+ INSERT INTO engram_sessions (
45
+ workspace_id, id, title, channel, participants, viewable_by,
46
+ created_at, status, summary, model,
47
+ trigger_conversation_id, trigger_event_id
48
+ )
43
49
  VALUES (
44
50
  ${this.workspaceId},
45
51
  ${init.id},
@@ -47,7 +53,12 @@ export class PostgresAdapter {
47
53
  ${init.channel ?? null},
48
54
  ${participants},
49
55
  ${viewableBy},
50
- ${init.createdAt}
56
+ ${init.createdAt},
57
+ ${init.status ?? "active"},
58
+ ${init.summary ?? null},
59
+ ${init.model ?? null},
60
+ ${init.trigger_conversation_id ?? null},
61
+ ${init.trigger_event_id ?? null}
51
62
  )
52
63
  ON CONFLICT (workspace_id, id) DO NOTHING
53
64
  `;
@@ -87,28 +98,52 @@ export class PostgresAdapter {
87
98
  }
88
99
  async getSession(sessionId) {
89
100
  const rows = await this.sql `
90
- SELECT id, title, channel, participants, viewable_by, created_at
101
+ SELECT id, title, channel, participants, viewable_by, created_at,
102
+ status, summary, model,
103
+ trigger_conversation_id, trigger_event_id
91
104
  FROM engram_sessions
92
105
  WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
93
106
  LIMIT 1
94
107
  `;
95
108
  if (rows.length === 0)
96
109
  return null;
97
- const r = rows[0];
98
110
  const events = await this.sql `
99
111
  SELECT payload FROM engram_events
100
112
  WHERE workspace_id = ${this.workspaceId} AND session_id = ${sessionId}
101
113
  ORDER BY seq
102
114
  `;
103
- const row = {
104
- id: r.id,
105
- ...(r.title ? { title: r.title } : {}),
106
- ...(r.channel ? { channel: r.channel } : {}),
107
- participants: r.participants,
108
- viewable_by: r.viewable_by,
109
- createdAt: typeof r.created_at === "string" ? r.created_at : r.created_at.toISOString(),
110
- };
111
- return foldEvents(row, events.map((e) => e.payload), new Date());
115
+ return foldEvents(toSessionRow(rows[0]), events.map((e) => e.payload), new Date());
116
+ }
117
+ async updateSession(sessionId, patch) {
118
+ // Translate JS undefined "no change" via a per-column "provided"
119
+ // flag the SQL evaluates with CASE WHEN. null in patch becomes a
120
+ // real null in the DB (clear), value becomes a set.
121
+ const titleProvided = patch.title !== undefined;
122
+ const channelProvided = patch.channel !== undefined;
123
+ const statusProvided = patch.status !== undefined;
124
+ const summaryProvided = patch.summary !== undefined;
125
+ const modelProvided = patch.model !== undefined;
126
+ const tcIdProvided = patch.trigger_conversation_id !== undefined;
127
+ const teIdProvided = patch.trigger_event_id !== undefined;
128
+ const rows = await this.sql `
129
+ UPDATE engram_sessions SET
130
+ title = CASE WHEN ${titleProvided} THEN ${patch.title ?? null} ELSE title END,
131
+ channel = CASE WHEN ${channelProvided} THEN ${patch.channel ?? null} ELSE channel END,
132
+ status = CASE WHEN ${statusProvided} THEN ${patch.status ?? "active"} ELSE status END,
133
+ summary = CASE WHEN ${summaryProvided} THEN ${patch.summary ?? null} ELSE summary END,
134
+ model = CASE WHEN ${modelProvided} THEN ${patch.model ?? null} ELSE model END,
135
+ trigger_conversation_id = CASE WHEN ${tcIdProvided}
136
+ THEN ${patch.trigger_conversation_id ?? null}
137
+ ELSE trigger_conversation_id END,
138
+ trigger_event_id = CASE WHEN ${teIdProvided}
139
+ THEN ${patch.trigger_event_id ?? null}
140
+ ELSE trigger_event_id END
141
+ WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
142
+ RETURNING id
143
+ `;
144
+ if (rows.length === 0)
145
+ return null;
146
+ return this.getSession(sessionId);
112
147
  }
113
148
  async getSessionEvents(sessionId) {
114
149
  const rows = await this.sql `
@@ -358,6 +393,29 @@ function toPersonInfo(r) {
358
393
  updated_at: toIso(r.updated_at),
359
394
  };
360
395
  }
396
+ function toSessionRow(r) {
397
+ return {
398
+ id: r.id,
399
+ ...(r.title ? { title: r.title } : {}),
400
+ ...(r.channel ? { channel: r.channel } : {}),
401
+ participants: r.participants,
402
+ viewable_by: r.viewable_by,
403
+ createdAt: typeof r.created_at === "string"
404
+ ? r.created_at
405
+ : r.created_at.toISOString(),
406
+ ...(r.status === "active" || r.status === "idle" || r.status === "completed"
407
+ ? { status: r.status }
408
+ : {}),
409
+ ...(r.summary !== null ? { summary: r.summary } : {}),
410
+ ...(r.model !== null ? { model: r.model } : {}),
411
+ ...(r.trigger_conversation_id !== null
412
+ ? { trigger_conversation_id: r.trigger_conversation_id }
413
+ : {}),
414
+ ...(r.trigger_event_id !== null
415
+ ? { trigger_event_id: r.trigger_event_id }
416
+ : {}),
417
+ };
418
+ }
361
419
  function toAliasInfo(r) {
362
420
  const toIso = (v) => (typeof v === "string" ? v : v.toISOString());
363
421
  // last_used is a DATE column; postgres-js returns it as a Date at UTC
@@ -0,0 +1,2 @@
1
+ export declare const name = "0004-schema-completion";
2
+ export declare const sql = "\n-- Wave 1 of the engram-as-sole-memory-source migration. Adds the\n-- richer columns monet needs to retire its in-tree memory tables, and\n-- tightens a few earlier schema choices before launch freezes the wire.\n\n-- Persons gain organizational fields. Previously engram was the\n-- retrieval primitive only, so role/team/source were intentionally\n-- absent (\"HR data belongs upstream\"). Now that engram is monet's\n-- canonical memory layer, those facts have to live here.\nALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS role TEXT;\nALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS team TEXT;\nALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS source TEXT;\nUPDATE engram_persons SET source = 'auto' WHERE source IS NULL;\nALTER TABLE engram_persons ALTER COLUMN source SET DEFAULT 'auto';\nALTER TABLE engram_persons ALTER COLUMN source SET NOT NULL;\n\n-- Sessions gain monet-derived metadata so the host's conversation_*\n-- columns disappear.\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS status TEXT;\nUPDATE engram_sessions SET status = 'active' WHERE status IS NULL;\nALTER TABLE engram_sessions ALTER COLUMN status SET DEFAULT 'active';\nALTER TABLE engram_sessions ALTER COLUMN status SET NOT NULL;\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS summary TEXT;\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS model TEXT;\n-- A session can be triggered by another conversation (manual fan-out)\n-- or by an external event id (calendar event in monet, etc.). No FK on\n-- trigger_event_id \u2014 it crosses systems.\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_conversation_id TEXT;\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_event_id TEXT;\n\n-- Identities tightening \u2014 last chance to fix these before launch.\n-- is_primary: nullable BOOLEAN where null/false were ambiguous \u2192\n-- pin to NOT NULL DEFAULT false.\n-- unlinked_at: new \u2014 represents \"this external connection was\n-- revoked\", which the append-via-upsert model previously couldn't\n-- express. nullable; non-null means the identity is no longer\n-- authoritative.\n-- linked_at: was DATE (lossy). Widen to TIMESTAMPTZ to preserve\n-- full timing. The cast is a no-op when already TIMESTAMPTZ.\nUPDATE engram_identities SET is_primary = false WHERE is_primary IS NULL;\nALTER TABLE engram_identities ALTER COLUMN is_primary SET DEFAULT false;\nALTER TABLE engram_identities ALTER COLUMN is_primary SET NOT NULL;\nALTER TABLE engram_identities ADD COLUMN IF NOT EXISTS unlinked_at TIMESTAMPTZ;\nALTER TABLE engram_identities ALTER COLUMN linked_at TYPE TIMESTAMPTZ\n USING linked_at::TIMESTAMPTZ;\n\n-- Index supporting \"active identities only\" queries \u2014 common after\n-- adding unlinked_at since most callers want non-revoked rows.\nCREATE INDEX IF NOT EXISTS idx_engram_identities_active_service_external\n ON engram_identities (workspace_id, service, external_id)\n WHERE unlinked_at IS NULL;\n";
@@ -0,0 +1,53 @@
1
+ export const name = "0004-schema-completion";
2
+ export const sql = `
3
+ -- Wave 1 of the engram-as-sole-memory-source migration. Adds the
4
+ -- richer columns monet needs to retire its in-tree memory tables, and
5
+ -- tightens a few earlier schema choices before launch freezes the wire.
6
+
7
+ -- Persons gain organizational fields. Previously engram was the
8
+ -- retrieval primitive only, so role/team/source were intentionally
9
+ -- absent ("HR data belongs upstream"). Now that engram is monet's
10
+ -- canonical memory layer, those facts have to live here.
11
+ ALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS role TEXT;
12
+ ALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS team TEXT;
13
+ ALTER TABLE engram_persons ADD COLUMN IF NOT EXISTS source TEXT;
14
+ UPDATE engram_persons SET source = 'auto' WHERE source IS NULL;
15
+ ALTER TABLE engram_persons ALTER COLUMN source SET DEFAULT 'auto';
16
+ ALTER TABLE engram_persons ALTER COLUMN source SET NOT NULL;
17
+
18
+ -- Sessions gain monet-derived metadata so the host's conversation_*
19
+ -- columns disappear.
20
+ ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS status TEXT;
21
+ UPDATE engram_sessions SET status = 'active' WHERE status IS NULL;
22
+ ALTER TABLE engram_sessions ALTER COLUMN status SET DEFAULT 'active';
23
+ ALTER TABLE engram_sessions ALTER COLUMN status SET NOT NULL;
24
+ ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS summary TEXT;
25
+ ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS model TEXT;
26
+ -- A session can be triggered by another conversation (manual fan-out)
27
+ -- or by an external event id (calendar event in monet, etc.). No FK on
28
+ -- trigger_event_id — it crosses systems.
29
+ ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_conversation_id TEXT;
30
+ ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_event_id TEXT;
31
+
32
+ -- Identities tightening — last chance to fix these before launch.
33
+ -- is_primary: nullable BOOLEAN where null/false were ambiguous →
34
+ -- pin to NOT NULL DEFAULT false.
35
+ -- unlinked_at: new — represents "this external connection was
36
+ -- revoked", which the append-via-upsert model previously couldn't
37
+ -- express. nullable; non-null means the identity is no longer
38
+ -- authoritative.
39
+ -- linked_at: was DATE (lossy). Widen to TIMESTAMPTZ to preserve
40
+ -- full timing. The cast is a no-op when already TIMESTAMPTZ.
41
+ UPDATE engram_identities SET is_primary = false WHERE is_primary IS NULL;
42
+ ALTER TABLE engram_identities ALTER COLUMN is_primary SET DEFAULT false;
43
+ ALTER TABLE engram_identities ALTER COLUMN is_primary SET NOT NULL;
44
+ ALTER TABLE engram_identities ADD COLUMN IF NOT EXISTS unlinked_at TIMESTAMPTZ;
45
+ ALTER TABLE engram_identities ALTER COLUMN linked_at TYPE TIMESTAMPTZ
46
+ USING linked_at::TIMESTAMPTZ;
47
+
48
+ -- Index supporting "active identities only" queries — common after
49
+ -- adding unlinked_at since most callers want non-revoked rows.
50
+ CREATE INDEX IF NOT EXISTS idx_engram_identities_active_service_external
51
+ ON engram_identities (workspace_id, service, external_id)
52
+ WHERE unlinked_at IS NULL;
53
+ `;
@@ -1,6 +1,7 @@
1
1
  import * as m0001 from "./0001-baseline";
2
2
  import * as m0002 from "./0002-aliases";
3
3
  import * as m0003 from "./0003-identities";
4
+ import * as m0004 from "./0004-schema-completion";
4
5
  /**
5
6
  * Schema migrations, applied in array order. Add a new file under
6
7
  * `migrations/NNNN-<slug>.ts` exporting `name` and `sql`, then append it
@@ -12,4 +13,5 @@ export const MIGRATIONS = [
12
13
  { name: m0001.name, sql: m0001.sql },
13
14
  { name: m0002.name, sql: m0002.sql },
14
15
  { name: m0003.name, sql: m0003.sql },
16
+ { name: m0004.name, sql: m0004.sql },
15
17
  ];
package/dist/openapi.js CHANGED
@@ -128,6 +128,17 @@ function buildPaths() {
128
128
  "401": res("認証エラー"),
129
129
  },
130
130
  },
131
+ patch: {
132
+ summary: "セッションのメタデータ(title / channel / status / summary / model / trigger_*)を部分更新する。",
133
+ parameters: [pathParam("id", "セッション id。")],
134
+ requestBody: jsonBody("SessionUpdate"),
135
+ responses: {
136
+ "200": res("更新後のセッションのエンベロープ"),
137
+ "400": res("リクエストボディが不正"),
138
+ "404": res("セッションが見つからない"),
139
+ "401": res("認証エラー"),
140
+ },
141
+ },
131
142
  }),
132
143
  "/v1/sessions/{id}/events": tagged("Sessions", {
133
144
  post: {
@@ -3,10 +3,11 @@ import type { Env } from "../context";
3
3
  import { type RouteConfig } from "./helpers";
4
4
  /**
5
5
  * Session routes. Mount under `/v1`:
6
- * POST /v1/sessions create a session
7
- * POST /v1/sessions/:id/events append events
8
- * GET /v1/sessions/:id fetch one session + its persons map
9
- * GET /v1/sessions/:id/events fetch the raw, ordered event log
10
- * GET /v1/sessions list recent sessions + persons map
6
+ * POST /v1/sessions create a session
7
+ * PATCH /v1/sessions/:id update session-level metadata
8
+ * POST /v1/sessions/:id/events append events
9
+ * GET /v1/sessions/:id fetch one session + its persons map
10
+ * GET /v1/sessions/:id/events fetch the raw, ordered event log
11
+ * GET /v1/sessions list recent sessions + persons map
11
12
  */
12
13
  export declare function sessionsRoutes(cfg: RouteConfig): Hono<Env>;
@@ -1,13 +1,14 @@
1
1
  import { Hono } from "hono";
2
- import { eventBatchSchema, parseJsonBody, sessionInitSchema } from "../schemas";
2
+ import { eventBatchSchema, parseJsonBody, sessionInitSchema, sessionUpdateSchema, } from "../schemas";
3
3
  import { clampLimit, resolvePersonMap } from "./helpers";
4
4
  /**
5
5
  * Session routes. Mount under `/v1`:
6
- * POST /v1/sessions create a session
7
- * POST /v1/sessions/:id/events append events
8
- * GET /v1/sessions/:id fetch one session + its persons map
9
- * GET /v1/sessions/:id/events fetch the raw, ordered event log
10
- * GET /v1/sessions list recent sessions + persons map
6
+ * POST /v1/sessions create a session
7
+ * PATCH /v1/sessions/:id update session-level metadata
8
+ * POST /v1/sessions/:id/events append events
9
+ * GET /v1/sessions/:id fetch one session + its persons map
10
+ * GET /v1/sessions/:id/events fetch the raw, ordered event log
11
+ * GET /v1/sessions list recent sessions + persons map
11
12
  */
12
13
  export function sessionsRoutes(cfg) {
13
14
  const app = new Hono();
@@ -33,6 +34,17 @@ export function sessionsRoutes(cfg) {
33
34
  }
34
35
  return c.body(null, 204);
35
36
  });
37
+ app.patch("/sessions/:id", async (c) => {
38
+ const id = c.req.param("id");
39
+ const body = await parseJsonBody(c, sessionUpdateSchema);
40
+ if (body instanceof Response)
41
+ return body;
42
+ const session = await c.var.ctx.storage.updateSession(id, body);
43
+ if (!session)
44
+ return c.json({ error: "session_not_found" }, 404);
45
+ const persons = await resolvePersonMap(c.var.ctx.storage, [session]);
46
+ return c.json({ session, persons });
47
+ });
36
48
  app.get("/sessions/:id", async (c) => {
37
49
  const id = c.req.param("id");
38
50
  const s = await c.var.ctx.storage.getSession(id);
package/dist/schemas.d.ts CHANGED
@@ -15,6 +15,28 @@ export declare const sessionInitSchema: z.ZodObject<{
15
15
  channel: z.ZodOptional<z.ZodString>;
16
16
  participants: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
17
  viewable_by: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
+ status: z.ZodOptional<z.ZodEnum<{
19
+ active: "active";
20
+ idle: "idle";
21
+ completed: "completed";
22
+ }>>;
23
+ summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
25
+ trigger_conversation_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
26
+ trigger_event_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
27
+ }, z.core.$strip>;
28
+ export declare const sessionUpdateSchema: z.ZodObject<{
29
+ title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
31
+ status: z.ZodOptional<z.ZodEnum<{
32
+ active: "active";
33
+ idle: "idle";
34
+ completed: "completed";
35
+ }>>;
36
+ summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
37
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
38
+ trigger_conversation_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
39
+ trigger_event_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
18
40
  }, z.core.$strip>;
19
41
  export declare const sessionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
20
42
  type: z.ZodLiteral<"step">;
package/dist/schemas.js CHANGED
@@ -15,6 +15,20 @@ export const sessionInitSchema = z.object({
15
15
  channel: z.string().optional(),
16
16
  participants: z.array(z.string()).optional(),
17
17
  viewable_by: z.array(z.string()).optional(),
18
+ status: z.enum(["active", "idle", "completed"]).optional(),
19
+ summary: z.string().nullable().optional(),
20
+ model: z.string().nullable().optional(),
21
+ trigger_conversation_id: z.string().nullable().optional(),
22
+ trigger_event_id: z.string().nullable().optional(),
23
+ });
24
+ export const sessionUpdateSchema = z.object({
25
+ title: z.string().nullable().optional(),
26
+ channel: z.string().nullable().optional(),
27
+ status: z.enum(["active", "idle", "completed"]).optional(),
28
+ summary: z.string().nullable().optional(),
29
+ model: z.string().nullable().optional(),
30
+ trigger_conversation_id: z.string().nullable().optional(),
31
+ trigger_event_id: z.string().nullable().optional(),
18
32
  });
19
33
  const stepEventSchema = z.object({
20
34
  type: z.literal("step"),
package/dist/server.js CHANGED
@@ -52,7 +52,7 @@ export function createServer(opts) {
52
52
  ok: true,
53
53
  routes: {
54
54
  sessions: "POST/GET /v1/sessions",
55
- sessionById: "GET /v1/sessions/:id",
55
+ sessionById: "GET/PATCH /v1/sessions/:id",
56
56
  events: "POST /v1/sessions/:id/events",
57
57
  sessionEvents: "GET /v1/sessions/:id/events",
58
58
  search: "POST /v1/search",
package/dist/storage.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Session } from "@hexis-ai/engram-core";
2
- import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit } from "@hexis-ai/engram-sdk";
2
+ import type { AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, PersonCreate, PersonInfo, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "@hexis-ai/engram-sdk";
3
3
  /**
4
4
  * Storage adapter interface. Each implementation owns persistence for
5
5
  * a single workspace's sessions and persons. Multi-tenancy is the host's
@@ -18,6 +18,13 @@ export interface StorageAdapter {
18
18
  appendEvents(sessionId: string, events: SessionEvent[]): Promise<void>;
19
19
  /** Materialize a session (events folded into Session shape). */
20
20
  getSession(sessionId: string): Promise<Session | null>;
21
+ /**
22
+ * Patch session-level metadata (title / channel / status / summary /
23
+ * model / trigger_*). Each field updates only when present in `patch`
24
+ * — `undefined` leaves the column alone, `null` clears it. Returns
25
+ * the materialized session post-update, or `null` if unknown.
26
+ */
27
+ updateSession(sessionId: string, patch: SessionUpdate): Promise<Session | null>;
21
28
  /**
22
29
  * Raw event log for a session, ordered by `seq`. Unlike `getSession`,
23
30
  * this does not fold events — callers get per-event timestamps and the
@@ -102,5 +109,10 @@ export interface SessionRow {
102
109
  participants: string[];
103
110
  viewable_by: string[];
104
111
  createdAt: string;
112
+ status?: "active" | "idle" | "completed";
113
+ summary?: string;
114
+ model?: string;
115
+ trigger_conversation_id?: string;
116
+ trigger_event_id?: string;
105
117
  }
106
118
  export declare function foldEvents(row: SessionRow, events: SessionEvent[], now: Date): Session;
package/dist/storage.js CHANGED
@@ -21,9 +21,19 @@ export function foldEvents(row, events, now) {
21
21
  return {
22
22
  id: row.id,
23
23
  ...(title ? { title } : {}),
24
+ ...(row.channel ? { channel: row.channel } : {}),
24
25
  steps,
25
26
  daysAgo,
26
27
  ...(participants.size > 0 ? { participants: [...participants] } : {}),
27
28
  ...(viewableSet.size > 0 ? { viewable_by: [...viewableSet] } : {}),
29
+ ...(row.status ? { status: row.status } : {}),
30
+ ...(row.summary !== undefined ? { summary: row.summary } : {}),
31
+ ...(row.model !== undefined ? { model: row.model } : {}),
32
+ ...(row.trigger_conversation_id !== undefined
33
+ ? { trigger_conversation_id: row.trigger_conversation_id }
34
+ : {}),
35
+ ...(row.trigger_event_id !== undefined
36
+ ? { trigger_event_id: row.trigger_event_id }
37
+ : {}),
28
38
  };
29
39
  }
package/openapi.json CHANGED
@@ -85,6 +85,54 @@
85
85
  "items": {
86
86
  "type": "string"
87
87
  }
88
+ },
89
+ "status": {
90
+ "type": "string",
91
+ "enum": [
92
+ "active",
93
+ "idle",
94
+ "completed"
95
+ ]
96
+ },
97
+ "summary": {
98
+ "anyOf": [
99
+ {
100
+ "type": "string"
101
+ },
102
+ {
103
+ "type": "null"
104
+ }
105
+ ]
106
+ },
107
+ "model": {
108
+ "anyOf": [
109
+ {
110
+ "type": "string"
111
+ },
112
+ {
113
+ "type": "null"
114
+ }
115
+ ]
116
+ },
117
+ "trigger_conversation_id": {
118
+ "anyOf": [
119
+ {
120
+ "type": "string"
121
+ },
122
+ {
123
+ "type": "null"
124
+ }
125
+ ]
126
+ },
127
+ "trigger_event_id": {
128
+ "anyOf": [
129
+ {
130
+ "type": "string"
131
+ },
132
+ {
133
+ "type": "null"
134
+ }
135
+ ]
88
136
  }
89
137
  },
90
138
  "additionalProperties": false
@@ -536,6 +584,47 @@
536
584
  "tags": [
537
585
  "Sessions"
538
586
  ]
587
+ },
588
+ "patch": {
589
+ "summary": "セッションのメタデータ(title / channel / status / summary / model / trigger_*)を部分更新する。",
590
+ "parameters": [
591
+ {
592
+ "name": "id",
593
+ "in": "path",
594
+ "required": true,
595
+ "schema": {
596
+ "type": "string"
597
+ },
598
+ "description": "セッション id。"
599
+ }
600
+ ],
601
+ "requestBody": {
602
+ "required": true,
603
+ "content": {
604
+ "application/json": {
605
+ "schema": {
606
+ "$ref": "#/components/schemas/SessionUpdate"
607
+ }
608
+ }
609
+ }
610
+ },
611
+ "responses": {
612
+ "200": {
613
+ "description": "更新後のセッションのエンベロープ"
614
+ },
615
+ "400": {
616
+ "description": "リクエストボディが不正"
617
+ },
618
+ "401": {
619
+ "description": "認証エラー"
620
+ },
621
+ "404": {
622
+ "description": "セッションが見つからない"
623
+ }
624
+ },
625
+ "tags": [
626
+ "Sessions"
627
+ ]
539
628
  }
540
629
  },
541
630
  "/v1/sessions/{id}/events": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexis-ai/engram-server",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Engram server: ingest agent session events, persist via a pluggable adapter, expose search.",
5
5
  "keywords": [
6
6
  "engram",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@hexis-ai/engram-core": "^0.1.5",
53
- "@hexis-ai/engram-sdk": "^0.6.0",
53
+ "@hexis-ai/engram-sdk": "^0.7.0",
54
54
  "hono": "^4.6.0",
55
55
  "zod": "^4.0.0"
56
56
  },