| 1 | package sessioninbox |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "time" |
| 6 | |
| 7 | "reasonix/internal/attachment" |
| 8 | ) |
| 9 | |
| 10 | // SchemaVersion is the on-disk format version. Unknown higher versions load |
| 11 | // read-only and force pause; they never auto-execute. |
| 12 | const SchemaVersion = 3 |
| 13 | |
| 14 | // Default capacity limits. |
| 15 | const ( |
| 16 | DefaultMaxItems = 64 |
| 17 | DefaultMaxItemBytes = 4 << 20 // 4 MiB |
| 18 | DefaultMaxTotalBytes = 64 << 20 // 64 MiB |
| 19 | DefaultPreviewRunes = 120 |
| 20 | ) |
| 21 | |
| 22 | // InboxIntent distinguishes durable follow-up turns from mid-turn steers. |
| 23 | type InboxIntent string |
| 24 | |
| 25 | const ( |
| 26 | IntentFollowup InboxIntent = "followup" |
| 27 | IntentSteer InboxIntent = "steer" |
| 28 | ) |
| 29 | |
| 30 | // InboxState is the durable lifecycle of one queue item. |
| 31 | type InboxState string |
| 32 | |
| 33 | const ( |
| 34 | StateQueued InboxState = "queued" |
| 35 | StateSteerAccepted InboxState = "steer_accepted" |
| 36 | StateSteerConsumed InboxState = "steer_consumed" |
| 37 | StateRunning InboxState = "running" |
| 38 | StateBlocked InboxState = "blocked" |
| 39 | StateUncertain InboxState = "uncertain" |
| 40 | ) |
| 41 | |
| 42 | // Disposition reports how an admission attempt settled. |
| 43 | type Disposition string |
| 44 | |
| 45 | const ( |
| 46 | DispositionStarted Disposition = "started" |
| 47 | DispositionSteerAccepted Disposition = "steer_accepted" |
| 48 | DispositionQueuedFollowup Disposition = "queued_followup" |
| 49 | DispositionRejectedBusy Disposition = "rejected_busy" |
| 50 | DispositionRejectedRotating Disposition = "rejected_rotating" |
| 51 | DispositionRejectedClosed Disposition = "rejected_closed" |
| 52 | DispositionRejectedCapacity Disposition = "rejected_capacity" |
| 53 | DispositionIdempotentHit Disposition = "idempotent_hit" |
| 54 | ) |
| 55 | |
| 56 | // Sentinel errors for capacity and validation. |
| 57 | var ( |
| 58 | ErrCapacityItems = errors.New("session inbox item limit reached") |
| 59 | ErrCapacityBytes = errors.New("session inbox byte limit reached") |
| 60 | ErrItemTooLarge = errors.New("inbox item exceeds single-item size limit") |
| 61 | ErrNotFound = errors.New("inbox item not found") |
| 62 | ErrInvalidState = errors.New("inbox item state does not allow this operation") |
| 63 | ErrSchemaReadonly = errors.New("inbox schema is newer and is read-only") |
| 64 | ErrClosed = errors.New("inbox is closed") |
| 65 | ErrSnapshotBusy = errors.New("inbox snapshot is busy") |
| 66 | ErrEmpty = errors.New("inbox item body is empty") |
| 67 | ErrPaused = errors.New("inbox is paused") |
| 68 | ErrIdempotencyConflict = errors.New("idempotency key was already used for different input") |
| 69 | ) |
| 70 | |
| 71 | // InboxItemMeta is the durable metadata kept in the manifest (never the body). |
| 72 | type InboxItemMeta struct { |
| 73 | ID string `json:"id"` |
| 74 | SessionID string `json:"sessionId,omitempty"` |
| 75 | Intent InboxIntent `json:"intent"` |
| 76 | State InboxState `json:"state"` |
| 77 | Revision int64 `json:"revision"` |
| 78 | // BlobName is the on-disk blob filename stem (without .json). Empty means |
| 79 | // legacy layout where the blob is named by item ID. Updates write a new |
| 80 | // immutable blob and switch this pointer after manifest commit. |
| 81 | BlobName string `json:"blobName,omitempty"` |
| 82 | Source string `json:"source,omitempty"` |
| 83 | CreatedAt time.Time `json:"createdAt"` |
| 84 | UpdatedAt time.Time `json:"updatedAt"` |
| 85 | Preview string `json:"preview"` |
| 86 | ByteSize int64 `json:"byteSize"` |
| 87 | Checksum string `json:"checksum"` |
| 88 | Idempotency string `json:"idempotencyKey,omitempty"` |
| 89 | Refs []RefSummary `json:"refs,omitempty"` |
| 90 | BlockReason string `json:"blockReason,omitempty"` |
| 91 | RunID string `json:"runId,omitempty"` |
| 92 | } |
| 93 | |
| 94 | // RefSummary is a short reference summary stored in the manifest. |
| 95 | type RefSummary struct { |
| 96 | Kind string `json:"kind"` // clean_git | frozen | external | attachment |
| 97 | Path string `json:"path,omitempty"` |
| 98 | Commit string `json:"commit,omitempty"` |
| 99 | Bytes int64 `json:"bytes,omitempty"` |
| 100 | Preview string `json:"preview,omitempty"` |
| 101 | } |
| 102 | |
| 103 | // RefSnapshot freezes a resolved @-reference at enqueue time. |
| 104 | type RefSnapshot struct { |
| 105 | Kind string `json:"kind"` // clean_git | frozen | external | attachment | mcp |
| 106 | Path string `json:"path,omitempty"` |
| 107 | DisplayPath string `json:"displayPath,omitempty"` |
| 108 | RepoIdentity string `json:"repoIdentity,omitempty"` |
| 109 | Commit string `json:"commit,omitempty"` |
| 110 | RangeStart int `json:"rangeStart,omitempty"` |
| 111 | RangeEnd int `json:"rangeEnd,omitempty"` |
| 112 | Content []byte `json:"content,omitempty"` |
| 113 | ContentSHA string `json:"contentSha,omitempty"` |
| 114 | Truncated bool `json:"truncated,omitempty"` |
| 115 | Server string `json:"server,omitempty"` |
| 116 | URI string `json:"uri,omitempty"` |
| 117 | } |
| 118 | |
| 119 | // StructuredInvocation is a frozen slash/command invocation. |
| 120 | type StructuredInvocation struct { |
| 121 | Name string `json:"name,omitempty"` |
| 122 | Kind string `json:"kind,omitempty"` |
| 123 | Offset int `json:"offset,omitempty"` |
| 124 | Args map[string]string `json:"args,omitempty"` |
| 125 | Display string `json:"display,omitempty"` |
| 126 | } |
| 127 | |
| 128 | // PromptEnvelope is the full durable body stored only in blobs/<id>.json. |
| 129 | type PromptEnvelope struct { |
| 130 | FingerprintVersion int `json:"fingerprintVersion,omitempty"` |
| 131 | RequestFingerprint string `json:"requestFingerprint,omitempty"` |
| 132 | DisplayText string `json:"displayText"` |
| 133 | RawText string `json:"rawText"` |
| 134 | SubmitText string `json:"submitText"` |
| 135 | // Invocation is retained for schema-v1 compatibility. New writers use |
| 136 | // Invocations so multiple rich-composer entities preserve visual order. |
| 137 | Invocation *StructuredInvocation `json:"invocation,omitempty"` |
| 138 | Invocations []StructuredInvocation `json:"invocations,omitempty"` |
| 139 | Format string `json:"format,omitempty"` |
| 140 | Attachments []string `json:"attachments,omitempty"` |
| 141 | AttachmentIdentities []string `json:"attachmentIdentities,omitempty"` |
| 142 | Refs []RefSnapshot `json:"refs,omitempty"` |
| 143 | // FrozenRefBlock is the exact typed reference context rendered at enqueue. |
| 144 | // FrozenImages contains already-authorized data URLs for direct image input. |
| 145 | FrozenRefBlock string `json:"frozenRefBlock,omitempty"` |
| 146 | FrozenImages []string `json:"frozenImages,omitempty"` |
| 147 | ImageInputs []attachment.ImageInput `json:"imageInputs,omitempty"` |
| 148 | ImageSourceRefs map[string]string `json:"imageSourceRefs,omitempty"` |
| 149 | ReferenceErrors []string `json:"referenceErrors,omitempty"` |
| 150 | ExplicitRefs []string `json:"explicitRefs,omitempty"` |
| 151 | Idempotency string `json:"idempotencyKey,omitempty"` |
| 152 | Source string `json:"source,omitempty"` |
| 153 | Extra map[string]string `json:"extra,omitempty"` |
| 154 | } |
| 155 | |
| 156 | // Capacity describes current usage against limits. |
| 157 | type Capacity struct { |
| 158 | Items int `json:"items"` |
| 159 | MaxItems int `json:"maxItems"` |
| 160 | Bytes int64 `json:"bytes"` |
| 161 | MaxBytes int64 `json:"maxBytes"` |
| 162 | MaxItemBytes int64 `json:"maxItemBytes"` |
| 163 | } |
| 164 | |
| 165 | // InboxSnapshot is the frontend-safe view: metadata only, never full bodies. |
| 166 | type InboxSnapshot struct { |
| 167 | SchemaVersion int `json:"schemaVersion"` |
| 168 | Revision int64 `json:"revision"` |
| 169 | Paused bool `json:"paused"` |
| 170 | Recovered bool `json:"recovered"` |
| 171 | RecoveredN int `json:"recoveredCount,omitempty"` |
| 172 | Readonly bool `json:"readonly,omitempty"` |
| 173 | RunID string `json:"runId,omitempty"` |
| 174 | SessionPath string `json:"sessionPath,omitempty"` |
| 175 | Items []InboxItemMeta `json:"items"` |
| 176 | Capacity Capacity `json:"capacity"` |
| 177 | } |
| 178 | |
| 179 | // InboxReceipt is returned after a durable enqueue or admission attempt. |
| 180 | type InboxReceipt struct { |
| 181 | ItemID string `json:"itemId"` |
| 182 | Disposition Disposition `json:"disposition"` |
| 183 | Position int `json:"position"` |
| 184 | Paused bool `json:"paused"` |
| 185 | Capacity Capacity `json:"capacity"` |
| 186 | Idempotent bool `json:"idempotent,omitempty"` |
| 187 | } |
| 188 | |
| 189 | // EnqueueRequest is the input for durable admission. |
| 190 | type EnqueueRequest struct { |
| 191 | Intent InboxIntent |
| 192 | Envelope PromptEnvelope |
| 193 | Source string |
| 194 | Idempotency string |
| 195 | SessionID string |
| 196 | } |
| 197 | |
| 198 | // Limits configures capacity. Zero fields use defaults. |
| 199 | type Limits struct { |
| 200 | MaxItems int |
| 201 | MaxItemBytes int64 |
| 202 | MaxTotalBytes int64 |
| 203 | } |
| 204 | |
| 205 | func (l Limits) withDefaults() Limits { |
| 206 | if l.MaxItems <= 0 { |
| 207 | l.MaxItems = DefaultMaxItems |
| 208 | } |
| 209 | if l.MaxItemBytes <= 0 { |
| 210 | l.MaxItemBytes = DefaultMaxItemBytes |
| 211 | } |
| 212 | if l.MaxTotalBytes <= 0 { |
| 213 | l.MaxTotalBytes = DefaultMaxTotalBytes |
| 214 | } |
| 215 | return l |
| 216 | } |
| 217 |