返回 DeepSeek-Reasonix
1 package workspacestate
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "os"
10 "path/filepath"
11 "slices"
12 "strings"
13 "sync"
14 "time"
15
16 "reasonix/internal/filelock"
17 "reasonix/internal/fileutil"
18 )
19
20 const (
21 SchemaVersion = 2
22 GlobalWorkspaceID = "global"
23 )
24
25 var (
26 ErrUnsupportedVersion = errors.New("workspace state version is unsupported")
27 ErrWorkspaceNotFound = errors.New("workspace is not registered")
28 ErrSessionNotFound = errors.New("session is not registered")
29 ErrMutationConflict = errors.New("workspace mutation conflicts with persisted state")
30 )
31
32 type Workspace struct {
33 ID string `json:"id"`
34 Root string `json:"root"`
35 Title string `json:"title"`
36 SessionIDs []string `json:"sessionIds"`
37 Visible bool `json:"visible"`
38 CreatedAt time.Time `json:"createdAt"`
39 UpdatedAt time.Time `json:"updatedAt"`
40 extra map[string]json.RawMessage
41 }
42
43 type PendingCreate struct {
44 OperationID string `json:"operationId"`
45 WorkspaceID string `json:"workspaceId"`
46 SessionID string `json:"sessionId"`
47 CreatedAt time.Time `json:"createdAt"`
48 ArchiveSource string `json:"archiveSource,omitempty"`
49 extra map[string]json.RawMessage
50 }
51
52 type State struct {
53 Version int `json:"version"`
54 Generation uint64 `json:"generation"`
55 Initialized bool `json:"initialized"`
56 WorkspaceIDs []string `json:"workspaceIds"`
57 Workspaces map[string]Workspace `json:"workspaces"`
58 ArchivedSessionIDs []string `json:"archivedSessionIds"`
59 PendingCreates map[string]PendingCreate `json:"pendingCreates"`
60 SessionStates map[string]SessionState `json:"sessionStates"`
61 SourceMappings map[string]SourceMapping `json:"sourceMappings"`
62 PendingOperations map[string]Operation `json:"pendingOperations"`
63 RecoveryEntries map[string]RecoveryEntry `json:"recoveryEntries"`
64 Presentation map[string]Presentation `json:"presentation"`
65 extra map[string]json.RawMessage
66 }
67
68 type Store struct {
69 path string
70 mu sync.Mutex
71 beforeUpgrade func(context.Context) error
72 }
73
74 func NewStore(path string, beforeUpgrade ...func(context.Context) error) *Store {
75 store := &Store{path: filepath.Clean(path)}
76 if len(beforeUpgrade) > 0 {
77 store.beforeUpgrade = beforeUpgrade[0]
78 }
79 return store
80 }
81
82 func (s *Store) Path() string {
83 if s == nil {
84 return ""
85 }
86 return s.path
87 }
88
89 func (s *Store) Load(ctx context.Context) (State, error) {
90 if s == nil || strings.TrimSpace(s.path) == "" || s.path == "." {
91 return State{}, errors.New("workspace state path is required")
92 }
93 s.mu.Lock()
94 defer s.mu.Unlock()
95 if err := ctx.Err(); err != nil {
96 return State{}, err
97 }
98 state, err := load(s.path)
99 if err != nil {
100 return State{}, err
101 }
102 return cloneState(state)
103 }
104
105 func (s *Store) EnsureWorkspace(ctx context.Context, workspace Workspace) error {
106 workspace.ID = strings.TrimSpace(workspace.ID)
107 if workspace.ID == "" {
108 return errors.New("workspace id is required")
109 }
110 return s.mutate(ctx, func(state *State) error {
111 now := time.Now().UTC()
112 current, exists := state.Workspaces[workspace.ID]
113 if exists {
114 if current.Root != workspace.Root {
115 return ErrMutationConflict
116 }
117 return nil
118 }
119 workspace.SessionIDs = []string{}
120 workspace.CreatedAt, workspace.UpdatedAt = now, now
121 state.Workspaces[workspace.ID] = workspace
122 state.WorkspaceIDs = append(state.WorkspaceIDs, workspace.ID)
123 return nil
124 })
125 }
126
127 func (s *Store) RenameWorkspace(ctx context.Context, workspaceID, title string) error {
128 return s.mutate(ctx, func(state *State) error {
129 workspace, ok := state.Workspaces[strings.TrimSpace(workspaceID)]
130 if !ok {
131 return ErrWorkspaceNotFound
132 }
133 workspace.Title = strings.TrimSpace(title)
134 workspace.UpdatedAt = time.Now().UTC()
135 state.Workspaces[workspace.ID] = workspace
136 return nil
137 })
138 }
139
140 func (s *Store) SetWorkspaceVisible(ctx context.Context, workspaceID string, visible bool) error {
141 return s.mutate(ctx, func(state *State) error {
142 workspace, ok := state.Workspaces[strings.TrimSpace(workspaceID)]
143 if !ok {
144 return ErrWorkspaceNotFound
145 }
146 workspace.Visible = visible
147 workspace.UpdatedAt = time.Now().UTC()
148 state.Workspaces[workspace.ID] = workspace
149 return nil
150 })
151 }
152
153 func (s *Store) MoveWorkspace(ctx context.Context, workspaceID, beforeWorkspaceID string) error {
154 workspaceID = strings.TrimSpace(workspaceID)
155 beforeWorkspaceID = strings.TrimSpace(beforeWorkspaceID)
156 return s.mutate(ctx, func(state *State) error {
157 if _, ok := state.Workspaces[workspaceID]; !ok {
158 return ErrWorkspaceNotFound
159 }
160 if beforeWorkspaceID != "" {
161 if _, ok := state.Workspaces[beforeWorkspaceID]; !ok {
162 return ErrWorkspaceNotFound
163 }
164 }
165 state.WorkspaceIDs = insertBefore(remove(state.WorkspaceIDs, workspaceID), workspaceID, beforeWorkspaceID)
166 return nil
167 })
168 }
169
170 func (s *Store) BeginCreate(ctx context.Context, pending PendingCreate) error {
171 pending.OperationID = strings.TrimSpace(pending.OperationID)
172 pending.WorkspaceID = strings.TrimSpace(pending.WorkspaceID)
173 pending.SessionID = strings.TrimSpace(pending.SessionID)
174 if pending.OperationID == "" || pending.WorkspaceID == "" || pending.SessionID == "" {
175 return errors.New("pending create requires operation, workspace, and session ids")
176 }
177 return s.mutate(ctx, func(state *State) error {
178 if _, ok := state.Workspaces[pending.WorkspaceID]; !ok {
179 return ErrWorkspaceNotFound
180 }
181 if state.SessionStates[pending.SessionID].Lifecycle == Deleted {
182 return ErrMutationConflict
183 }
184 if current, ok := state.PendingCreates[pending.SessionID]; ok {
185 if current.OperationID == pending.OperationID && current.WorkspaceID == pending.WorkspaceID {
186 return nil
187 }
188 return ErrMutationConflict
189 }
190 if owner, ok := sessionOwner(*state, pending.SessionID); ok && owner != pending.WorkspaceID {
191 return ErrMutationConflict
192 }
193 if pending.CreatedAt.IsZero() {
194 pending.CreatedAt = time.Now().UTC()
195 }
196 state.PendingCreates[pending.SessionID] = pending
197 return nil
198 })
199 }
200
201 func (s *Store) AttachSession(ctx context.Context, operationID, workspaceID, sessionID, beforeSessionID string) error {
202 return s.attachSession(ctx, operationID, workspaceID, sessionID, beforeSessionID, "", nil)
203 }
204
205 // AttachSessionFromSourceIfUnchanged publishes a derived child only while its
206 // resolved source is still active, in the same workspace, and at the same
207 // lifecycle generation.
208 func (s *Store) AttachSessionFromSourceIfUnchanged(
209 ctx context.Context,
210 operationID, workspaceID, sessionID, beforeSessionID, sourceSessionID string,
211 sourceGeneration uint64,
212 ) error {
213 return s.attachSession(ctx, operationID, workspaceID, sessionID, beforeSessionID, sourceSessionID, &sourceGeneration)
214 }
215
216 func (s *Store) attachSession(
217 ctx context.Context,
218 operationID, workspaceID, sessionID, beforeSessionID, sourceSessionID string,
219 sourceGeneration *uint64,
220 ) error {
221 operationID, workspaceID, sessionID = strings.TrimSpace(operationID), strings.TrimSpace(workspaceID), strings.TrimSpace(sessionID)
222 if workspaceID == "" || sessionID == "" {
223 return errors.New("attach requires workspace and session ids")
224 }
225 return s.mutate(ctx, func(state *State) error {
226 if sourceGeneration != nil {
227 sourceSessionID = strings.TrimSpace(sourceSessionID)
228 sourceState := state.SessionStates[sourceSessionID]
229 sourceOwner, owned := sessionOwner(*state, sourceSessionID)
230 if sourceSessionID == "" || !owned || sourceOwner != workspaceID ||
231 sourceState.Lifecycle != Active || sourceState.Generation != *sourceGeneration {
232 return ErrMutationConflict
233 }
234 }
235 if state.SessionStates[sessionID].Lifecycle == Deleted {
236 return ErrMutationConflict
237 }
238 workspace, ok := state.Workspaces[workspaceID]
239 if !ok {
240 return ErrWorkspaceNotFound
241 }
242 if owner, owned := sessionOwner(*state, sessionID); owned {
243 if owner != workspaceID {
244 return ErrMutationConflict
245 }
246 delete(state.PendingCreates, sessionID)
247 return nil
248 }
249 if operationID != "" {
250 pending, ok := state.PendingCreates[sessionID]
251 if !ok || pending.OperationID != operationID || pending.WorkspaceID != workspaceID {
252 return ErrMutationConflict
253 }
254 }
255 workspace.SessionIDs = insertBefore(workspace.SessionIDs, sessionID, beforeSessionID)
256 workspace.UpdatedAt = time.Now().UTC()
257 state.Workspaces[workspaceID] = workspace
258 delete(state.PendingCreates, sessionID)
259 return nil
260 })
261 }
262
263 // CommitRotation atomically publishes a prepared replacement into its
264 // workspace and, for Clear, archives the source without removing its stable
265 // position from the registry.
266 func (s *Store) CommitRotation(ctx context.Context, operationID, workspaceID, sessionID, beforeSessionID, archiveSessionID string) error {
267 operationID, workspaceID, sessionID = strings.TrimSpace(operationID), strings.TrimSpace(workspaceID), strings.TrimSpace(sessionID)
268 archiveSessionID = strings.TrimSpace(archiveSessionID)
269 if operationID == "" || workspaceID == "" || sessionID == "" {
270 return errors.New("rotation commit requires operation, workspace, and session ids")
271 }
272 return s.mutate(ctx, func(state *State) error {
273 workspace, ok := state.Workspaces[workspaceID]
274 if !ok {
275 return ErrWorkspaceNotFound
276 }
277 pending, ok := state.PendingCreates[sessionID]
278 if !ok || pending.OperationID != operationID || pending.WorkspaceID != workspaceID {
279 if owner, attached := sessionOwner(*state, sessionID); !attached || owner != workspaceID {
280 return ErrMutationConflict
281 }
282 } else if owner, attached := sessionOwner(*state, sessionID); attached && owner != workspaceID {
283 return ErrMutationConflict
284 } else if !attached {
285 workspace.SessionIDs = insertBefore(workspace.SessionIDs, sessionID, beforeSessionID)
286 workspace.UpdatedAt = time.Now().UTC()
287 state.Workspaces[workspaceID] = workspace
288 }
289 delete(state.PendingCreates, sessionID)
290 if archiveSessionID != "" {
291 if _, exists := sessionOwner(*state, archiveSessionID); !exists {
292 return ErrSessionNotFound
293 }
294 setLifecycle(state, archiveSessionID, Archived)
295 }
296 return nil
297 })
298 }
299
300 func (s *Store) AbortCreate(ctx context.Context, sessionID string) error {
301 return s.mutate(ctx, func(state *State) error {
302 delete(state.PendingCreates, strings.TrimSpace(sessionID))
303 return nil
304 })
305 }
306
307 func (s *Store) MoveSession(ctx context.Context, workspaceID, sessionID, beforeSessionID string) error {
308 return s.moveSession(ctx, workspaceID, sessionID, beforeSessionID, nil)
309 }
310
311 // MoveSessionIfUnchanged reorders one active session only while the caller's
312 // resolved lifecycle generation and workspace owner remain current.
313 func (s *Store) MoveSessionIfUnchanged(ctx context.Context, workspaceID, sessionID, beforeSessionID string, generation uint64) error {
314 return s.moveSession(ctx, workspaceID, sessionID, beforeSessionID, &generation)
315 }
316
317 func (s *Store) moveSession(ctx context.Context, workspaceID, sessionID, beforeSessionID string, generation *uint64) error {
318 return s.mutate(ctx, func(state *State) error {
319 workspace, ok := state.Workspaces[strings.TrimSpace(workspaceID)]
320 if !ok {
321 return ErrWorkspaceNotFound
322 }
323 if !contains(workspace.SessionIDs, sessionID) {
324 return ErrSessionNotFound
325 }
326 status := state.SessionStates[sessionID]
327 if status.Lifecycle != Active {
328 return ErrSessionNotFound
329 }
330 if generation != nil && status.Generation != *generation {
331 return ErrMutationConflict
332 }
333 workspace.SessionIDs = insertBefore(remove(workspace.SessionIDs, sessionID), sessionID, beforeSessionID)
334 workspace.UpdatedAt = time.Now().UTC()
335 state.Workspaces[workspace.ID] = workspace
336 status.Generation++
337 state.SessionStates[sessionID] = status
338 return nil
339 })
340 }
341
342 func (s *Store) ArchiveSession(ctx context.Context, sessionID string) error {
343 return s.SetLifecycle(ctx, []string{sessionID}, Archived)
344 }
345
346 func (s *Store) RestoreSession(ctx context.Context, sessionID string) error {
347 return s.SetLifecycle(ctx, []string{sessionID}, Active)
348 }
349
350 func (s *Store) Contains(ctx context.Context, sessionID string) (bool, error) {
351 state, err := s.Load(ctx)
352 if err != nil {
353 return false, err
354 }
355 _, ok := sessionOwner(state, strings.TrimSpace(sessionID))
356 return ok, nil
357 }
358
359 // WithSessionUnchanged serializes a durable metadata commit with lifecycle and
360 // workspace changes, including writers in other processes. The callback must
361 // not call the registry; it may only commit session content metadata.
362 func (s *Store) WithSessionUnchanged(ctx context.Context, id, workspaceID string, generation uint64, commit func() error) error {
363 s.mu.Lock()
364 defer s.mu.Unlock()
365 release, err := filelock.Acquire(ctx, s.path+".lock")
366 if err != nil {
367 return err
368 }
369 defer release()
370 state, err := load(s.path)
371 if err != nil {
372 return err
373 }
374 owner, ok := sessionOwner(state, id)
375 status := state.SessionStates[id]
376 if !ok || status.Lifecycle != Active {
377 return ErrSessionNotFound
378 }
379 if owner != workspaceID || status.Generation != generation {
380 return ErrMutationConflict
381 }
382 return commit()
383 }
384
385 func (s *Store) mutate(ctx context.Context, change func(*State) error) error {
386 if s == nil || strings.TrimSpace(s.path) == "" || s.path == "." {
387 return errors.New("workspace state path is required")
388 }
389 s.mu.Lock()
390 defer s.mu.Unlock()
391 if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil {
392 return err
393 }
394 release, err := filelock.Acquire(ctx, s.path+".lock")
395 if err != nil {
396 return err
397 }
398 defer release()
399 upgrading := false
400 if body, err := os.ReadFile(s.path); err == nil {
401 var header struct {
402 Version int `json:"version"`
403 }
404 upgrading = json.Unmarshal(body, &header) == nil && header.Version == 1
405 }
406 if s.beforeUpgrade != nil {
407 body, readErr := os.ReadFile(s.path)
408 if readErr != nil && !os.IsNotExist(readErr) {
409 return readErr
410 }
411 var header struct {
412 Version int `json:"version"`
413 }
414 if len(body) > 0 && json.Unmarshal(body, &header) == nil && header.Version == 1 {
415 if err := s.beforeUpgrade(ctx); err != nil {
416 return err
417 }
418 }
419 }
420 if err := backupV1(s.path); err != nil {
421 return err
422 }
423 state, err := load(s.path)
424 if err != nil {
425 return err
426 }
427 before, err := json.Marshal(state)
428 if err != nil {
429 return err
430 }
431 if err := change(&state); err != nil {
432 return err
433 }
434 after, err := json.Marshal(state)
435 if err != nil {
436 return err
437 }
438 if bytes.Equal(before, after) && !upgrading {
439 return nil
440 }
441 state.Generation++
442 state.Initialized = true
443 normalize(&state)
444 if err := validate(state); err != nil {
445 return err
446 }
447 body, err := json.Marshal(state)
448 if err != nil {
449 return err
450 }
451 return fileutil.AtomicWriteFileStrict(s.path, append(body, '\n'), 0o600)
452 }
453
454 func load(path string) (State, error) {
455 body, err := os.ReadFile(path)
456 if errors.Is(err, os.ErrNotExist) {
457 return newState(), nil
458 }
459 if err != nil {
460 return State{}, err
461 }
462 var state State
463 if err := json.Unmarshal(body, &state); err != nil {
464 return State{}, fmt.Errorf("decode workspace state: %w", err)
465 }
466 if state.Version != 1 && state.Version != SchemaVersion {
467 return State{}, fmt.Errorf("%w: %d", ErrUnsupportedVersion, state.Version)
468 }
469 if state.Version == 1 {
470 state.SessionStates = map[string]SessionState{}
471 for _, id := range state.ArchivedSessionIDs {
472 state.SessionStates[id] = SessionState{Lifecycle: Archived, Generation: state.Generation}
473 }
474 state.Version = SchemaVersion
475 } else {
476 if state.WorkspaceIDs == nil || state.Workspaces == nil || state.SessionStates == nil {
477 return State{}, fmt.Errorf("%w: missing required registry fields", ErrUnsupportedVersion)
478 }
479 for _, workspace := range state.Workspaces {
480 for _, id := range workspace.SessionIDs {
481 if _, ok := state.SessionStates[id]; !ok {
482 return State{}, fmt.Errorf("%w: missing session lifecycle", ErrUnsupportedVersion)
483 }
484 }
485 }
486 }
487 normalize(&state)
488 if err := validate(state); err != nil {
489 return State{}, err
490 }
491 return state, nil
492 }
493
494 func newState() State {
495 state := State{Version: SchemaVersion, WorkspaceIDs: []string{}, Workspaces: map[string]Workspace{}, ArchivedSessionIDs: []string{}, PendingCreates: map[string]PendingCreate{}}
496 normalize(&state)
497 return state
498 }
499
500 func normalize(state *State) {
501 if state.SessionStates == nil {
502 state.SessionStates = map[string]SessionState{}
503 }
504 if state.SourceMappings == nil {
505 state.SourceMappings = map[string]SourceMapping{}
506 }
507 if state.PendingOperations == nil {
508 state.PendingOperations = map[string]Operation{}
509 }
510 if state.RecoveryEntries == nil {
511 state.RecoveryEntries = map[string]RecoveryEntry{}
512 }
513 if state.Presentation == nil {
514 state.Presentation = map[string]Presentation{}
515 }
516 if state.WorkspaceIDs == nil {
517 state.WorkspaceIDs = []string{}
518 }
519 if state.Workspaces == nil {
520 state.Workspaces = map[string]Workspace{}
521 }
522 if state.ArchivedSessionIDs == nil {
523 state.ArchivedSessionIDs = []string{}
524 }
525 if state.PendingCreates == nil {
526 state.PendingCreates = map[string]PendingCreate{}
527 }
528 for id, workspace := range state.Workspaces {
529 if workspace.SessionIDs == nil {
530 workspace.SessionIDs = []string{}
531 }
532 state.Workspaces[id] = workspace
533 for _, sessionID := range workspace.SessionIDs {
534 if _, ok := state.SessionStates[sessionID]; !ok {
535 state.SessionStates[sessionID] = SessionState{Lifecycle: Active, Generation: state.Generation}
536 }
537 }
538 }
539 state.ArchivedSessionIDs = []string{}
540 for id, status := range state.SessionStates {
541 if status.Lifecycle == Archived {
542 state.ArchivedSessionIDs = append(state.ArchivedSessionIDs, id)
543 }
544 }
545 slices.Sort(state.ArchivedSessionIDs)
546 }
547
548 func validate(state State) error {
549 if err := validateLifecycleState(state); err != nil {
550 return err
551 }
552 seen := map[string]struct{}{}
553 for _, id := range state.WorkspaceIDs {
554 if _, duplicate := seen[id]; duplicate {
555 return fmt.Errorf("workspace state has duplicate workspace %q", id)
556 }
557 seen[id] = struct{}{}
558 workspace, ok := state.Workspaces[id]
559 if !ok || workspace.ID != id {
560 return fmt.Errorf("workspace state has invalid workspace %q", id)
561 }
562 }
563 owners := map[string]string{}
564 for id, workspace := range state.Workspaces {
565 for _, sessionID := range workspace.SessionIDs {
566 if owner, duplicate := owners[sessionID]; duplicate {
567 return fmt.Errorf("session %q belongs to both %q and %q", sessionID, owner, id)
568 }
569 owners[sessionID] = id
570 }
571 }
572 return nil
573 }
574
575 func sessionOwner(state State, sessionID string) (string, bool) {
576 for id, workspace := range state.Workspaces {
577 if contains(workspace.SessionIDs, sessionID) {
578 return id, true
579 }
580 }
581 return "", false
582 }
583
584 func insertBefore(ids []string, id, before string) []string {
585 ids = remove(ids, id)
586 if before != "" {
587 for i, current := range ids {
588 if current == before {
589 return append(append(append([]string{}, ids[:i]...), id), ids[i:]...)
590 }
591 }
592 }
593 return append(ids, id)
594 }
595
596 func remove(ids []string, target string) []string {
597 result := make([]string, 0, len(ids))
598 for _, id := range ids {
599 if id != target {
600 result = append(result, id)
601 }
602 }
603 return result
604 }
605
606 func contains(ids []string, target string) bool {
607 return slices.Contains(ids, target)
608 }
609
610 func cloneState(state State) (State, error) {
611 body, err := json.Marshal(state)
612 if err != nil {
613 return State{}, err
614 }
615 var clone State
616 if err := json.Unmarshal(body, &clone); err != nil {
617 return State{}, err
618 }
619 normalize(&clone)
620 return clone, nil
621 }
622
623 func (s *State) UnmarshalJSON(body []byte) error {
624 type plain State
625 var decoded plain
626 if err := json.Unmarshal(body, &decoded); err != nil {
627 return err
628 }
629 var fields map[string]json.RawMessage
630 if err := json.Unmarshal(body, &fields); err != nil {
631 return err
632 }
633 for _, key := range []string{"version", "generation", "initialized", "workspaceIds", "workspaces", "archivedSessionIds", "pendingCreates", "sessionStates", "sourceMappings", "pendingOperations", "recoveryEntries", "presentation"} {
634 delete(fields, key)
635 }
636 *s = State(decoded)
637 s.extra = fields
638 return nil
639 }
640
641 func (s State) MarshalJSON() ([]byte, error) {
642 type plain State
643 body, err := json.Marshal(plain(s))
644 if err != nil {
645 return nil, err
646 }
647 return mergeUnknown(body, s.extra)
648 }
649
650 func (w *Workspace) UnmarshalJSON(body []byte) error {
651 type plain Workspace
652 var decoded plain
653 if err := json.Unmarshal(body, &decoded); err != nil {
654 return err
655 }
656 var fields map[string]json.RawMessage
657 if err := json.Unmarshal(body, &fields); err != nil {
658 return err
659 }
660 for _, key := range []string{"id", "root", "title", "sessionIds", "visible", "createdAt", "updatedAt"} {
661 delete(fields, key)
662 }
663 *w = Workspace(decoded)
664 w.extra = fields
665 return nil
666 }
667
668 func (w Workspace) MarshalJSON() ([]byte, error) {
669 type plain Workspace
670 body, err := json.Marshal(plain(w))
671 if err != nil {
672 return nil, err
673 }
674 return mergeUnknown(body, w.extra)
675 }
676
677 func mergeUnknown(known []byte, extra map[string]json.RawMessage) ([]byte, error) {
678 if len(extra) == 0 {
679 return known, nil
680 }
681 var fields map[string]json.RawMessage
682 if err := json.Unmarshal(known, &fields); err != nil {
683 return nil, err
684 }
685 for key, value := range extra {
686 if _, exists := fields[key]; !exists {
687 fields[key] = bytes.Clone(value)
688 }
689 }
690 return json.Marshal(fields)
691 }
692
692 lines GO