已合并
[SESSION-03] Implement frontend-neutral canonical user-event ingestion #23
urandon创建于 8月10日
[SESSION-03] Implement frontend-neutral canonical user-event ingestion #23
已合并
urandon创建于 8月10日
19 个文件变更+1932-77
MREADME.md+9-2文件内容审核中,请稍后刷新重试
Adocs/canonical-ingress.md+102-0文件内容审核中,请稍后刷新重试
@@ -46,6 +46,19 @@ operations. `CreateAndSwitchSession` is the `/new` transaction boundary: a
46failure cannot leave either an orphan product session or a half-switched46failure cannot leave either an orphan product session or a half-switched
47frontend conversation.47frontend conversation.
48 48 
49+`CanonicalIngressStore` is the application persistence boundary shared by all
50+frontends. An authenticated adapter supplies an internal user, opaque frontend
51+and external conversation/event identifiers, and a stable delivery time. The
52+store requires active tenant membership and a writable session participant,
53+then commits the frontend deduplication fact, canonical user event, run,
54+initial attempt, input manifest, and dispatch outbox in one serializable YDB
55+transaction. No Telegram update, chat, or message type crosses this contract.
56+ 
57+Frontend deduplication is keyed by tenant, binding, and the normalized delivery
58+key rather than the current session. Therefore a delayed duplicate received
59+after a clean-context binding switch still resolves to the original event and
60+run instead of being appended to the newer session.
61+ 
49`SessionParticipant` grants `owner`, `member`, or `viewer` access to an active62`SessionParticipant` grants `owner`, `member`, or `viewer` access to an active
50tenant membership. Tenant, session, and user must all match before access is63tenant membership. Tenant, session, and user must all match before access is
51allowed; viewers cannot append. Authentication establishes a user identity,64allowed; viewers cannot append. Authentication establishes a user identity,
@@ -134,9 +147,9 @@ the canonical event exists. Retrying or adding a frontend therefore cannot
134duplicate or rewrite canonical history.147duplicate or rewrite canonical history.
135 148 
136Until #23 migrates worker finalization, the existing Telegram delivery outbox149Until #23 migrates worker finalization, the existing Telegram delivery outbox
137-is a compatibility projection. Until #22/#36 migrate ingestion, the existing150+is a compatibility projection. Until #36 adapts Telegram to
138-Telegram update transaction is a compatibility ingress path. Neither changes151+`CanonicalIngressStore`, the existing Telegram update transaction is a
139-the canonical contract above.152+compatibility ingress path. Neither changes the canonical contract above.
140 153 
141## Quota and usage154## Quota and usage
142 155 
Mdocs/ydb-partitioning.md+1-0文件内容审核中,请稍后刷新重试
Mdocs/ydb-state-store.md+16-2文件内容审核中,请稍后刷新重试
Minternal/domain/identity.go+36-0文件内容审核中,请稍后刷新重试
@@ -23,21 +23,25 @@ func CanTransitionDispatch(from, to DispatchStatus) bool {
23}23}
24 24 
25type DispatchOutbox struct {25type DispatchOutbox struct {
26- ID DispatchOutboxID `json:"id"`26+ ID DispatchOutboxID `json:"id"`
27- TenantID TenantID `json:"tenant_id"`27+ TenantID TenantID `json:"tenant_id"`
28- RunID RunID `json:"run_id"`28+ RunID RunID `json:"run_id"`
29- AttemptID AttemptID `json:"attempt_id"`29+ AttemptID AttemptID `json:"attempt_id"`
30- InputManifestID ArtifactManifestID `json:"input_manifest_id"`30+ InputManifestID ArtifactManifestID `json:"input_manifest_id"`
31- ContextSnapshot BlobRef `json:"context_snapshot"`31+ ContextSnapshot BlobRef `json:"context_snapshot"`
32- WorkspaceSnapshot *BlobRef `json:"workspace_snapshot,omitempty"`32+ WorkspaceSnapshot *BlobRef `json:"workspace_snapshot,omitempty"`
33- SkillBundle *BlobRef `json:"skill_bundle,omitempty"`33+ SkillBundle *BlobRef `json:"skill_bundle,omitempty"`
34- AllowedMCPServers []string `json:"allowed_mcp_servers,omitempty"`34+ AllowedMCPServers []string `json:"allowed_mcp_servers,omitempty"`
35- DeliveryChat TelegramChatRef `json:"delivery_chat"`35+ Origin *FrontendEventOrigin `json:"origin,omitempty"`
36- ReplyToMessageID int64 `json:"reply_to_message_id"`36+ // DeliveryChat and ReplyToMessageID are the compatibility bridge for the
37- Status DispatchStatus `json:"status"`37+ // pre-canonical Telegram worker flow. New ingress paths use Origin; #36
38- IdempotencyKey IdempotencyKey `json:"idempotency_key"`38+ // removes this bridge when Telegram projects canonical assistant events.
39- CreatedAt time.Time `json:"created_at"`39+ DeliveryChat TelegramChatRef `json:"delivery_chat"`
40- UpdatedAt time.Time `json:"updated_at"`40+ ReplyToMessageID int64 `json:"reply_to_message_id"`
41+ Status DispatchStatus `json:"status"`
42+ IdempotencyKey IdempotencyKey `json:"idempotency_key"`
43+ CreatedAt time.Time `json:"created_at"`
44+ UpdatedAt time.Time `json:"updated_at"`
41}45}
42 46 
43func (outbox DispatchOutbox) ValidateForAttempt(run Run, attempt Attempt) error {47func (outbox DispatchOutbox) ValidateForAttempt(run Run, attempt Attempt) error {
@@ -69,14 +73,24 @@ func (outbox DispatchOutbox) ValidateForAttempt(run Run, attempt Attempt) error
69 return err73 return err
70 }74 }
71 }75 }
72- if err := outbox.DeliveryChat.Validate(); err != nil {76+ if outbox.Origin == nil && outbox.DeliveryChat.ChatID == 0 {
73- return err77+ return ValidationError{Field: "dispatch_outbox.origin", Reason: "a frontend origin or legacy delivery target is required"}
74 }78 }
75- if err := EnsureSameTenant(run.TenantID, outbox.DeliveryChat.TenantID); err != nil {79+ if outbox.Origin != nil {
76- return err80+ if err := outbox.Origin.Validate(); err != nil {
81+ return err
82+ }
77 }83 }
78- if outbox.ReplyToMessageID == 0 {84+ if outbox.DeliveryChat.ChatID != 0 {
79- return ValidationError{Field: "dispatch_outbox.reply_to_message_id", Reason: "must not be zero"}85+ if err := outbox.DeliveryChat.Validate(); err != nil {
86+ return err
87+ }
88+ if err := EnsureSameTenant(run.TenantID, outbox.DeliveryChat.TenantID); err != nil {
89+ return err
90+ }
91+ if outbox.ReplyToMessageID == 0 {
92+ return ValidationError{Field: "dispatch_outbox.reply_to_message_id", Reason: "must not be zero for a legacy delivery target"}
93+ }
80 }94 }
81 if !outbox.Status.Valid() {95 if !outbox.Status.Valid() {
82 return ValidationError{Field: "dispatch_outbox.status", Reason: "is unknown"}96 return ValidationError{Field: "dispatch_outbox.status", Reason: "is unknown"}
@@ -8,21 +8,24 @@ import (
8// WorkerJob is the durable, point-addressable materialization contract for one8// WorkerJob is the durable, point-addressable materialization contract for one
9// admitted run. Queue messages carry only its tenant/run routing identity.9// admitted run. Queue messages carry only its tenant/run routing identity.
10type WorkerJob struct {10type WorkerJob struct {
11- TenantID TenantID `json:"tenant_id"`11+ TenantID TenantID `json:"tenant_id"`
12- RunID RunID `json:"run_id"`12+ RunID RunID `json:"run_id"`
13- SessionID SessionID `json:"session_id"`13+ SessionID SessionID `json:"session_id"`
14- TriggerEventID SessionEventID `json:"trigger_event_id"`14+ TriggerEventID SessionEventID `json:"trigger_event_id"`
15- AttemptID AttemptID `json:"attempt_id"`15+ AttemptID AttemptID `json:"attempt_id"`
16- ReservationID QuotaReservationID `json:"reservation_id"`16+ ReservationID QuotaReservationID `json:"reservation_id"`
17- InputManifestID ArtifactManifestID `json:"input_manifest_id"`17+ InputManifestID ArtifactManifestID `json:"input_manifest_id"`
18- ContextSnapshot BlobRef `json:"context_snapshot"`18+ ContextSnapshot BlobRef `json:"context_snapshot"`
19- WorkspaceSnapshot *BlobRef `json:"workspace_snapshot,omitempty"`19+ WorkspaceSnapshot *BlobRef `json:"workspace_snapshot,omitempty"`
20- SkillBundle *BlobRef `json:"skill_bundle,omitempty"`20+ SkillBundle *BlobRef `json:"skill_bundle,omitempty"`
21- AllowedMCPServers []string `json:"allowed_mcp_servers,omitempty"`21+ AllowedMCPServers []string `json:"allowed_mcp_servers,omitempty"`
22- Limits ProductLimits `json:"limits"`22+ Limits ProductLimits `json:"limits"`
23- DeliveryChat TelegramChatRef `json:"delivery_chat"`23+ Origin *FrontendEventOrigin `json:"origin,omitempty"`
24- ReplyToMessageID int64 `json:"reply_to_message_id"`24+ // Compatibility bridge for Telegram until #36 projects results from the
25- CreatedAt time.Time `json:"created_at"`25+ // canonical session stream.
26+ DeliveryChat TelegramChatRef `json:"delivery_chat"`
27+ ReplyToMessageID int64 `json:"reply_to_message_id"`
28+ CreatedAt time.Time `json:"created_at"`
26}29}
27 30 
28func (job WorkerJob) ValidateForRun(run Run) error {31func (job WorkerJob) ValidateForRun(run Run) error {
@@ -76,14 +79,24 @@ func (job WorkerJob) ValidateForRun(run Run) error {
76 if uint64(job.ContextSnapshot.Size) > job.Limits.MaxContextBytes {79 if uint64(job.ContextSnapshot.Size) > job.Limits.MaxContextBytes {
77 return ValidationError{Field: "worker_job.context_snapshot", Reason: "exceeds the admitted context limit"}80 return ValidationError{Field: "worker_job.context_snapshot", Reason: "exceeds the admitted context limit"}
78 }81 }
79- if err := job.DeliveryChat.Validate(); err != nil {82+ if job.Origin == nil && job.DeliveryChat.ChatID == 0 {
80- return err83+ return ValidationError{Field: "worker_job.origin", Reason: "a frontend origin or legacy delivery target is required"}
81 }84 }
82- if err := EnsureSameTenant(run.TenantID, job.DeliveryChat.TenantID); err != nil {85+ if job.Origin != nil {
83- return err86+ if err := job.Origin.Validate(); err != nil {
87+ return err
88+ }
84 }89 }
85- if job.ReplyToMessageID == 0 {90+ if job.DeliveryChat.ChatID != 0 {
86- return ValidationError{Field: "worker_job.reply_to_message_id", Reason: "must not be zero"}91+ if err := job.DeliveryChat.Validate(); err != nil {
92+ return err
93+ }
94+ if err := EnsureSameTenant(run.TenantID, job.DeliveryChat.TenantID); err != nil {
95+ return err
96+ }
97+ if job.ReplyToMessageID == 0 {
98+ return ValidationError{Field: "worker_job.reply_to_message_id", Reason: "must not be zero for a legacy delivery target"}
99+ }
87 }100 }
88 if job.CreatedAt.IsZero() || job.CreatedAt.Before(run.CreatedAt) {101 if job.CreatedAt.IsZero() || job.CreatedAt.Before(run.CreatedAt) {
89 return ValidationError{Field: "worker_job.created_at", Reason: "must not precede the owning run"}102 return ValidationError{Field: "worker_job.created_at", Reason: "must not precede the owning run"}
Ainternal/ports/canonical_ingress.go+147-0文件内容审核中,请稍后刷新重试
Minternal/preprodreset/reset.go+1-0文件内容审核中,请稍后刷新重试
Ainternal/sessioningress/service.go+317-0文件内容审核中,请稍后刷新重试
Ainternal/sessioningress/service_test.go+347-0文件内容审核中,请稍后刷新重试
Ainternal/syntheticfrontend/adapter.go+57-0文件内容审核中,请稍后刷新重试
Minternal/ydbpartition/contract.go+1-0文件内容审核中,请稍后刷新重试
Ainternal/ydbstore/canonical_ingress.go+514-0文件内容审核中,请稍后刷新重试
Minternal/ydbstore/scheduler.go+12-0文件内容审核中,请稍后刷新重试
Minternal/ydbstore/sessions.go+1-28文件内容审核中,请稍后刷新重试
Amigrations/ydb/00057_create_frontend_ingress_idempotency.sql+22-0文件内容审核中,请稍后刷新重试
Mtest/ydbintegration/session_store_test.go+264-0文件内容审核中,请稍后刷新重试