@opencode-ai/protocol 0.0.0-next-14722 → 0.0.0-next-14730

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.
@@ -4,9 +4,10 @@ import { PromptInput } from "@opencode-ai/schema/prompt-input";
4
4
  import { Session } from "@opencode-ai/schema/session";
5
5
  import { SessionContextEntry } from "@opencode-ai/schema/session-context-entry";
6
6
  import { Project } from "@opencode-ai/schema/project";
7
- import { AbsolutePath, NonNegativeInt, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema";
7
+ import { AbsolutePath, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema";
8
+ import { Event } from "@opencode-ai/schema/event";
8
9
  import { Workspace } from "@opencode-ai/schema/workspace";
9
- import { Context, Effect, Encoding, Result, Schema, Struct } from "effect";
10
+ import { Context, Effect, Encoding, Result, Schema, SchemaGetter, Struct } from "effect";
10
11
  import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi";
11
12
  import { ConflictError, CommandEvaluationError, CommandNotFoundError, InvalidCursorError, InvalidRequestError, MessageNotFoundError, ServiceUnavailableError, SessionBusyError, SessionNotFoundError, SkillNotFoundError, UnknownError, } from "../errors.js";
12
13
  import { Agent } from "@opencode-ai/schema/agent";
@@ -14,6 +15,7 @@ import { Model } from "@opencode-ai/schema/model";
14
15
  import { Location } from "@opencode-ai/schema/location";
15
16
  import { Revert } from "@opencode-ai/schema/revert";
16
17
  import { SessionEvent } from "@opencode-ai/schema/session-event";
18
+ import { EventLog } from "@opencode-ai/schema/event-log";
17
19
  const SessionsQueryFields = {
18
20
  workspace: Workspace.ID.pipe(Schema.optional),
19
21
  limit: Schema.NumberFromString.pipe(Schema.decodeTo(PositiveInt), Schema.optional).annotate({
@@ -62,11 +64,14 @@ export const SessionsCursor = Schema.String.pipe(Schema.brand("SessionsCursor"),
62
64
  const SessionActive = Schema.Struct({
63
65
  type: Schema.Literal("running"),
64
66
  }).annotate({ identifier: "SessionActive" });
65
- const SessionHistoryLimit = PositiveInt.check(Schema.isLessThanOrEqualTo(100));
66
- export const SessionHistoryQuery = Schema.Struct({
67
- limit: Schema.NumberFromString.pipe(Schema.decodeTo(SessionHistoryLimit), Schema.optional),
68
- after: Schema.NumberFromString.pipe(Schema.decodeTo(NonNegativeInt), Schema.optional),
67
+ const SessionWatermarks = Schema.Record(Session.ID, Event.Seq).annotate({
68
+ identifier: "SessionWatermarks",
69
+ description: "Durable log seq each session's snapshot was computed at. Attach a live log read after the watermark to compose fetch and stream gap-free; apply a snapshot only where its watermark is at or beyond already-applied events. Sessions without durable events are absent.",
69
70
  });
71
+ const BooleanFromString = Schema.Literals(["true", "false"]).pipe(Schema.decodeTo(Schema.Boolean, {
72
+ decode: SchemaGetter.transform((value) => value === "true"),
73
+ encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
74
+ }));
70
75
  const SessionsQueryCursor = SessionsCursor.annotate({
71
76
  description: "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response.",
72
77
  });
@@ -82,6 +87,7 @@ export const makeSessionGroup = (sessionLocationMiddleware) => HttpApiGroup.make
82
87
  query: SessionsQuery,
83
88
  success: Schema.Struct({
84
89
  data: Schema.Array(Session.Info),
90
+ watermarks: SessionWatermarks,
85
91
  cursor: Schema.Struct({
86
92
  previous: SessionsCursor.pipe(Schema.optional),
87
93
  next: SessionsCursor.pipe(Schema.optional),
@@ -107,11 +113,11 @@ export const makeSessionGroup = (sessionLocationMiddleware) => HttpApiGroup.make
107
113
  description: "Create a session at the requested location.",
108
114
  })))
109
115
  .add(HttpApiEndpoint.get("session.active", "/api/session/active", {
110
- success: Schema.Struct({ data: Schema.Record(Session.ID, SessionActive) }),
116
+ success: Schema.Struct({ data: Schema.Record(Session.ID, SessionActive), watermarks: SessionWatermarks }),
111
117
  }).annotateMerge(OpenApi.annotations({
112
118
  identifier: "v2.session.active",
113
119
  summary: "List active sessions",
114
- description: "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive.",
120
+ description: "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive. Watermarks are the durable log positions read alongside the activity snapshot; activity itself is process state, so the pairing is advisory rather than transactional.",
115
121
  })))
116
122
  .add(HttpApiEndpoint.get("session.get", "/api/session/:sessionID", {
117
123
  params: { sessionID: Session.ID },
@@ -336,34 +342,22 @@ export const makeSessionGroup = (sessionLocationMiddleware) => HttpApiGroup.make
336
342
  summary: "Remove context entry",
337
343
  description: "Remove one context entry; the removal is announced to the model at the next turn boundary.",
338
344
  })))
339
- .add(HttpApiEndpoint.get("session.history", "/api/session/:sessionID/history", {
340
- params: { sessionID: Session.ID },
341
- query: SessionHistoryQuery,
342
- success: Schema.Struct({
343
- data: Schema.Array(SessionEvent.Durable),
344
- hasMore: Schema.Boolean,
345
- }).annotate({ identifier: "SessionHistory" }),
346
- error: SessionNotFoundError,
347
- })
348
- .middleware(sessionLocationMiddleware)
349
- .annotateMerge(OpenApi.annotations({
350
- identifier: "v2.session.history",
351
- summary: "Get session history",
352
- description: "Read one finite page of public durable Session events after an exclusive aggregate sequence. Newly committed events may appear on later pages.",
353
- })))
354
- .add(HttpApiEndpoint.get("session.events", "/api/session/:sessionID/event", {
345
+ .add(HttpApiEndpoint.get("session.log", "/api/session/:sessionID/log", {
355
346
  params: { sessionID: Session.ID },
356
347
  query: {
357
- after: Schema.NumberFromString.pipe(Schema.decodeTo(NonNegativeInt), Schema.optional),
348
+ after: Schema.NumberFromString.pipe(Schema.decodeTo(Event.Seq), Schema.optional),
349
+ follow: BooleanFromString.pipe(Schema.optional),
358
350
  },
359
- success: HttpApiSchema.StreamSse({ data: SessionEvent.Durable }),
351
+ success: HttpApiSchema.StreamSse({
352
+ data: Schema.Union([SessionEvent.Durable, EventLog.CaughtUp]).annotate({ identifier: "SessionLogItem" }),
353
+ }),
360
354
  error: SessionNotFoundError,
361
355
  })
362
356
  .middleware(sessionLocationMiddleware)
363
357
  .annotateMerge(OpenApi.annotations({
364
- identifier: "v2.session.events",
365
- summary: "Subscribe to session events",
366
- description: "Replay durable events after an aggregate sequence, then continue with new durable events.",
358
+ identifier: "v2.session.log",
359
+ summary: "Read the session log",
360
+ description: "Durable, ordered, gap-free read of public session events after an exclusive aggregate sequence. Emits a caught-up marker once the replay reaches the end of the log, then completes; with follow=true it continues with live events instead. The only event API that promises reliability: attach after a snapshot watermark to compose fetch and stream without a race window.",
367
361
  })))
368
362
  .add(HttpApiEndpoint.post("session.interrupt", "/api/session/:sessionID/interrupt", {
369
363
  params: { sessionID: Session.ID },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode-ai/protocol",
4
- "version": "0.0.0-next-14722",
4
+ "version": "0.0.0-next-14730",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "typecheck": "tsgo --noEmit"
27
27
  },
28
28
  "dependencies": {
29
- "@opencode-ai/schema": "0.0.0-next-14722",
29
+ "@opencode-ai/schema": "0.0.0-next-14730",
30
30
  "effect": "4.0.0-beta.83"
31
31
  },
32
32
  "devDependencies": {
@@ -1,92 +0,0 @@
1
- import { Location } from "@opencode-ai/schema/location";
2
- import { Context, Schema } from "effect";
3
- import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema } from "effect/unstable/httpapi";
4
- import { QuestionNotFoundError, SessionNotFoundError } from "../errors.js";
5
- export declare const makeQuestionGroup: <LocationId extends HttpApiMiddleware.AnyId, LocationService, SessionLocationId extends HttpApiMiddleware.AnyId, SessionLocationService>(locationMiddleware: Context.Key<LocationId, LocationService>, sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>) => HttpApiGroup.HttpApiGroup<"server.question", HttpApiEndpoint.HttpApiEndpoint<"question.request.list", "GET", "/api/question/request", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<Schema.Struct<{
6
- readonly location: Schema.optional<Schema.Struct<{
7
- readonly directory: Schema.optional<Schema.String>;
8
- readonly workspace: Schema.optional<Schema.String>;
9
- }>>;
10
- }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
11
- readonly location: typeof Location.Info;
12
- readonly data: Schema.$Array<Schema.Struct<{
13
- readonly id: Schema.brand<Schema.String, "QuestionV2.ID"> & {
14
- create: () => string & import("effect/Brand").Brand<"QuestionV2.ID">;
15
- ascending: (id?: string) => string & import("effect/Brand").Brand<"QuestionV2.ID">;
16
- };
17
- readonly sessionID: Schema.brand<Schema.String, "SessionID"> & {
18
- create: () => string & import("effect/Brand").Brand<"SessionID">;
19
- descending: (id?: string) => string & import("effect/Brand").Brand<"SessionID">;
20
- };
21
- readonly questions: Schema.$Array<Schema.Struct<{
22
- readonly custom: Schema.decodeTo<Schema.optional<Schema.toType<Schema.Boolean>>, Schema.optionalKey<Schema.Boolean>, never, never>;
23
- readonly question: Schema.String;
24
- readonly header: Schema.String;
25
- readonly options: Schema.$Array<Schema.Struct<{
26
- readonly label: Schema.String;
27
- readonly description: Schema.String;
28
- }>>;
29
- readonly multiple: Schema.decodeTo<Schema.optional<Schema.toType<Schema.Boolean>>, Schema.optionalKey<Schema.Boolean>, never, never>;
30
- }>>;
31
- readonly tool: Schema.decodeTo<Schema.optional<Schema.toType<Schema.Struct<{
32
- readonly messageID: Schema.String;
33
- readonly callID: Schema.String;
34
- }>>>, Schema.optionalKey<Schema.Struct<{
35
- readonly messageID: Schema.String;
36
- readonly callID: Schema.String;
37
- }>>, never, never>;
38
- }>>;
39
- }>>, HttpApiEndpoint.Json<never>, LocationId, HttpApiMiddleware.Requires<LocationId>> | HttpApiEndpoint.HttpApiEndpoint<"session.question.list", "GET", "/api/session/:sessionID/question", HttpApiEndpoint.StringTree<Schema.Struct<{
40
- sessionID: Schema.brand<Schema.String, "SessionID"> & {
41
- create: () => string & import("effect/Brand").Brand<"SessionID">;
42
- descending: (id?: string) => string & import("effect/Brand").Brand<"SessionID">;
43
- };
44
- }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
45
- readonly data: Schema.$Array<Schema.Struct<{
46
- readonly id: Schema.brand<Schema.String, "QuestionV2.ID"> & {
47
- create: () => string & import("effect/Brand").Brand<"QuestionV2.ID">;
48
- ascending: (id?: string) => string & import("effect/Brand").Brand<"QuestionV2.ID">;
49
- };
50
- readonly sessionID: Schema.brand<Schema.String, "SessionID"> & {
51
- create: () => string & import("effect/Brand").Brand<"SessionID">;
52
- descending: (id?: string) => string & import("effect/Brand").Brand<"SessionID">;
53
- };
54
- readonly questions: Schema.$Array<Schema.Struct<{
55
- readonly custom: Schema.decodeTo<Schema.optional<Schema.toType<Schema.Boolean>>, Schema.optionalKey<Schema.Boolean>, never, never>;
56
- readonly question: Schema.String;
57
- readonly header: Schema.String;
58
- readonly options: Schema.$Array<Schema.Struct<{
59
- readonly label: Schema.String;
60
- readonly description: Schema.String;
61
- }>>;
62
- readonly multiple: Schema.decodeTo<Schema.optional<Schema.toType<Schema.Boolean>>, Schema.optionalKey<Schema.Boolean>, never, never>;
63
- }>>;
64
- readonly tool: Schema.decodeTo<Schema.optional<Schema.toType<Schema.Struct<{
65
- readonly messageID: Schema.String;
66
- readonly callID: Schema.String;
67
- }>>>, Schema.optionalKey<Schema.Struct<{
68
- readonly messageID: Schema.String;
69
- readonly callID: Schema.String;
70
- }>>, never, never>;
71
- }>>;
72
- }>>, HttpApiEndpoint.Json<typeof SessionNotFoundError>, SessionLocationId, HttpApiMiddleware.Requires<SessionLocationId>> | HttpApiEndpoint.HttpApiEndpoint<"session.question.reply", "POST", "/api/session/:sessionID/question/:requestID/reply", HttpApiEndpoint.StringTree<Schema.Struct<{
73
- sessionID: Schema.brand<Schema.String, "SessionID"> & {
74
- create: () => string & import("effect/Brand").Brand<"SessionID">;
75
- descending: (id?: string) => string & import("effect/Brand").Brand<"SessionID">;
76
- };
77
- requestID: Schema.brand<Schema.String, "QuestionV2.ID"> & {
78
- create: () => string & import("effect/Brand").Brand<"QuestionV2.ID">;
79
- ascending: (id?: string) => string & import("effect/Brand").Brand<"QuestionV2.ID">;
80
- };
81
- }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
82
- readonly answers: Schema.$Array<Schema.$Array<Schema.String>>;
83
- }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<HttpApiSchema.NoContent>, HttpApiEndpoint.Json<typeof SessionNotFoundError | typeof QuestionNotFoundError>, SessionLocationId, HttpApiMiddleware.Requires<SessionLocationId>> | HttpApiEndpoint.HttpApiEndpoint<"session.question.reject", "POST", "/api/session/:sessionID/question/:requestID/reject", HttpApiEndpoint.StringTree<Schema.Struct<{
84
- sessionID: Schema.brand<Schema.String, "SessionID"> & {
85
- create: () => string & import("effect/Brand").Brand<"SessionID">;
86
- descending: (id?: string) => string & import("effect/Brand").Brand<"SessionID">;
87
- };
88
- requestID: Schema.brand<Schema.String, "QuestionV2.ID"> & {
89
- create: () => string & import("effect/Brand").Brand<"QuestionV2.ID">;
90
- ascending: (id?: string) => string & import("effect/Brand").Brand<"QuestionV2.ID">;
91
- };
92
- }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<HttpApiSchema.NoContent>, HttpApiEndpoint.Json<typeof SessionNotFoundError | typeof QuestionNotFoundError>, SessionLocationId, HttpApiMiddleware.Requires<SessionLocationId>>, false>;
@@ -1,56 +0,0 @@
1
- import { Question } from "@opencode-ai/schema/question";
2
- import { Location } from "@opencode-ai/schema/location";
3
- import { Session } from "@opencode-ai/schema/session";
4
- import { Context, Schema } from "effect";
5
- import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi";
6
- import { QuestionNotFoundError, SessionNotFoundError } from "../errors.js";
7
- import { LocationQuery, locationQueryOpenApi } from "./location.js";
8
- export const makeQuestionGroup = (locationMiddleware, sessionLocationMiddleware) => HttpApiGroup.make("server.question")
9
- .add(HttpApiEndpoint.get("question.request.list", "/api/question/request", {
10
- query: LocationQuery,
11
- success: Location.response(Schema.Array(Question.Request)),
12
- })
13
- .annotateMerge(locationQueryOpenApi)
14
- .annotateMerge(OpenApi.annotations({
15
- identifier: "v2.question.request.list",
16
- summary: "List pending question requests",
17
- description: "Retrieve pending question requests for a location.",
18
- })))
19
- .annotateMerge(OpenApi.annotations({ title: "questions", description: "Experimental question routes." }))
20
- // Effect applies group middleware only to endpoints already added; session endpoints use session placement below.
21
- .middleware(locationMiddleware)
22
- .add(HttpApiEndpoint.get("session.question.list", "/api/session/:sessionID/question", {
23
- params: { sessionID: Session.ID },
24
- success: Schema.Struct({ data: Schema.Array(Question.Request) }),
25
- error: SessionNotFoundError,
26
- })
27
- .middleware(sessionLocationMiddleware)
28
- .annotateMerge(OpenApi.annotations({
29
- identifier: "v2.session.question.list",
30
- summary: "List session question requests",
31
- description: "Retrieve pending question requests owned by a session.",
32
- })))
33
- .add(HttpApiEndpoint.post("session.question.reply", "/api/session/:sessionID/question/:requestID/reply", {
34
- params: { sessionID: Session.ID, requestID: Question.ID },
35
- payload: Question.Reply,
36
- success: HttpApiSchema.NoContent,
37
- error: [SessionNotFoundError, QuestionNotFoundError],
38
- })
39
- .middleware(sessionLocationMiddleware)
40
- .annotateMerge(OpenApi.annotations({
41
- identifier: "v2.session.question.reply",
42
- summary: "Reply to pending question request",
43
- description: "Answer a pending question request owned by a session.",
44
- })))
45
- .add(HttpApiEndpoint.post("session.question.reject", "/api/session/:sessionID/question/:requestID/reject", {
46
- params: { sessionID: Session.ID, requestID: Question.ID },
47
- success: HttpApiSchema.NoContent,
48
- error: [SessionNotFoundError, QuestionNotFoundError],
49
- })
50
- .middleware(sessionLocationMiddleware)
51
- .annotateMerge(OpenApi.annotations({
52
- identifier: "v2.session.question.reject",
53
- summary: "Reject pending question request",
54
- description: "Reject a pending question request owned by a session.",
55
- })))
56
- .annotateMerge(OpenApi.annotations({ title: "session questions", description: "Experimental session question routes." }));