已合并
SESSION-01: define canonical session and event contracts #18
urandon创建于 8月4日
SESSION-01: define canonical session and event contracts #18
已合并
urandon创建于 8月4日
30 个文件变更+956-307
MAGENTS.md+25-12文件内容审核中,请稍后刷新重试
MREADME.md+18-14
@@ -3,13 +3,13 @@
3Sessionless is a serverless, cloud-hosted control plane for routing3Sessionless is a serverless, cloud-hosted control plane for routing
4conversation-backed work to isolated AI-agent workers.4conversation-backed work to isolated AI-agent workers.
5 5 
6-The core is frontend-aware but not frontend-specific. Telegram is the first6+Sessionless owns the canonical conversation model: an append-only, strictly
7-frontend and its chat history provides the authoritative conversation context7+ordered `SessionEvent` stream with immutable snapshots as optional context
8-for the initial product slice. Additional frontends can be added through8+materializations. Telegram is the first frontend adapter, and WebUI is the next;
9-transport adapters without redefining runs, scheduling, quota accounting, or9+both bind external conversations to canonical sessions without redefining runs,
10-worker isolation. A new clean context is always an explicit frontend action10+scheduling, quota accounting, or worker isolation. A `/new` action creates a
11-that advances a context epoch; it does not create a hidden primary session11+new session and switches the frontend binding atomically. Existing sessions and
12-model or delete the frontend's history.12+their history remain intact.
13 13 
14The control plane is also harness-neutral. Codex, OpenCode, Claude, and14The control plane is also harness-neutral. Codex, OpenCode, Claude, and
15Hermes-style runtimes are candidates for isolated worker adapters, not15Hermes-style runtimes are candidates for isolated worker adapters, not
@@ -24,18 +24,22 @@ and MCP access.
24 24 
25The repository currently contains the Go component boundaries, harness-neutral25The repository currently contains the Go component boundaries, harness-neutral
26domain/runtime contracts, the authoritative YDB state store and migrations,26domain/runtime contracts, the authoritative YDB state store and migrations,
27-authenticated Telegram webhook ingestion, durable Telegram delivery, bounded27+authenticated Telegram webhook ingestion, durable Telegram delivery, canonical
28+session domain and port contracts, bounded
28subscription-aware admission and dispatch, a reproducible local development29subscription-aware admission and dispatch, a reproducible local development
29stand, isolated worker packaging, pinned developer tools, subscription state30stand, isolated worker packaging, pinned developer tools, subscription state
30-commands, explicit clean-context epochs, and GitHub Actions CI fed by the31+commands, and GitHub Actions CI fed by the
31GitCode mirror. It also contains the complete isolated worker lifecycle with a32GitCode mirror. It also contains the complete isolated worker lifecycle with a
32credential-free deterministic harness: durable job materialization, fenced33credential-free deterministic harness: durable job materialization, fenced
33lease renewal, bounded scratch space, checkpoint/resume, usage events,34lease renewal, bounded scratch space, checkpoint/resume, usage events,
34content-addressed artifacts, cancellation/timeout handling, and atomic terminal35content-addressed artifacts, cancellation/timeout handling, and atomic terminal
35delivery. A credential-free two-tenant black-box suite now composes the full36delivery. A credential-free two-tenant black-box suite now composes the full
36local Telegram-to-worker-to-Telegram path and its recovery cases. Provider37local Telegram-to-worker-to-Telegram path and its recovery cases. Provider
37-authorization and subscription-backed Codex, OpenCode, Claude, or Hermes38+authorization, canonical-session persistence, frontend projection, and
38-adapters remain later implementation slices.39+subscription-backed Codex, OpenCode, Claude, or Hermes adapters remain later
40+implementation slices. The current Telegram/YDB adapter still carries a private
41+legacy revision until those persistence and adapter migrations land; it is not
42+part of the product contract.
39 43 
40## Components44## Components
41 45 
@@ -52,8 +56,8 @@ adapters remain later implementation slices.
52- `internal/worker`: durable materialize/execute/checkpoint/finalize lifecycle;56- `internal/worker`: durable materialize/execute/checkpoint/finalize lifecycle;
53- `internal/deterministicharness`: credential-free adapter that proves the57- `internal/deterministicharness`: credential-free adapter that proves the
54 worker contract before a subscription CLI is selected;58 worker contract before a subscription CLI is selected;
55-- `internal/domain`: tenant-scoped identities, state machines, quota/usage59+- `internal/domain`: canonical sessions/events/bindings/snapshots, tenant-scoped
56- semantics, outboxes, artifacts, and explicit context epochs;60+ identities, state machines, quota/usage semantics, outboxes, and artifacts;
57- `internal/ports`: YDB/queue/blob/frontend/credential/harness-neutral runtime61- `internal/ports`: YDB/queue/blob/frontend/credential/harness-neutral runtime
58 interfaces;62 interfaces;
59- `internal/portlog`: process-boundary structured correlation logs without63- `internal/portlog`: process-boundary structured correlation logs without
@@ -73,7 +77,7 @@ adapters remain later implementation slices.
73 reservation enforcement, durable queue publication, and quota expiry;77 reservation enforcement, durable queue publication, and quota expiry;
74- `internal/telegramingress`: webhook authentication, opaque deterministic78- `internal/telegramingress`: webhook authentication, opaque deterministic
75 identity resolution, normalized input/blob handling, durable subscription79 identity resolution, normalized input/blob handling, durable subscription
76- commands, explicit clean-context transitions, and idempotent run creation;80+ commands, transitional binding compatibility, and idempotent run creation;
77- `internal/telegramdelivery`: bounded YDB-ready traversal, transactional delivery81- `internal/telegramdelivery`: bounded YDB-ready traversal, transactional delivery
78 claims, retry policy, and Telegram Bot API sending;82 claims, retry policy, and Telegram Bot API sending;
79- `internal/queuecontract`: versioned queue envelopes containing opaque IDs only.83- `internal/queuecontract`: versioned queue envelopes containing opaque IDs only.
Mdocs/contracts.md+64-19文件内容审核中,请稍后刷新重试
Mdocs/development.md+8-6文件内容审核中,请稍后刷新重试
Mdocs/local-development-stand.md+14-6文件内容审核中,请稍后刷新重试
Mdocs/local-e2e.md+2-1文件内容审核中,请稍后刷新重试
Mdocs/telegram.md+10-8文件内容审核中,请稍后刷新重试
Minternal/deterministicharness/driver_test.go+2-1文件内容审核中,请稍后刷新重试
@@ -1,80 +0,0 @@
1-package domain
2- 
3-import "time"
4- 
5-// ContextEpoch identifies an explicit frontend-visible context generation.
6-// Epoch 1 is the initial context; it changes only through a clean-context event.
7-type ContextEpoch uint64
8- 
9-const InitialContextEpoch ContextEpoch = 1
10- 
11-func (epoch ContextEpoch) Validate() error {
12- if epoch < InitialContextEpoch {
13- return ValidationError{Field: "context_epoch", Reason: "must be at least 1"}
14- }
15- return nil
16-}
17- 
18-func (epoch ContextEpoch) Next() (ContextEpoch, error) {
19- if err := epoch.Validate(); err != nil {
20- return 0, err
21- }
22- if epoch == ^ContextEpoch(0) {
23- return 0, ValidationError{Field: "context_epoch", Reason: "cannot overflow"}
24- }
25- return epoch + 1, nil
26-}
27- 
28-// CleanContextEvent records the explicit user action that moves a
29-// conversation to a fresh epoch. It does not delete frontend history.
30-type CleanContextEvent struct {
31- TenantID TenantID `json:"tenant_id"`
32- Conversation ConversationRef `json:"conversation"`
33- RequestedBy ActorRef `json:"requested_by"`
34- PreviousEpoch ContextEpoch `json:"previous_epoch"`
35- NewEpoch ContextEpoch `json:"new_epoch"`
36- TriggerMessageID string `json:"trigger_message_id"`
37- IdempotencyKey IdempotencyKey `json:"idempotency_key"`
38- RequestedAt time.Time `json:"requested_at"`
39-}
40- 
41-func (event CleanContextEvent) Validate() error {
42- if err := event.TenantID.Validate(); err != nil {
43- return err
44- }
45- if err := event.Conversation.Validate(); err != nil {
46- return err
47- }
48- if err := EnsureSameTenant(event.TenantID, event.Conversation.TenantID); err != nil {
49- return err
50- }
51- if err := event.RequestedBy.Validate(); err != nil {
52- return err
53- }
54- if err := EnsureSameTenant(event.TenantID, event.RequestedBy.TenantID); err != nil {
55- return err
56- }
57- if event.Conversation.Frontend != event.RequestedBy.Frontend {
58- return ValidationError{Field: "requested_by.frontend", Reason: "must match the conversation frontend"}
59- }
60- if err := event.PreviousEpoch.Validate(); err != nil {
61- return err
62- }
63- next, err := event.PreviousEpoch.Next()
64- if err != nil {
65- return err
66- }
67- if event.NewEpoch != next {
68- return ValidationError{Field: "new_epoch", Reason: "must increment previous_epoch by exactly one"}
69- }
70- if event.TriggerMessageID == "" {
71- return ValidationError{Field: "trigger_message_id", Reason: "must not be empty"}
72- }
73- if err := event.IdempotencyKey.Validate(); err != nil {
74- return err
75- }
76- if event.RequestedAt.IsZero() {
77- return ValidationError{Field: "requested_at", Reason: "must not be zero"}
78- }
79- return nil
80-}
Minternal/domain/identity.go+14-0文件内容审核中,请稍后刷新重试
@@ -11,18 +11,12 @@ import (
11var testTime = time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)11var testTime = time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
12 12 
13func validRun() domain.Run {13func validRun() domain.Run {
14- conversation := domain.ConversationRef{
15- TenantID: "tenant-a",
16- Frontend: domain.FrontendTelegram,
17- ExternalID: "-1000123",
18- ID: "conversation-1",
19- }
20 return domain.Run{14 return domain.Run{
21 ID: "run-1",15 ID: "run-1",
22 TenantID: "tenant-a",16 TenantID: "tenant-a",
23- Conversation: conversation,17+ SessionID: "session-1",
18+ TriggerEventID: "event-1",
24 SubscriptionConnectionID: "subscription-1",19 SubscriptionConnectionID: "subscription-1",
25- ContextEpoch: domain.InitialContextEpoch,
26 Status: domain.RunCreated,20 Status: domain.RunCreated,
27 IdempotencyKey: "telegram-update-1",21 IdempotencyKey: "telegram-update-1",
28 CreatedAt: testTime,22 CreatedAt: testTime,
@@ -75,45 +69,6 @@ func TestBlobMustRemainInsideTenantPrefix(t *testing.T) {
75 }69 }
76}70}
77 71 
78-func TestCleanContextRequiresExplicitNextEpochAndMatchingTenant(t *testing.T) {
79- t.Parallel()
80- 
81- event := domain.CleanContextEvent{
82- TenantID: "tenant-a",
83- Conversation: domain.ConversationRef{
84- TenantID: "tenant-a",
85- Frontend: domain.FrontendTelegram,
86- ExternalID: "-1000123",
87- ID: "conversation-1",
88- },
89- RequestedBy: domain.ActorRef{
90- TenantID: "tenant-a",
91- Frontend: domain.FrontendTelegram,
92- ExternalID: "1234",
93- ID: "actor-1",
94- },
95- PreviousEpoch: 1,
96- NewEpoch: 2,
97- TriggerMessageID: "telegram-message-7",
98- IdempotencyKey: "telegram-update-7",
99- RequestedAt: testTime,
100- }
101- if err := event.Validate(); err != nil {
102- t.Fatalf("valid CleanContextEvent rejected: %v", err)
103- }
104- 
105- event.NewEpoch = 3
106- if err := event.Validate(); err == nil {
107- t.Fatal("CleanContextEvent.Validate() accepted a skipped epoch")
108- }
109- 
110- event.NewEpoch = 2
111- event.RequestedBy.TenantID = "tenant-b"
112- if err := event.Validate(); err == nil {
113- t.Fatal("CleanContextEvent.Validate() accepted a cross-tenant actor")
114- }
115-}
116- 
117func TestUnknownQuotaCannotFabricateRemainingValue(t *testing.T) {72func TestUnknownQuotaCannotFabricateRemainingValue(t *testing.T) {
118 t.Parallel()73 t.Parallel()
119 74 
@@ -57,9 +57,9 @@ func CanTransitionRun(from, to RunStatus) bool {
57type Run struct {57type Run struct {
58 ID RunID `json:"id"`58 ID RunID `json:"id"`
59 TenantID TenantID `json:"tenant_id"`59 TenantID TenantID `json:"tenant_id"`
60- Conversation ConversationRef `json:"conversation"`60+ SessionID SessionID `json:"session_id"`
61+ TriggerEventID SessionEventID `json:"trigger_event_id"`
61 SubscriptionConnectionID SubscriptionConnectionID `json:"subscription_connection_id"`62 SubscriptionConnectionID SubscriptionConnectionID `json:"subscription_connection_id"`
62- ContextEpoch ContextEpoch `json:"context_epoch"`
63 Status RunStatus `json:"status"`63 Status RunStatus `json:"status"`
64 IdempotencyKey IdempotencyKey `json:"idempotency_key"`64 IdempotencyKey IdempotencyKey `json:"idempotency_key"`
65 CancellationRequestedAt *time.Time `json:"cancellation_requested_at,omitempty"`65 CancellationRequestedAt *time.Time `json:"cancellation_requested_at,omitempty"`
@@ -76,18 +76,15 @@ func (run Run) Validate() error {
76 if err := run.TenantID.Validate(); err != nil {76 if err := run.TenantID.Validate(); err != nil {
77 return err77 return err
78 }78 }
79- if err := run.Conversation.Validate(); err != nil {79+ if err := run.SessionID.Validate(); err != nil {
80 return err80 return err
81 }81 }
82- if err := EnsureSameTenant(run.TenantID, run.Conversation.TenantID); err != nil {82+ if err := run.TriggerEventID.Validate(); err != nil {
83 return err83 return err
84 }84 }
85 if err := run.SubscriptionConnectionID.Validate(); err != nil {85 if err := run.SubscriptionConnectionID.Validate(); err != nil {
86 return err86 return err
87 }87 }
88- if err := run.ContextEpoch.Validate(); err != nil {
89- return err
90- }
91 if !run.Status.Valid() {88 if !run.Status.Valid() {
92 return ValidationError{Field: "run.status", Reason: "is unknown"}89 return ValidationError{Field: "run.status", Reason: "is unknown"}
93 }90 }
Ainternal/domain/session.go+435-0文件内容审核中,请稍后刷新重试
Ainternal/domain/session_test.go+123-0文件内容审核中,请稍后刷新重试
Minternal/domain/worker.go+5-0文件内容审核中,请稍后刷新重试
Minternal/idgen/generator.go+5-0文件内容审核中,请稍后刷新重试
Minternal/ports/ports.go+26-1文件内容审核中,请稍后刷新重试
Minternal/ports/validation.go+6-0文件内容审核中,请稍后刷新重试
Minternal/ports/validation_test.go+4-0文件内容审核中,请稍后刷新重试
Minternal/telegramingress/processor.go+20-3文件内容审核中,请稍后刷新重试
Minternal/telegramingress/processor_test.go+2-6文件内容审核中,请稍后刷新重试
Minternal/worker/manager.go+1-0文件内容审核中,请稍后刷新重试
Minternal/worker/manager_test.go+6-7文件内容审核中,请稍后刷新重试
Minternal/ydbstore/operations.go+40-47文件内容审核中,请稍后刷新重试
Minternal/ydbstore/scheduler.go+1-0文件内容审核中,请稍后刷新重试
Minternal/ydbstore/store.go+2-2文件内容审核中,请稍后刷新重试
Mscripts/dev-up.sh+37-6文件内容审核中,请稍后刷新重试
Mtest/e2e/slice_test.go+61-14文件内容审核中,请稍后刷新重试
Mtest/localintegration/stand_test.go+9-9文件内容审核中,请稍后刷新重试
Mtest/ydbintegration/store_test.go+10-11文件内容审核中,请稍后刷新重试