netizen-cli 0.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. netizen_cli/__init__.py +3 -0
  2. netizen_cli/__main__.py +4 -0
  3. netizen_cli/admin/__init__.py +1 -0
  4. netizen_cli/admin/auth.py +928 -0
  5. netizen_cli/admin/errors.py +9 -0
  6. netizen_cli/admin/port_config.py +115 -0
  7. netizen_cli/admin/presentation.py +257 -0
  8. netizen_cli/admin/queries.py +337 -0
  9. netizen_cli/admin/static/admin.css +260 -0
  10. netizen_cli/admin/static/admin.js +2898 -0
  11. netizen_cli/admin/static/index.html +327 -0
  12. netizen_cli/admin/transport.py +935 -0
  13. netizen_cli/admin/web.py +2717 -0
  14. netizen_cli/bindings.py +3215 -0
  15. netizen_cli/builtin_skills.py +93 -0
  16. netizen_cli/cards/__init__.py +105 -0
  17. netizen_cli/cards/callbacks.py +565 -0
  18. netizen_cli/cards/controls.py +2273 -0
  19. netizen_cli/cards/defaults.py +213 -0
  20. netizen_cli/cards/model_info.py +80 -0
  21. netizen_cli/cards/questions.py +220 -0
  22. netizen_cli/cards/reply.py +2247 -0
  23. netizen_cli/cards/scheduled.py +836 -0
  24. netizen_cli/channel/__init__.py +1 -0
  25. netizen_cli/channel/completion_mentions.py +60 -0
  26. netizen_cli/channel/input_preparation.py +644 -0
  27. netizen_cli/channel/messages.py +57 -0
  28. netizen_cli/channel/ports.py +52 -0
  29. netizen_cli/channel/question_inputs.py +51 -0
  30. netizen_cli/channel/reactions.py +293 -0
  31. netizen_cli/channel/reply_presenter.py +1505 -0
  32. netizen_cli/channel/topics.py +70 -0
  33. netizen_cli/channel_app.py +6593 -0
  34. netizen_cli/cli.py +287 -0
  35. netizen_cli/cli_data.py +536 -0
  36. netizen_cli/cli_packages.py +526 -0
  37. netizen_cli/cli_services.py +651 -0
  38. netizen_cli/cli_setup.py +242 -0
  39. netizen_cli/cli_update.py +303 -0
  40. netizen_cli/cli_update_restore.py +53 -0
  41. netizen_cli/cli_update_worker.py +333 -0
  42. netizen_cli/codex_runtime.py +7125 -0
  43. netizen_cli/completion_mention.py +16 -0
  44. netizen_cli/database_migrations.py +218 -0
  45. netizen_cli/defaults/__init__.py +5 -0
  46. netizen_cli/defaults/models.py +39 -0
  47. netizen_cli/defaults/service.py +232 -0
  48. netizen_cli/defaults/store.py +260 -0
  49. netizen_cli/deployment/__init__.py +1 -0
  50. netizen_cli/deployment/restart_worker.py +134 -0
  51. netizen_cli/deployment/update_executor.py +258 -0
  52. netizen_cli/deployment/update_protocol.py +281 -0
  53. netizen_cli/domain.py +416 -0
  54. netizen_cli/error_messages.py +124 -0
  55. netizen_cli/experience.py +531 -0
  56. netizen_cli/feishu_app_onboarding.py +187 -0
  57. netizen_cli/feishu_app_permissions.py +123 -0
  58. netizen_cli/git_status.py +63 -0
  59. netizen_cli/image_inputs.py +579 -0
  60. netizen_cli/instance.py +84 -0
  61. netizen_cli/lark_app.py +125 -0
  62. netizen_cli/main.py +903 -0
  63. netizen_cli/management/__init__.py +83 -0
  64. netizen_cli/management/blocking_io.py +352 -0
  65. netizen_cli/management/chat_labels.py +266 -0
  66. netizen_cli/management/coordination.py +32 -0
  67. netizen_cli/management/service.py +2187 -0
  68. netizen_cli/management/updates.py +214 -0
  69. netizen_cli/markdown_images.py +78 -0
  70. netizen_cli/message_content.py +786 -0
  71. netizen_cli/message_history.py +643 -0
  72. netizen_cli/message_preparation.py +60 -0
  73. netizen_cli/message_projection.py +923 -0
  74. netizen_cli/migrations/__init__.py +1 -0
  75. netizen_cli/migrations/schema.py +103 -0
  76. netizen_cli/migrations/v14.py +438 -0
  77. netizen_cli/model_settings.py +269 -0
  78. netizen_cli/package_resources.py +22 -0
  79. netizen_cli/projects.py +327 -0
  80. netizen_cli/prompt_projection.py +327 -0
  81. netizen_cli/quoted_context.py +312 -0
  82. netizen_cli/resources/config.example.yaml +35 -0
  83. netizen_cli/resources/skills/netizen-lark/SKILL.md +64 -0
  84. netizen_cli/resources/skills/netizen-user-guide/SKILL.md +37 -0
  85. netizen_cli/resources/skills/netizen-user-guide/references/user-guide.md +842 -0
  86. netizen_cli/result_images.py +123 -0
  87. netizen_cli/runtime/__init__.py +1 -0
  88. netizen_cli/runtime/contracts.py +792 -0
  89. netizen_cli/runtime/name_writes.py +67 -0
  90. netizen_cli/runtime/thread_naming.py +451 -0
  91. netizen_cli/schedules/__init__.py +1 -0
  92. netizen_cli/schedules/mcp.py +535 -0
  93. netizen_cli/schedules/models.py +394 -0
  94. netizen_cli/schedules/scheduler.py +374 -0
  95. netizen_cli/schedules/service.py +766 -0
  96. netizen_cli/schedules/store.py +771 -0
  97. netizen_cli/sdk_gap_adapter.py +1151 -0
  98. netizen_cli/service_launcher.py +583 -0
  99. netizen_cli/session_settings.py +126 -0
  100. netizen_cli/settings.py +216 -0
  101. netizen_cli/skill_references.py +40 -0
  102. netizen_cli/terminal_cleanup.py +155 -0
  103. netizen_cli/turn_activity.py +688 -0
  104. netizen_cli/turn_files.py +812 -0
  105. netizen_cli/turn_patch_children.py +254 -0
  106. netizen_cli/turn_plan_observer.py +315 -0
  107. netizen_cli/user_questions.py +106 -0
  108. netizen_cli-0.10.0.dist-info/METADATA +18 -0
  109. netizen_cli-0.10.0.dist-info/RECORD +112 -0
  110. netizen_cli-0.10.0.dist-info/WHEEL +5 -0
  111. netizen_cli-0.10.0.dist-info/entry_points.txt +2 -0
  112. netizen_cli-0.10.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,792 @@
