返回 DeepSeek-Reasonix
service.go
根目录 / internal / session / service.go
1 package session
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "log/slog"
8 "os"
9 "path/filepath"
10 "sync"
11 "sync/atomic"
12 "time"
13
14 "reasonix/internal/agent"
15 "reasonix/internal/event"
16 "reasonix/internal/transcript"
17 )
18
19 // SessionRef is the only execution identity used by the linear session
20 // service. Paths and UI selection are deliberately absent.
21 type SessionRef struct {
22 HostID string `json:"hostId"`
23 SessionID string `json:"sessionId"`
24 }
25
26 func (r SessionRef) validate(hostID string) error {
27 if r.HostID == "" || r.HostID != hostID {
28 return fmt.Errorf("session: session host %q does not match service host %q", r.HostID, hostID)
29 }
30 return validateSessionID(r.SessionID)
31 }
32
33 var (
34 ErrSessionNotRunning = errors.New("session runtime is not attached")
35 ErrRuntimeBusy = errors.New("session runtime already has an activity")
36 ErrRuntimeBound = errors.New("session runtime still has client bindings")
37 ErrRuntimeRetiring = errors.New("session runtime is retiring")
38 ErrRecoveryRequired = errors.New("session runtime requires recovery")
39 ErrStaleActivity = errors.New("session activity no longer owns commit authority")
40 ErrStaleExecution = errors.New("session execution generation no longer owns commit authority")
41 )
42
43 type RuntimePhase string
44
45 const (
46 RuntimeIdle RuntimePhase = "idle"
47 RuntimeRunning RuntimePhase = "running"
48 RuntimeCancelling RuntimePhase = "cancelling"
49 RuntimeFinalizing RuntimePhase = "finalizing"
50 RuntimeRecoveryRequired RuntimePhase = "recovery_required"
51 RuntimeClosed RuntimePhase = "closed"
52 )
53
54 type RuntimeSnapshot struct {
55 Ref SessionRef `json:"session"`
56 Epoch string `json:"runtimeEpoch"`
57 ActivityRevision uint64 `json:"activityRevision"`
58 Phase RuntimePhase `json:"phase"`
59 Activity string `json:"activity,omitempty"`
60 Session Snapshot `json:"sessionSnapshot"`
61 }
62
63 type CancelReceipt struct {
64 Ref SessionRef `json:"session"`
65 Accepted bool `json:"accepted"`
66 RuntimeEpoch string `json:"runtimeEpoch,omitempty"`
67 ActivityRevision uint64 `json:"activityRevision,omitempty"`
68 Phase RuntimePhase `json:"phase"`
69 }
70
71 // Runtime is the sole owner of a live Session and its write handle. Execution
72 // lifecycle lives in the bound turn-loop; persisted running events never
73 // create a Runtime after process restart.
74 type Runtime struct {
75 transcript *transcript.Projection
76 ref SessionRef
77 epoch string
78 session *Session
79 owner *Service
80 // instance stamps the publish grant so a delayed owner can prove it still
81 // refers to the exact instance it published.
82 instance string
83
84 mu sync.Mutex
85 phase RuntimePhase
86 activity string
87 revision atomic.Uint64
88 // execution is the generation-scoped turn-loop. Cancel loads it without
89 // taking mu so Stop never waits on a commit or persistence lock.
90 execution atomic.Pointer[executionBinding]
91 bindGen atomic.Uint64
92 canceling atomic.Bool
93 closeDone chan struct{}
94 closeErr error
95 }
96
97 func newRuntime(ref SessionRef, session *Session) (*Runtime, error) {
98 runtime, err := initializeRuntime(ref, session)
99 // Logging can perform I/O; release the session lock before emitting.
100 var diagnostic *TranscriptInitializationError
101 if errors.As(err, &diagnostic) {
102 slog.Error("session transcript initialization failed", "diagnostic", diagnostic)
103 }
104 return runtime, err
105 }
106
107 func initializeRuntime(ref SessionRef, session *Session) (*Runtime, error) {
108 session.mu.Lock()
109 defer session.mu.Unlock()
110 runtime := &Runtime{ref: ref, epoch: randomID(), session: session, phase: RuntimeIdle}
111 baseline := session.recentMessages
112 if len(baseline) == 0 {
113 baseline = session.projection.Messages
114 }
115 totalMessages := len(baseline)
116 if len(baseline) > 96 {
117 baseline = baseline[len(baseline)-96:]
118 }
119 projection, err := transcript.NewProjection(transcript.Identity{SessionID: ref.SessionID, RuntimeEpoch: runtime.epoch}, session.transcriptRows(baseline), session.next-1)
120 if err != nil {
121 return nil, &TranscriptInitializationError{sessionID: ref.SessionID, covered: session.next - 1,
122 messageCount: len(baseline), totalMessages: totalMessages, cause: err}
123 }
124 runtime.transcript = projection
125 durable := uint64(0)
126 if session.binding != nil {
127 durable, _, _ = session.binding.progress()
128 }
129 restored := transcript.Runtime{TurnID: session.projection.TurnID, Status: session.projection.TurnStatus, FinalMessageID: session.projection.CurrentTurnMessageID}
130 if receipt, ok := session.projection.Submissions.byTurn[session.id+"\x00"+restored.TurnID]; ok {
131 restored.SubmissionID = receipt.SubmissionID
132 }
133 restored.SamplingCount, restored.ToolCount = len(session.projection.CurrentAttempts), len(session.projection.CurrentCalls)
134 if restored.TurnID != "" && !restored.Status.Terminal() {
135 // A persisted open turn is recovery evidence, not a running model.
136 restored.Status = event.TurnRecoveryRequired
137 }
138 if restored.TurnID == "" && len(session.projection.Turns) > 0 {
139 last := session.projection.Turns[len(session.projection.Turns)-1]
140 restored.TurnID, restored.FinalMessageID = last.TurnID, last.MessageID
141 restored.DurationMs = last.DurationMs
142 restored.SamplingCount, restored.ToolCount = last.SamplingCount, last.ToolCount
143 }
144 for _, message := range baseline {
145 if message.ID == restored.FinalMessageID {
146 restored.DurationMs = max(restored.DurationMs, message.WorkDurationMs)
147 }
148 }
149 runtime.transcript.RestoreRuntime(restored, durable)
150 session.transcript = runtime.transcript
151 runtime.revision.Store(1)
152 return runtime, nil
153 }
154
155 func (r *Runtime) Ref() SessionRef { return r.ref }
156
157 func (r *Runtime) Session() *Session { return r.session }
158
159 func (r *Runtime) Snapshot() RuntimeSnapshot {
160 state := r.activitySnapshot()
161 state.Session = r.session.Snapshot()
162 return state
163 }
164
165 func (r *Runtime) StateSnapshot() RuntimeSnapshot {
166 state := r.activitySnapshot()
167 state.Session = r.session.StateSnapshot()
168 return state
169 }
170
171 // ExecutionSnapshot returns the provider projection and lightweight turn
172 // boundaries without reconstructing the durable UI transcript.
173 func (r *Runtime) ExecutionSnapshot() RuntimeSnapshot {
174 state := r.activitySnapshot()
175 state.Session = r.session.ExecutionSnapshot()
176 return state
177 }
178
179 func (r *Runtime) activitySnapshot() RuntimeSnapshot {
180 r.mu.Lock()
181 phase := r.phase
182 if r.canceling.Load() && phase == RuntimeRunning {
183 phase = RuntimeCancelling
184 }
185 state := RuntimeSnapshot{Ref: r.ref, Epoch: r.epoch, ActivityRevision: r.revision.Load(), Phase: phase, Activity: r.activity}
186 r.mu.Unlock()
187 return state
188 }
189
190 // Cancel forwards Stop to the bound turn-loop without taking the runtime
191 // mutex. An unbound runtime is already idle.
192 func (r *Runtime) Cancel() bool {
193 for {
194 exec := r.loadExecution()
195 if exec == nil || exec.control == nil {
196 return false
197 }
198 if !exec.control.Cancel() {
199 // A host cutover may linearize while Cancel is inside the outgoing
200 // loop. Retry only when ownership actually changed; a stable owner
201 // rejecting Cancel remains a normal idle result.
202 if r.loadExecution() != exec {
203 continue
204 }
205 return false
206 }
207 break
208 }
209 r.canceling.Store(true)
210 if r.mu.TryLock() {
211 if r.phase == RuntimeRunning {
212 r.phase = RuntimeCancelling
213 r.activity = "cancelling"
214 r.revision.Add(1)
215 }
216 r.mu.Unlock()
217 }
218 return true
219 }
220
221 func (r *Runtime) RequireRecovery(activity string) {
222 r.mu.Lock()
223 defer r.mu.Unlock()
224 if r.phase == RuntimeClosed {
225 return
226 }
227 r.phase = RuntimeRecoveryRequired
228 r.activity = activity
229 r.revision.Add(1)
230 }
231
232 func (r *Runtime) close(ctx context.Context) error {
233 r.mu.Lock()
234 if r.closeDone != nil {
235 done := r.closeDone
236 r.mu.Unlock()
237 <-done
238 return r.closeErr
239 }
240 if r.phase.busy() && r.loadExecution() != nil {
241 r.mu.Unlock()
242 return ErrRuntimeBusy
243 }
244 // Seal admission in the same critical section as the idle check. The
245 // irreversible close has one uncancellable result for every caller.
246 r.closeDone = make(chan struct{})
247 r.phase = RuntimeClosed
248 r.activity = ""
249 r.revision.Add(1)
250 r.mu.Unlock()
251 r.transcript.CloseFollowers()
252 r.closeErr = r.session.close(context.Background())
253 close(r.closeDone)
254 return r.closeErr
255 }
256
257 func osClosedError() error { return errors.New("session runtime is closed") }
258
259 // Service applies DSH's prepare/publish/exact-detach rule. Candidate handles
260 // are opened outside the registry lock; only the exact published Runtime can
261 // later unregister itself.
262 type Service struct {
263 hostID string
264 persistence SessionPersistence
265
266 mu sync.Mutex
267 active map[SessionRef]*Runtime
268 closed map[SessionRef]error
269 preparing map[SessionRef]*prepareRuntime
270 bindings map[*Runtime]int
271 retiring map[*Runtime]chan struct{}
272 retireIdle map[*Runtime]bool
273 idleTimers map[*Runtime]*time.Timer
274 idleWeight map[*Runtime]int64
275 idleOrder map[*Runtime]uint64
276 idleUsed int64
277 idleClock uint64
278 idleBudget int64
279 idleTTL time.Duration
280 query *Query
281 revision atomic.Uint64
282 }
283
284 func (s *Service) Cancel(ref SessionRef) (RuntimeSnapshot, error) {
285 runtime, ok := s.Runtime(ref)
286 if !ok {
287 return RuntimeSnapshot{}, ErrSessionNotRunning
288 }
289 runtime.Cancel()
290 return runtime.StateSnapshot(), nil
291 }
292
293 // CancelSession is the public session-scoped Stop contract. A missing runtime
294 // is already idle and therefore succeeds idempotently; no caller-supplied turn
295 // id participates in routing or authorization.
296 func (s *Service) CancelSession(ref SessionRef) (CancelReceipt, error) {
297 if err := ref.validate(s.hostID); err != nil {
298 return CancelReceipt{}, err
299 }
300 runtime, ok := s.Runtime(ref)
301 if !ok {
302 return CancelReceipt{Ref: ref, Accepted: true, Phase: RuntimeIdle}, nil
303 }
304 if runtime.Cancel() {
305 return CancelReceipt{
306 Ref: ref,
307 Accepted: true,
308 RuntimeEpoch: runtime.epoch,
309 ActivityRevision: runtime.revision.Load(),
310 Phase: RuntimeCancelling,
311 }, nil
312 }
313 snapshot := runtime.activitySnapshot()
314 return CancelReceipt{Ref: ref, Accepted: true, RuntimeEpoch: snapshot.Epoch, ActivityRevision: snapshot.ActivityRevision, Phase: snapshot.Phase}, nil
315 }
316
317 func (s *Service) Flush(ctx context.Context, ref SessionRef) (DurableReceipt, error) {
318 runtime, ok := s.Runtime(ref)
319 if !ok {
320 return DurableReceipt{}, ErrSessionNotRunning
321 }
322 return runtime.session.Flush(ctx)
323 }
324
325 // ContinueLegacy freezes one legacy head, publishes its deterministic final
326 // session, then attaches that exact session. It never writes the source and it
327 // does not accept the caller's pending submission; hosts enqueue the unchanged
328 // submission only after this method returns the new immutable identity.
329 func (s *Service) ContinueLegacy(ctx context.Context, sourcePath, headID string) (*Runtime, MigrationResult, error) {
330 filesystem, ok := s.persistence.(*FilesystemPersistence)
331 if !ok {
332 return nil, MigrationResult{}, errors.New("session: persistence does not support legacy migration")
333 }
334 result, err := migrateLegacyHeadForHost(ctx, sourcePath, filesystem.Root, headID)
335 if err != nil {
336 return nil, result, err
337 }
338 runtime, err := s.openRuntime(ctx, SessionRef{HostID: s.hostID, SessionID: result.TargetID})
339 return runtime, result, err
340 }
341
342 // ContinueImported resolves the paired legacy transcript and retired event
343 // sidecar as one frozen migration decision. It refuses divergent histories
344 // instead of letting a caller accidentally resume whichever source it opened
345 // first.
346 func (s *Service) ContinueImported(ctx context.Context, sourcePath, headID string) (*Runtime, ImportResult, error) {
347 return s.ContinueImportedWithHeader(ctx, sourcePath, headID, CreateOptions{})
348 }
349
350 // ContinueImportedWithHeader publishes Desktop ownership in the same atomic
351 // directory publication as the imported history.
352 func (s *Service) ContinueImportedWithHeader(ctx context.Context, sourcePath, headID string, options CreateOptions) (*Runtime, ImportResult, error) {
353 filesystem, ok := s.persistence.(*FilesystemPersistence)
354 if !ok {
355 return nil, ImportResult{}, errors.New("session: persistence does not support imported sessions")
356 }
357 result, err := importSourceForLegacyWithHeader(ctx, sourcePath, filesystem.Root, headID, options)
358 if err != nil {
359 return nil, result, err
360 }
361 runtime, err := s.openRuntime(ctx, SessionRef{HostID: s.hostID, SessionID: result.TargetID})
362 return runtime, result, err
363 }
364
365 // ContinuePrototype is the explicit, fail-closed bridge for the retired
366 // sidecar codec. Unknown required events or conflicting tails remain read-only.
367 func (s *Service) ContinuePrototype(ctx context.Context, sourceDir string) (*Runtime, PrototypeImportResult, error) {
368 filesystem, ok := s.persistence.(*FilesystemPersistence)
369 if !ok {
370 return nil, PrototypeImportResult{}, errors.New("session: persistence does not support prototype import")
371 }
372 result, err := ImportPrototype(ctx, sourceDir, filesystem.Root)
373 if err != nil {
374 return nil, result, err
375 }
376 runtime, err := s.openRuntime(ctx, SessionRef{HostID: s.hostID, SessionID: result.TargetID})
377 return runtime, result, err
378 }
379
380 // ContinueImportedFrom resolves legacy history against its original paired
381 // store while publishing only into this service's separate staging root.
382 func (s *Service) ContinueImportedFrom(ctx context.Context, sourcePath, sourceRoot, headID string) (*Runtime, ImportResult, error) {
383 return s.ContinueImportedSource(ctx, sourcePath, filepath.Join(sourceRoot, agent.BranchID(sourcePath)), headID)
384 }
385
386 // ContinueImportedSource accepts a provenance-linked directory whose identity
387 // may have changed when a legacy head was previously converted.
388 func (s *Service) ContinueImportedSource(ctx context.Context, sourcePath, sourceDir, headID string) (*Runtime, ImportResult, error) {
389 filesystem, ok := s.persistence.(*FilesystemPersistence)
390 if !ok {
391 return nil, ImportResult{}, errors.New("session: persistence does not support imported sessions")
392 }
393 result, err := importSourceForLegacyAt(ctx, sourcePath, sourceDir, filesystem.Root, headID, CreateOptions{})
394 if err != nil {
395 return nil, result, err
396 }
397 sourceRoot := filepath.Dir(sourceDir)
398 if result.Kind == "final" && filepath.Clean(sourceRoot) != filepath.Clean(filesystem.Root) {
399 tmp, err := os.MkdirTemp("", "reasonix-canonical-stage-")
400 if err != nil {
401 return nil, result, err
402 }
403 defer os.RemoveAll(tmp)
404 bundle := filepath.Join(tmp, "bundle")
405 if err := NewFilesystemPersistence(sourceRoot).exportCold(ctx, result.TargetID, bundle); err != nil {
406 return nil, result, err
407 }
408 if _, err := s.Import(ctx, bundle); err != nil {
409 return nil, result, err
410 }
411 }
412 runtime, err := s.openRuntime(ctx, SessionRef{HostID: s.hostID, SessionID: result.TargetID})
413 return runtime, result, err
414 }
415
416 // ContinueStoredPreview upgrades a pre-ownership linear store selected by its
417 // former session id. The old directory remains read-only; execution resumes on
418 // the deterministic final-codec identity returned here.
419 func (s *Service) ContinueStoredPreview(ctx context.Context, sessionID string) (*Runtime, PrototypeImportResult, error) {
420 filesystem, ok := s.persistence.(*FilesystemPersistence)
421 if !ok {
422 return nil, PrototypeImportResult{}, errors.New("session: persistence does not support preview import")
423 }
424 if err := validateSessionID(sessionID); err != nil {
425 return nil, PrototypeImportResult{}, err
426 }
427 sourceDir, err := filesystem.sessionDir(sessionID, true)
428 if err != nil {
429 return nil, PrototypeImportResult{}, err
430 }
431 result, err := ImportStoredPreview(ctx, sourceDir, filesystem.Root)
432 if err != nil {
433 return nil, result, err
434 }
435 runtime, err := s.openRuntime(ctx, SessionRef{HostID: s.hostID, SessionID: result.TargetID})
436 return runtime, result, err
437 }
438
439 // Fork creates an independent child at the exact end event of a completed
440 // turn. No message-count inference is involved.
441 func (s *Service) Fork(ctx context.Context, ref SessionRef, afterTurnID, childID string) (*Runtime, error) {
442 runtime, ok := s.Runtime(ref)
443 if !ok {
444 return nil, ErrSessionNotRunning
445 }
446 turn, ok := completedTurn(runtime.session.Snapshot().Projection.Turns, afterTurnID)
447 if !ok {
448 return nil, fmt.Errorf("session: completed turn %q not found", afterTurnID)
449 }
450 return s.forkAt(ctx, runtime, turn.EndSequence, childID)
451 }
452
453 // Rewind creates a child from the event immediately before beforeTurnID.
454 func (s *Service) Rewind(ctx context.Context, ref SessionRef, beforeTurnID, childID string) (*Runtime, error) {
455 runtime, ok := s.Runtime(ref)
456 if !ok {
457 return nil, ErrSessionNotRunning
458 }
459 turn, ok := completedTurn(runtime.session.Snapshot().Projection.Turns, beforeTurnID)
460 if !ok {
461 return nil, fmt.Errorf("session: completed turn %q not found", beforeTurnID)
462 }
463 return s.forkAt(ctx, runtime, turn.StartSequence-1, childID)
464 }
465
466 func completedTurn(turns []TurnBoundary, id string) (TurnBoundary, bool) {
467 for _, turn := range turns {
468 if turn.TurnID == id {
469 return turn, true
470 }
471 }
472 return TurnBoundary{}, false
473 }
474
475 func (s *Service) forkAt(ctx context.Context, parent *Runtime, sequence uint64, childID string) (*Runtime, error) {
476 filesystem, ok := s.persistence.(*FilesystemPersistence)
477 if !ok {
478 return nil, errors.New("session: persistence does not support filesystem fork")
479 }
480 if childID == "" {
481 childID = randomID()
482 }
483 if err := validateSessionID(childID); err != nil {
484 return nil, err
485 }
486 childDir := filepath.Join(filesystem.Root, childID)
487 if _, err := parent.session.Fork(ctx, childDir, childID, sequence); err != nil {
488 return nil, err
489 }
490 return s.openRuntime(ctx, SessionRef{HostID: s.hostID, SessionID: childID})
491 }
492
493 func (s *Service) Close(ctx context.Context, ref SessionRef) error {
494 if err := ref.validate(s.hostID); err != nil {
495 return err
496 }
497 s.mu.Lock()
498 runtime := s.active[ref]
499 closedErr, closed := s.closed[ref]
500 s.mu.Unlock()
501 if runtime == nil {
502 if closed {
503 return closedErr
504 }
505 return ErrSessionNotRunning
506 }
507 return s.closeOwned(ctx, runtime, "")
508 }
509
510 // closeOwned is the teardown entry point for a RuntimeOwner holding one exact
511 // instance grant. A delayed old disposer must never close its same-ID
512 // successor, and a client-bound runtime is never torn down underneath it.
513 func (s *Service) closeOwned(ctx context.Context, runtime *Runtime, instance string) error {
514 return s.closeRuntime(ctx, runtime, instance, false)
515 }
516
517 // closeRuntime adds the terminal variant Shutdown needs. Refusing a bound
518 // runtime is right while the process keeps running, but at shutdown it would
519 // strand the writer lease and recovery handles for the process lifetime, so
520 // the final teardown releases them and still reports the leaked binding.
521 func (s *Service) closeRuntime(ctx context.Context, runtime *Runtime, instance string, terminal bool) error {
522 if runtime == nil {
523 return ErrSessionNotRunning
524 }
525 if err := runtime.ref.validate(s.hostID); err != nil {
526 return err
527 }
528 if instance != "" && runtime.instance != instance {
529 return ErrSessionNotRunning
530 }
531 var leaked error
532 s.mu.Lock()
533 if timer := s.idleTimers[runtime]; timer != nil {
534 s.removeIdleCacheLocked(runtime, true)
535 }
536 if s.bindings[runtime] != 0 {
537 if !terminal {
538 s.mu.Unlock()
539 return ErrRuntimeBound
540 }
541 delete(s.bindings, runtime)
542 leaked = fmt.Errorf("%w: %s", ErrRuntimeBound, runtime.ref.SessionID)
543 }
544 if s.active[runtime.ref] != runtime {
545 s.mu.Unlock()
546 return errors.Join(leaked, runtime.close(ctx))
547 }
548 if done := s.retiring[runtime]; done != nil {
549 s.mu.Unlock()
550 select {
551 case <-done:
552 return errors.Join(leaked, runtime.close(ctx))
553 case <-ctx.Done():
554 return errors.Join(leaked, ctx.Err())
555 }
556 }
557 done := make(chan struct{})
558 s.retiring[runtime] = done
559 s.mu.Unlock()
560 err := runtime.close(ctx)
561 s.mu.Lock()
562 delete(s.retiring, runtime)
563 if !errors.Is(err, ErrRuntimeBusy) && s.active[runtime.ref] == runtime {
564 delete(s.active, runtime.ref)
565 delete(s.retireIdle, runtime)
566 s.removeIdleCacheLocked(runtime, true)
567 s.closed[runtime.ref] = err
568 s.revision.Add(1)
569 }
570 close(done)
571 s.mu.Unlock()
572 return errors.Join(leaked, err)
573 }
574
575 // Detach removes a runtime only if it is still the exact published instance.
576 // It is used by host callbacks that may arrive after a replacement.
577 func (s *Service) Detach(runtime *Runtime) bool {
578 if runtime == nil {
579 return false
580 }
581 s.mu.Lock()
582 defer s.mu.Unlock()
583 if s.active[runtime.ref] != runtime {
584 return false
585 }
586 if timer := s.idleTimers[runtime]; timer != nil {
587 s.removeIdleCacheLocked(runtime, true)
588 }
589 delete(s.active, runtime.ref)
590 s.revision.Add(1)
591 return true
592 }
593
594 type ObserveResult struct {
595 Runtime *RuntimeSnapshot `json:"runtime,omitempty"`
596 Events EventPage `json:"events"`
597 }
598
599 // SessionDir resolves the on-disk directory of a final-format identity
600 // without opening it. Hosts use it to probe writer occupancy for takeover
601 // flows; the writer lease itself is never taken here.
602 func (s *Service) SessionDir(ctx context.Context, ref SessionRef) (string, error) {
603 if err := ref.validate(s.hostID); err != nil {
604 return "", err
605 }
606 info, err := s.persistence.Stat(ctx, ref.SessionID)
607 if err != nil {
608 return "", err
609 }
610 return info.Path, nil
611 }
612
613 func (s *Service) Observe(ctx context.Context, ref SessionRef, cursor uint64, limit int) (ObserveResult, error) {
614 if err := ref.validate(s.hostID); err != nil {
615 return ObserveResult{}, err
616 }
617 if runtime, ok := s.Runtime(ref); ok {
618 // Observe reports runtime state plus an explicitly paged event tail. It
619 // must not duplicate the provider model workset into every poll.
620 snapshot := runtime.StateSnapshot()
621 page, err := runtime.session.AcceptedPage(ctx, cursor, limit)
622 return ObserveResult{Runtime: &snapshot, Events: page}, err
623 }
624 handle, err := s.persistence.Open(ref.SessionID, ReadOnly)
625 if err != nil {
626 return ObserveResult{}, err
627 }
628 defer handle.Close(context.Background())
629 page, err := handle.Read(ctx, cursor, limit)
630 return ObserveResult{Events: page}, err
631 }
632
632 lines GO