1
+ """Runtime protocols, errors and immutable operation inputs and snapshots.
2
+
3
+ These types describe the shared boundary. They do not own execution tasks,
4
+ locks, subscriptions or persistence.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import AsyncIterator, Awaitable, Callable
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+ from pathlib import Path
13
+ from typing import Any, Protocol
14
+
15
+ from ..bindings import BindingTaskFeedback, SideTopicState
16
+ from ..domain import ActiveState, GoalOperationState, MessageContextAnchor
17
+ from ..sdk_gap_adapter import GoalSnapshot
18
+ from ..turn_activity import TurnActivityEntrySnapshot, TurnPlanStepSnapshot
19
+ from ..turn_patch_children import TaskPatchChildren
20
+
21
+
22
+ class NativeTurnHandle(Protocol):
23
+ id: str
24
+ thread_id: str
25
+
26
+ async def steer(self, input: Any) -> object: ...
27
+
28
+ async def interrupt(self) -> object: ...
29
+
30
+ async def run(self) -> object: ...
31
+
32
+ def stream(self) -> AsyncIterator[object]: ...
33
+
34
+
35
+ class NativeThread(Protocol):
36
+ id: str
37
+
38
+ async def turn(self, input: Any, **kwargs: object) -> NativeTurnHandle: ...
39
+
40
+ async def read(self, *, include_turns: bool = False) -> object: ...
41
+
42
+ async def compact(self) -> object: ...
43
+
44
+ async def set_name(self, name: str) -> object: ...
45
+
46
+
47
+ class NativeCodex(Protocol):
48
+ async def thread_start(self, **kwargs: object) -> NativeThread: ...
49
+
50
+ async def thread_resume(self, thread_id: str, **kwargs: object) -> NativeThread: ...
51
+
52
+ async def thread_list(self, **kwargs: object) -> object: ...
53
+
54
+ async def thread_archive(self, thread_id: str) -> object: ...
55
+
56
+ async def thread_unarchive(self, thread_id: str) -> NativeThread: ...
57
+
58
+ async def thread_fork(
59
+ self,
60
+ thread_id: str,
61
+ **kwargs: object,
62
+ ) -> NativeThread: ...
63
+
64
+ async def models(self, *, include_hidden: bool = False) -> object: ...
65
+
66
+
67
+ class RuntimeClosed(RuntimeError):
68
+ pass
69
+
70
+
71
+ class ThreadStopping(RuntimeError):
72
+ pass
73
+
74
+
75
+ class ThreadOccupied(RuntimeError):
76
+ """The exact Thread resume was rejected because another writer owns it."""
77
+
78
+
79
+ class ThreadRunningConfiguration(RuntimeError):
80
+ pass
81
+
82
+
83
+ class ThreadCompacting(RuntimeError):
84
+ pass
85
+
86
+
87
+ class ThreadNotMaterialized(RuntimeError):
88
+ pass
89
+
90
+
91
+ class SteerRace(RuntimeError):
92
+ pass
93
+
94
+
95
+ class TerminalCleanupFailed(RuntimeError):
96
+ pass
97
+
98
+
99
+ class TurnInterruptFailed(RuntimeError):
100
+ pass
101
+
102
+
103
+ class TurnStartFailed(RuntimeError):
104
+ pass
105
+
106
+
107
+ class ContextBoundaryCommitFailed(RuntimeError):
108
+ """The native submission succeeded but its catch-up cursor did not."""
109
+
110
+ pass
111
+
112
+
113
+ class ThreadCompactStartFailed(RuntimeError):
114
+ pass
115
+
116
+
117
+ class CompactionFailed(RuntimeError):
118
+ pass
119
+
120
+
121
+ class CompactionStateUnknown(RuntimeError):
122
+ pass
123
+
124
+
125
+ class TerminalStateUnknown(RuntimeError):
126
+ pass
127
+
128
+
129
+ class SkillReferenceError(RuntimeError):
130
+ pass
131
+
132
+
133
+ class ThreadGoalActive(RuntimeError):
134
+ pass
135
+
136
+
137
+ class ExternalGoalActive(ThreadGoalActive):
138
+ pass
139
+
140
+
141
+ class GoalNotFound(RuntimeError):
142
+ pass
143
+
144
+
145
+ class GoalNotMaterialized(RuntimeError):
146
+ pass
147
+
148
+
149
+ class GoalStateUnknown(RuntimeError):
150
+ pass
151
+
152
+
153
+ class ThreadLifecycleError(RuntimeError):
154
+ pass
155
+
156
+
157
+ class TurnObservationUnavailable(ThreadLifecycleError):
158
+ pass
159
+
160
+
161
+ class ScheduledInitialStartConflict(ThreadLifecycleError):
162
+ """An initial scheduled submission can no longer start its exact Binding."""
163
+
164
+
165
+ class ScheduledTurnReadError(ThreadLifecycleError):
166
+ """One bounded read could not establish the scheduled exact Turn state."""
167
+
168
+ def __init__(self, code: str, message: str) -> None:
169
+ super().__init__(message)
170
+ self.code = code
171
+
172
+
173
+ class ContextAnchorRequired(ThreadLifecycleError):
174
+ pass
175
+
176
+
177
+ class ThreadLifecycleStateUnknown(ThreadLifecycleError):
178
+ pass
179
+
180
+
181
+ class ThreadArchived(ThreadLifecycleError):
182
+ pass
183
+
184
+
185
+ class ThreadNotArchived(ThreadLifecycleError):
186
+ pass
187
+
188
+
189
+ class ThreadDeleteUnavailable(ThreadLifecycleError):
190
+ pass
191
+
192
+
193
+ class ThreadDeleteTargetChanged(ThreadLifecycleError):
194
+ pass
195
+
196
+
197
+ class ThreadActivityChanged(ThreadLifecycleError):
198
+ pass
199
+
200
+
201
+ class ThreadReleaseError(ThreadLifecycleError):
202
+ pass
203
+
204
+
205
+ class ThreadReleaseStateUnknown(ThreadReleaseError):
206
+ pass
207
+
208
+
209
+ class ThreadBackgroundTerminalsActive(ThreadReleaseError):
210
+ pass
211
+
212
+
213
+ class ThreadCatalogError(RuntimeError):
214
+ pass
215
+
216
+
217
+ class ThreadCatalogDeadlineExceeded(ThreadCatalogError):
218
+ pass
219
+
220
+
221
+ class ThreadCatalogLimitExceeded(ThreadCatalogError):
222
+ pass
223
+
224
+
225
+ class ThreadCatalogIdentityMissing(ThreadCatalogError):
226
+ pass
227
+
228
+
229
+ class SideUnavailable(RuntimeError):
230
+ pass
231
+
232
+
233
+ class SideSessionNotFound(LookupError):
234
+ pass
235
+
236
+
237
+ class SideSessionConflict(RuntimeError):
238
+ pass
239
+
240
+
241
+ class SideSessionClosing(RuntimeError):
242
+ pass
243
+
244
+
245
+ class SideStartFailed(RuntimeError):
246
+ pass
247
+
248
+
249
+ class SideCloseFailed(RuntimeError):
250
+ pass
251
+
252
+
253
+ class SubmitDisposition(str, Enum):
254
+ STARTED = "started"
255
+ STEERED = "steered"
256
+
257
+
258
+ class GoalFinalizationStatus(str, Enum):
259
+ NOT_APPLICABLE = "not-applicable"
260
+ CLEARED = "cleared"
261
+ UNKNOWN = "unknown"
262
+
263
+
264
+ class ThreadLifecycleState(str, Enum):
265
+ RENAMING = "renaming"
266
+ ARCHIVING = "archiving"
267
+ UNARCHIVING = "unarchiving"
268
+ DELETING = "deleting"
269
+ UNKNOWN = "lifecycle-unknown"
270
+
271
+
272
+ class StopDisposition(str, Enum):
273
+ NOT_RUNNING = "not-running"
274
+ REQUESTED = "requested"
275
+ STOPPING = "stopping"
276
+ COMPACTING = "compacting"
277
+ GOAL_REQUESTED = "goal-requested"
278
+ GOAL_STOPPING = "goal-stopping"
279
+ EXTERNAL_GOAL = "externally-active-goal"
280
+
281
+
282
+ class SideSessionState(str, Enum):
283
+ OPEN = "open"
284
+ CLOSING = "closing"
285
+
286
+
287
+ class ThreadSubscriptionState(str, Enum):
288
+ SUBSCRIBED = "subscribed"
289
+ RELEASE_PENDING = "release-pending"
290
+ RELEASING = "releasing"
291
+ RELEASED = "released"
292
+ RELEASE_UNKNOWN = "release-unknown"
293
+
294
+
295
+ class ReleaseDisposition(str, Enum):
296
+ NOT_MATERIALIZED = "not-materialized"
297
+ NOT_SUBSCRIBED = "not-subscribed"
298
+ RELEASED = "released"
299
+
300
+
301
+ @dataclass(frozen=True, slots=True)
302
+ class Submission:
303
+ disposition: SubmitDisposition
304
+ binding_id: str
305
+ thread_id: str
306
+ turn_id: str
307
+ release_receipt_attempt: Callable[[], None] | None = None
308
+ task_feedback: BindingTaskFeedback = BindingTaskFeedback()
309
+ feedback_revision: int = 1
310
+
311
+ def __post_init__(self) -> None:
312
+ if self.feedback_revision < 1:
313
+ raise ValueError("feedback revision must be positive")
314
+
315
+
316
+ @dataclass(frozen=True, slots=True)
317
+ class CompactSubmission:
318
+ binding_id: str
319
+ thread_id: str
320
+ release_receipt_attempt: Callable[[], None]
321
+
322
+
323
+ @dataclass(frozen=True, slots=True)
324
+ class GoalSubmission:
325
+ binding_id: str
326
+ thread_id: str
327
+ logical_turn_id: str
328
+ release_receipt_attempt: Callable[[], None]
329
+ task_feedback: BindingTaskFeedback = BindingTaskFeedback()
330
+ feedback_revision: int = 1
331
+
332
+ def __post_init__(self) -> None:
333
+ if self.feedback_revision < 1:
334
+ raise ValueError("feedback revision must be positive")
335
+
336
+
337
+ @dataclass(frozen=True, slots=True)
338
+ class SideSubmission:
339
+ disposition: SubmitDisposition
340
+ side_id: str
341
+ thread_id: str
342
+ turn_id: str
343
+ release_receipt_attempt: Callable[[], None] | None = None
344
+ task_feedback: BindingTaskFeedback = BindingTaskFeedback()
345
+ feedback_revision: int = 1
346
+
347
+ def __post_init__(self) -> None:
348
+ if self.feedback_revision < 1:
349
+ raise ValueError("feedback revision must be positive")
350
+
351
+
352
+ @dataclass(frozen=True, slots=True)
353
+ class SideSubmissionAdmission:
354
+ side_id: str
355
+ revision: int
356
+ thread_id: str
357
+ turn_id: str | None
358
+
359
+ def __post_init__(self) -> None:
360
+ if self.revision < 0:
361
+ raise ValueError("Side admission revision must be non-negative")
362
+ if not self.side_id or not self.thread_id:
363
+ raise ValueError("Side admission identity must not be empty")
364
+
365
+
366
+ @dataclass(frozen=True, slots=True)
367
+ class ActiveTurnSnapshot:
368
+ binding_id: str
369
+ thread_id: str
370
+ turn_id: str
371
+ owner_id: str
372
+ state: ActiveState
373
+
374
+
375
+ @dataclass(frozen=True, slots=True)
376
+ class TurnProgressSnapshot:
377
+ binding_id: str
378
+ thread_id: str
379
+ turn_id: str
380
+ steer_count: int
381
+ plan_available: bool
382
+ plan_generated: bool
383
+ plan_may_be_stale: bool
384
+ steps: tuple[TurnPlanStepSnapshot, ...]
385
+
386
+
387
+ @dataclass(frozen=True, slots=True)
388
+ class TurnActivitySnapshot:
389
+ """Latest bounded display projection for one exact active Turn.
390
+
391
+ The projection keeps only bounded, allowlisted activity and the latest full
392
+ plan replacement. It is process-local display data, not a Turn history or
393
+ a second terminal-state authority.
394
+ """
395
+
396
+ binding_id: str
397
+ thread_id: str
398
+ turn_id: str
399
+ revision: int
400
+ state: ActiveState
401
+ steer_count: int
402
+ plan_available: bool
403
+ plan_generated: bool
404
+ plan_may_be_stale: bool
405
+ steps: tuple[TurnPlanStepSnapshot, ...]
406
+ commentary: tuple[TurnActivityEntrySnapshot, ...] = ()
407
+ operations: tuple[TurnActivityEntrySnapshot, ...] = ()
408
+
409
+ def __post_init__(self) -> None:
410
+ if not self.binding_id or not self.thread_id or not self.turn_id:
411
+ raise ValueError("Turn activity identity must not be empty")
412
+ if self.revision < 1:
413
+ raise ValueError("Turn activity revision must be positive")
414
+ if self.steer_count < 0:
415
+ raise ValueError("Turn activity steer count must be non-negative")
416
+
417
+
418
+ @dataclass(frozen=True, slots=True)
419
+ class SideTurnActivitySnapshot:
420
+ """Latest bounded display projection for one exact active Side Turn."""
421
+
422
+ side_id: str
423
+ thread_id: str
424
+ turn_id: str
425
+ revision: int
426
+ state: ActiveState
427
+ steer_count: int
428
+ plan_available: bool
429
+ plan_generated: bool
430
+ plan_may_be_stale: bool
431
+ steps: tuple[TurnPlanStepSnapshot, ...]
432
+ commentary: tuple[TurnActivityEntrySnapshot, ...] = ()
433
+ operations: tuple[TurnActivityEntrySnapshot, ...] = ()
434
+
435
+ def __post_init__(self) -> None:
436
+ if not self.side_id or not self.thread_id or not self.turn_id:
437
+ raise ValueError("Side Turn activity identity must not be empty")
438
+ if self.revision < 1:
439
+ raise ValueError("Side Turn activity revision must be positive")
440
+ if self.steer_count < 0:
441
+ raise ValueError(
442
+ "Side Turn activity steer count must be non-negative"
443
+ )
444
+
445
+
446
+ @dataclass(frozen=True, slots=True)
447
+ class GoalActivitySnapshot:
448
+ """Latest bounded display projection for one exact native Goal run."""
449
+
450
+ binding_id: str
451
+ thread_id: str
452
+ logical_turn_id: str
453
+ physical_turn_id: str | None
454
+ revision: int
455
+ state: GoalOperationState
456
+ plan_available: bool
457
+ plan_generated: bool
458
+ steps: tuple[TurnPlanStepSnapshot, ...]
459
+ commentary: tuple[TurnActivityEntrySnapshot, ...] = ()
460
+ operations: tuple[TurnActivityEntrySnapshot, ...] = ()
461
+
462
+ def __post_init__(self) -> None:
463
+ if not self.binding_id or not self.thread_id or not self.logical_turn_id:
464
+ raise ValueError("Goal activity identity must not be empty")
465
+ if self.revision < 1:
466
+ raise ValueError("Goal activity revision must be positive")
467
+
468
+
469
+ @dataclass(frozen=True, slots=True)
470
+ class NativeThreadMetadata:
471
+ thread_id: str
472
+ name: str | None
473
+ preview: str
474
+ updated_at: int | None = None # Native Unix timestamp in seconds; never persisted here.
475
+
476
+
477
+ class NativeThreadCatalogState(str, Enum):
478
+ ACTIVE = "active"
479
+ ARCHIVED = "archived"
480
+ MISSING = "missing"
481
+
482
+
483
+ @dataclass(frozen=True, slots=True)
484
+ class NativeThreadCatalog:
485
+ archived: bool
486
+ threads: tuple[NativeThreadMetadata, ...]
487
+
488
+ def by_id(self) -> dict[str, NativeThreadMetadata]:
489
+ return {thread.thread_id: thread for thread in self.threads}
490
+
491
+
492
+ @dataclass(frozen=True, slots=True)
493
+ class ContextWindowUsage:
494
+ used_tokens: int
495
+ context_window_tokens: int | None
496
+
497
+ def __post_init__(self) -> None:
498
+ if (
499
+ isinstance(self.used_tokens, bool)
500
+ or not isinstance(self.used_tokens, int)
501
+ or self.used_tokens < 0
502
+ ):
503
+ raise ValueError("used tokens must be a non-negative integer")
504
+ if self.context_window_tokens is not None and (
505
+ isinstance(self.context_window_tokens, bool)
506
+ or not isinstance(self.context_window_tokens, int)
507
+ or self.context_window_tokens <= 0
508
+ ):
509
+ raise ValueError("context window tokens must be a positive integer")
510
+
511
+
512
+ @dataclass(frozen=True, slots=True)
513
+ class ActiveGoalSnapshot:
514
+ binding_id: str
515
+ thread_id: str
516
+ logical_turn_id: str | None
517
+ owner_id: str
518
+ state: GoalOperationState
519
+ persisted: GoalSnapshot | None = None
520
+
521
+
522
+ @dataclass(frozen=True, slots=True)
523
+ class ThreadLifecycleSnapshot:
524
+ binding_id: str
525
+ thread_id: str | None
526
+ state: ThreadLifecycleState
527
+
528
+
529
+ @dataclass(frozen=True, slots=True)
530
+ class ThreadSubscriptionSnapshot:
531
+ binding_id: str
532
+ thread_id: str
533
+ state: ThreadSubscriptionState
534
+ release_in_seconds: float | None
535
+
536
+
537
+ @dataclass(frozen=True, slots=True)
538
+ class BindingRuntimeSnapshot:
539
+ """One immutable, process-local view of an ordinary Binding's activity."""
540
+
541
+ binding_id: str
542
+ activity_revision: int
543
+ turn: ActiveTurnSnapshot | None
544
+ goal: ActiveGoalSnapshot | None
545
+ compacting: bool
546
+ lifecycle: ThreadLifecycleSnapshot | None
547
+ subscription: ThreadSubscriptionSnapshot | None
548
+ context_window_usage: ContextWindowUsage | None
549
+
550
+ def __post_init__(self) -> None:
551
+ if not self.binding_id:
552
+ raise ValueError("Binding runtime snapshot identity must not be empty")
553
+ if self.activity_revision < 0:
554
+ raise ValueError("Binding runtime snapshot revision must be non-negative")
555
+
556
+
557
+ @dataclass(frozen=True, slots=True)
558
+ class SideSessionSnapshot:
559
+ side_id: str
560
+ parent_binding_id: str
561
+ parent_thread_id: str
562
+ thread_id: str
563
+ project_alias: str
564
+ cwd: Path
565
+ creator_id: str
566
+ state: SideSessionState
567
+ topic_id: str | None
568
+ root_message_id: str | None
569
+ turn_id: str | None
570
+ turn_state: ActiveState | None
571
+ last_activity: float
572
+
573
+
574
+ @dataclass(frozen=True, slots=True)
575
+ class SubmissionAdmission:
576
+ """Condition captured before asynchronous prompt preparation.
577
+
578
+ The monotonic revision prevents an idle -> running -> idle ABA from
579
+ turning a delayed prompt into a different native submission.
580
+ """
581
+
582
+ binding_id: str
583
+ revision: int
584
+ thread_id: str | None
585
+ turn_id: str | None
586
+ settings_revision: int
587
+ context_revision: int = 1
588
+ feedback_revision: int = 1
589
+
590
+ def __post_init__(self) -> None:
591
+ if self.revision < 0:
592
+ raise ValueError("submission admission revision must be non-negative")
593
+ if self.settings_revision < 1:
594
+ raise ValueError("settings revision must be positive")
595
+ if self.context_revision < 1:
596
+ raise ValueError("context revision must be positive")
597
+ if self.feedback_revision < 1:
598
+ raise ValueError("feedback revision must be positive")
599
+ if (self.thread_id is None) != (self.turn_id is None):
600
+ raise ValueError(
601
+ "submission admission thread_id and turn_id must both be set or unset"
602
+ )
603
+
604
+
605
+ @dataclass(frozen=True, slots=True)
606
+ class ContextCursorCommit:
607
+ """Catch-up boundary to commit after one exact native submission."""
608
+
609
+ expected_context_revision: int
610
+ anchor: MessageContextAnchor
611
+
612
+ def __post_init__(self) -> None:
613
+ if self.expected_context_revision < 1:
614
+ raise ValueError("expected context revision must be positive")
615
+
616
+
617
+ @dataclass(frozen=True, slots=True)
618
+ class TurnOutcome:
619
+ binding_id: str
620
+ thread_id: str
621
+ turn_id: str
622
+ owner_id: str
623
+ origin: object
624
+ result: object | None = None
625
+ error: BaseException | None = None
626
+ # True only means the exact backgroundTerminals/clean RPC returned
627
+ # successfully. It does not attest that a foreground tool process exited.
628
+ background_cleanup_requested: bool = False
629
+ # Latest aggregate snapshot observed from the public
630
+ # ``turn/diff/updated`` notification for this exact Turn. It is carried
631
+ # only through completion delivery and is never persisted by Netizen.
632
+ turn_diff: str | None = None
633
+ task_feedback: BindingTaskFeedback = BindingTaskFeedback()
634
+ feedback_revision: int = 1
635
+ activity: TurnActivitySnapshot | None = None
636
+ patch_children: TaskPatchChildren = TaskPatchChildren()
637
+
638
+ def __post_init__(self) -> None:
639
+ if self.feedback_revision < 1:
640
+ raise ValueError("feedback revision must be positive")
641
+ if self.activity is not None and (
642
+ self.activity.binding_id != self.binding_id
643
+ or self.activity.thread_id != self.thread_id
644
+ or self.activity.turn_id != self.turn_id
645
+ ):
646
+ raise ValueError("Turn outcome activity belongs to another Turn")
647
+
648
+ @property
649
+ def final_response(self) -> str | None:
650
+ value = getattr(self.result, "final_response", None)
651
+ return value if isinstance(value, str) else None
652
+
653
+ @property
654
+ def status(self) -> str | None:
655
+ status = getattr(self.result, "status", None)
656
+ value = getattr(status, "value", status)
657
+ return value if isinstance(value, str) else None
658
+
659
+
660
+ @dataclass(frozen=True, slots=True)
661
+ class TurnObservationUnavailableOutcome:
662
+ """One non-terminal notice that bounded exact Turn observation failed."""
663
+
664
+ binding_id: str
665
+ thread_id: str
666
+ turn_id: str
667
+ owner_id: str
668
+ origin: object
669
+ error: TerminalStateUnknown
670
+ state: ActiveState = ActiveState.OBSERVATION_UNAVAILABLE
671
+
672
+
673
+ @dataclass(frozen=True, slots=True)
674
+ class ThreadActivityDiscardedOutcome:
675
+ """Presentation-only notice after native Thread removal is committed."""
676
+
677
+ binding_id: str
678
+ thread_id: str
679
+ turn_id: str | None
680
+
681
+
682
+ @dataclass(frozen=True, slots=True)
683
+ class CompactionOutcome:
684
+ binding_id: str
685
+ thread_id: str
686
+ owner_id: str
687
+ origin: object
688
+ compact_turn_id: str | None = None
689
+ status: str | None = None
690
+ error: BaseException | None = None
691
+
692
+
693
+ @dataclass(frozen=True, slots=True)
694
+ class GoalOutcome:
695
+ binding_id: str
696
+ thread_id: str
697
+ logical_turn_id: str | None
698
+ owner_id: str
699
+ origin: object
700
+ goal: GoalSnapshot | None = None
701
+ final_physical_turn_id: str | None = None
702
+ final_turn_status: str | None = None
703
+ final_items: tuple[object, ...] = ()
704
+ final_response: str | None = None
705
+ # Latest aggregate diff captured for the exact final physical Turn by the
706
+ # existing Goal notification consumer. It is never reconstructed from
707
+ # persisted Turn history or stored by Netizen.
708
+ turn_diff: str | None = None
709
+ error: BaseException | None = None
710
+ background_cleanup_requested: bool = False
711
+ task_feedback: BindingTaskFeedback = BindingTaskFeedback()
712
+ feedback_revision: int = 1
713
+ activity: GoalActivitySnapshot | None = None
714
+ finalization: GoalFinalizationStatus = GoalFinalizationStatus.NOT_APPLICABLE
715
+ finalization_error: BaseException | None = None
716
+ patch_children: TaskPatchChildren = TaskPatchChildren()
717
+
718
+ def __post_init__(self) -> None:
719
+ if self.feedback_revision < 1:
720
+ raise ValueError("feedback revision must be positive")
721
+ if self.activity is not None and (
722
+ self.activity.binding_id != self.binding_id
723
+ or self.activity.thread_id != self.thread_id
724
+ or self.activity.logical_turn_id != self.logical_turn_id
725
+ ):
726
+ raise ValueError("Goal outcome activity belongs to another Goal run")
727
+
728
+
729
+ @dataclass(frozen=True, slots=True)
730
+ class SideTurnOutcome:
731
+ side_id: str
732
+ parent_binding_id: str
733
+ thread_id: str
734
+ turn_id: str
735
+ owner_id: str
736
+ origin: object
737
+ cwd: Path
738
+ result: object | None = None
739
+ error: BaseException | None = None
740
+ background_cleanup_requested: bool = False
741
+ # Captures stop/close intent even when the Turn wins the interrupt race.
742
+ # Unlike background_cleanup_requested, this does not attest cleanup success.
743
+ stop_requested: bool = False
744
+ task_feedback: BindingTaskFeedback = BindingTaskFeedback()
745
+ feedback_revision: int = 1
746
+ activity: SideTurnActivitySnapshot | None = None
747
+ patch_children: TaskPatchChildren = TaskPatchChildren()
748
+
749
+ def __post_init__(self) -> None:
750
+ if self.feedback_revision < 1:
751
+ raise ValueError("feedback revision must be positive")
752
+ if self.activity is not None and (
753
+ self.activity.side_id != self.side_id
754
+ or self.activity.thread_id != self.thread_id
755
+ or self.activity.turn_id != self.turn_id
756
+ ):
757
+ raise ValueError("Side outcome activity belongs to another Side Turn")
758
+
759
+ @property
760
+ def final_response(self) -> str | None:
761
+ value = getattr(self.result, "final_response", None)
762
+ return value if isinstance(value, str) else None
763
+
764
+ @property
765
+ def status(self) -> str | None:
766
+ status = getattr(self.result, "status", None)
767
+ value = getattr(status, "value", status)
768
+ return value if isinstance(value, str) else None
769
+
770
+
771
+ @dataclass(frozen=True, slots=True)
772
+ class SideLifecycleOutcome:
773
+ side_id: str
774
+ state: SideTopicState
775
+ error: BaseException | None = None
776
+
777
+
778
+ RuntimeOutcome = (
779
+ TurnOutcome
780
+ | TurnObservationUnavailableOutcome
781
+ | ThreadActivityDiscardedOutcome
782
+ | CompactionOutcome
783
+ | GoalOutcome
784
+ | SideTurnOutcome
785
+ | SideLifecycleOutcome
786
+ )
787
+
788
+
789
+ CompletionHandler = Callable[[RuntimeOutcome], Awaitable[None]]
790
+
791
+
792
+ StopAcknowledger = Callable[[], Awaitable[None]]