@swarmclawai/swarmclaw 1.2.4 → 1.2.5

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 (260) hide show
  1. package/README.md +14 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +15 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/query/client.ts +17 -0
  109. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  110. package/src/lib/server/agents/agent-service.ts +429 -0
  111. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  112. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  113. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  114. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  115. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  116. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  117. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  118. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  119. package/src/lib/server/build-llm.ts +7 -15
  120. package/src/lib/server/capability-router.test.ts +70 -1
  121. package/src/lib/server/capability-router.ts +24 -99
  122. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  123. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  124. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  125. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  126. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  127. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  128. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  129. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  130. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  131. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  132. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  133. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  134. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  135. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  136. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  137. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  138. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  139. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  140. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  141. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  142. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  143. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  144. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  145. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  146. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  147. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  148. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  149. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  150. package/src/lib/server/chats/chat-session-service.ts +410 -0
  151. package/src/lib/server/connectors/access.ts +1 -1
  152. package/src/lib/server/connectors/commands.ts +7 -6
  153. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  154. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  155. package/src/lib/server/connectors/connector-service.ts +453 -0
  156. package/src/lib/server/connectors/delivery.ts +17 -12
  157. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  158. package/src/lib/server/connectors/media.ts +1 -1
  159. package/src/lib/server/connectors/response-media.ts +1 -1
  160. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  161. package/src/lib/server/connectors/session.ts +9 -7
  162. package/src/lib/server/connectors/voice-note.ts +2 -1
  163. package/src/lib/server/context-manager.ts +20 -1
  164. package/src/lib/server/cost.ts +2 -3
  165. package/src/lib/server/credentials/credential-repository.ts +43 -4
  166. package/src/lib/server/credentials/credential-service.ts +112 -0
  167. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  168. package/src/lib/server/daemon/controller.ts +577 -0
  169. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  170. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  171. package/src/lib/server/daemon/types.ts +101 -0
  172. package/src/lib/server/embeddings.ts +3 -9
  173. package/src/lib/server/eval/agent-regression.ts +3 -2
  174. package/src/lib/server/eval/runner.ts +2 -2
  175. package/src/lib/server/execution-brief.test.ts +167 -0
  176. package/src/lib/server/execution-brief.ts +295 -0
  177. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  178. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  179. package/src/lib/server/execution-engine/index.ts +35 -0
  180. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  181. package/src/lib/server/execution-engine/types.ts +33 -0
  182. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  183. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  184. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  185. package/src/lib/server/messages/message-repository.ts +330 -0
  186. package/src/lib/server/missions/mission-service/core.ts +8 -6
  187. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  188. package/src/lib/server/openclaw/doctor.ts +1 -1
  189. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  190. package/src/lib/server/openclaw/gateway.ts +5 -14
  191. package/src/lib/server/openclaw/health.ts +3 -11
  192. package/src/lib/server/openclaw/sync.ts +8 -6
  193. package/src/lib/server/persistence/storage-context.ts +3 -0
  194. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  195. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  196. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  197. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  198. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  199. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  200. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  201. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  202. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  203. package/src/lib/server/protocols/protocol-types.ts +10 -7
  204. package/src/lib/server/provider-endpoint.ts +7 -12
  205. package/src/lib/server/provider-model-discovery.ts +2 -11
  206. package/src/lib/server/query-expansion.ts +5 -6
  207. package/src/lib/server/run-context.test.ts +365 -0
  208. package/src/lib/server/run-context.ts +367 -0
  209. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  210. package/src/lib/server/runtime/queue/core.ts +61 -190
  211. package/src/lib/server/runtime/run-ledger.ts +8 -0
  212. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  213. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  214. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  215. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  216. package/src/lib/server/service-result.ts +16 -0
  217. package/src/lib/server/session-note.ts +2 -3
  218. package/src/lib/server/session-reset-policy.ts +4 -3
  219. package/src/lib/server/session-tools/connector.ts +9 -6
  220. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  221. package/src/lib/server/session-tools/crud.ts +162 -10
  222. package/src/lib/server/session-tools/delegate.ts +1 -1
  223. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  224. package/src/lib/server/session-tools/memory.ts +6 -4
  225. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  226. package/src/lib/server/session-tools/session-info.ts +119 -12
  227. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  228. package/src/lib/server/session-tools/skills.ts +15 -15
  229. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  230. package/src/lib/server/session-tools/subagent.ts +125 -7
  231. package/src/lib/server/session-tools/team-context.ts +4 -3
  232. package/src/lib/server/session-tools/wallet.ts +0 -58
  233. package/src/lib/server/sessions/session-lineage.ts +55 -0
  234. package/src/lib/server/sessions/session-repository.ts +2 -2
  235. package/src/lib/server/skills/learned-skills.ts +24 -23
  236. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  237. package/src/lib/server/skills/skill-repository.ts +136 -13
  238. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  239. package/src/lib/server/storage-normalization.test.ts +44 -267
  240. package/src/lib/server/storage-normalization.ts +75 -0
  241. package/src/lib/server/storage.ts +19 -0
  242. package/src/lib/server/structured-extract.ts +3 -14
  243. package/src/lib/server/tasks/task-followups.ts +16 -11
  244. package/src/lib/server/tasks/task-result.test.ts +25 -29
  245. package/src/lib/server/tasks/task-result.ts +5 -9
  246. package/src/lib/server/tasks/task-route-service.ts +449 -0
  247. package/src/lib/server/text-normalization.ts +41 -0
  248. package/src/lib/server/tool-planning.ts +6 -42
  249. package/src/lib/server/upload-path.ts +5 -0
  250. package/src/lib/server/working-state/extraction.ts +614 -0
  251. package/src/lib/server/working-state/normalization.ts +866 -0
  252. package/src/lib/server/working-state/prompt.ts +60 -0
  253. package/src/lib/server/working-state/repository.ts +38 -0
  254. package/src/lib/server/working-state/service.test.ts +253 -0
  255. package/src/lib/server/working-state/service.ts +293 -0
  256. package/src/lib/validation/schemas.ts +1 -0
  257. package/src/lib/ws-client.ts +3 -3
  258. package/src/stores/slices/task-slice.ts +1 -4
  259. package/src/stores/use-chatroom-store.ts +2 -2
  260. package/src/types/index.ts +277 -12
@@ -0,0 +1,866 @@
1
+ import { z } from 'zod'
2
+
3
+ import { genId } from '@/lib/id'
4
+ import { cleanText, cleanMultiline, normalizeList } from '@/lib/server/text-normalization'
5
+ import type {
6
+ EvidenceRef,
7
+ Mission,
8
+ SessionWorkingState,
9
+ WorkingArtifact,
10
+ WorkingArtifactPatch,
11
+ WorkingBlocker,
12
+ WorkingBlockerPatch,
13
+ WorkingDecision,
14
+ WorkingDecisionPatch,
15
+ WorkingFact,
16
+ WorkingFactPatch,
17
+ WorkingHypothesis,
18
+ WorkingHypothesisPatch,
19
+ WorkingPlanStep,
20
+ WorkingPlanStepPatch,
21
+ WorkingQuestion,
22
+ WorkingQuestionPatch,
23
+ WorkingStateItemStatus,
24
+ WorkingStateStatus,
25
+ } from '@/types'
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Constants
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export const MAX_PLAN_STEPS = 12
32
+ export const MAX_CONFIRMED_FACTS = 20
33
+ export const MAX_ARTIFACTS = 20
34
+ export const MAX_DECISIONS = 12
35
+ export const MAX_BLOCKERS = 8
36
+ export const MAX_OPEN_QUESTIONS = 8
37
+ export const MAX_HYPOTHESES = 8
38
+ export const MAX_EVIDENCE_REFS = 40
39
+ export const EXTRACTION_TIMEOUT_MS = 7_500
40
+
41
+ export const ACTIVE_STATUS: WorkingStateItemStatus = 'active'
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Zod Schemas
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const WorkingItemStatusSchema = z.enum(['active', 'resolved', 'superseded'])
48
+ const WorkingStateStatusSchema = z.enum(['idle', 'progress', 'blocked', 'waiting', 'completed'])
49
+
50
+ export const WorkingPlanStepPatchSchema = z.object({
51
+ id: z.string().optional().nullable(),
52
+ text: z.string().optional().nullable(),
53
+ status: WorkingItemStatusSchema.optional().nullable(),
54
+ })
55
+
56
+ export const WorkingFactPatchSchema = z.object({
57
+ id: z.string().optional().nullable(),
58
+ statement: z.string().optional().nullable(),
59
+ source: z.enum(['user', 'tool', 'assistant', 'mission', 'system']).optional().nullable(),
60
+ status: WorkingItemStatusSchema.optional().nullable(),
61
+ evidenceIds: z.array(z.string()).optional().nullable(),
62
+ })
63
+
64
+ export const WorkingArtifactPatchSchema = z.object({
65
+ id: z.string().optional().nullable(),
66
+ label: z.string().optional().nullable(),
67
+ kind: z.enum(['file', 'url', 'approval', 'message', 'other']).optional().nullable(),
68
+ path: z.string().optional().nullable(),
69
+ url: z.string().optional().nullable(),
70
+ sourceTool: z.string().optional().nullable(),
71
+ status: WorkingItemStatusSchema.optional().nullable(),
72
+ evidenceIds: z.array(z.string()).optional().nullable(),
73
+ })
74
+
75
+ export const WorkingDecisionPatchSchema = z.object({
76
+ id: z.string().optional().nullable(),
77
+ summary: z.string().optional().nullable(),
78
+ rationale: z.string().optional().nullable(),
79
+ status: WorkingItemStatusSchema.optional().nullable(),
80
+ evidenceIds: z.array(z.string()).optional().nullable(),
81
+ })
82
+
83
+ export const WorkingBlockerPatchSchema = z.object({
84
+ id: z.string().optional().nullable(),
85
+ summary: z.string().optional().nullable(),
86
+ kind: z.enum(['approval', 'credential', 'human_input', 'external_dependency', 'error', 'other']).optional().nullable(),
87
+ nextAction: z.string().optional().nullable(),
88
+ status: WorkingItemStatusSchema.optional().nullable(),
89
+ evidenceIds: z.array(z.string()).optional().nullable(),
90
+ })
91
+
92
+ export const WorkingQuestionPatchSchema = z.object({
93
+ id: z.string().optional().nullable(),
94
+ question: z.string().optional().nullable(),
95
+ status: WorkingItemStatusSchema.optional().nullable(),
96
+ evidenceIds: z.array(z.string()).optional().nullable(),
97
+ })
98
+
99
+ export const WorkingHypothesisPatchSchema = z.object({
100
+ id: z.string().optional().nullable(),
101
+ statement: z.string().optional().nullable(),
102
+ confidence: z.enum(['low', 'medium', 'high']).optional().nullable(),
103
+ status: WorkingItemStatusSchema.optional().nullable(),
104
+ evidenceIds: z.array(z.string()).optional().nullable(),
105
+ })
106
+
107
+ export const WorkingStatePatchSchema = z.object({
108
+ objective: z.string().optional().nullable(),
109
+ summary: z.string().optional().nullable(),
110
+ constraints: z.array(z.string()).optional().nullable(),
111
+ successCriteria: z.array(z.string()).optional().nullable(),
112
+ status: WorkingStateStatusSchema.optional().nullable(),
113
+ nextAction: z.string().optional().nullable(),
114
+ planSteps: z.array(WorkingPlanStepPatchSchema).optional().nullable(),
115
+ factsUpsert: z.array(WorkingFactPatchSchema).optional().nullable(),
116
+ artifactsUpsert: z.array(WorkingArtifactPatchSchema).optional().nullable(),
117
+ decisionsAppend: z.array(WorkingDecisionPatchSchema).optional().nullable(),
118
+ blockersUpsert: z.array(WorkingBlockerPatchSchema).optional().nullable(),
119
+ questionsUpsert: z.array(WorkingQuestionPatchSchema).optional().nullable(),
120
+ hypothesesUpsert: z.array(WorkingHypothesisPatchSchema).optional().nullable(),
121
+ supersedeIds: z.array(z.string()).optional().nullable(),
122
+ }).passthrough()
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Helper types
126
+ // ---------------------------------------------------------------------------
127
+
128
+ export type TimedWorkingItem = {
129
+ id: string
130
+ status: WorkingStateItemStatus
131
+ createdAt: number
132
+ updatedAt: number
133
+ }
134
+
135
+ export type UpsertConfig<TItem extends TimedWorkingItem, TPatch> = {
136
+ max: number
137
+ getPatchId: (patch: TPatch) => string | null
138
+ getPatchKey: (patch: TPatch) => string
139
+ getItemKey: (item: TItem) => string
140
+ create: (patch: TPatch, nowTs: number) => TItem
141
+ merge: (current: TItem, patch: TPatch, nowTs: number) => TItem
142
+ compact?: (items: TItem[], max: number) => TItem[]
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Exported input interfaces
147
+ // ---------------------------------------------------------------------------
148
+
149
+ import type { MessageToolEvent } from '@/types'
150
+
151
+ export interface WorkingStateDeterministicUpdateInput {
152
+ sessionId: string
153
+ mission?: Mission | null
154
+ message?: string | null
155
+ assistantText?: string | null
156
+ error?: string | null
157
+ toolEvents?: MessageToolEvent[]
158
+ runId?: string | null
159
+ source?: string | null
160
+ }
161
+
162
+ export interface WorkingStateExtractionInput extends WorkingStateDeterministicUpdateInput {
163
+ agentId?: string | null
164
+ currentState?: SessionWorkingState | null
165
+ }
166
+
167
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- intentional type alias preserved from original service.ts
168
+ export interface SynchronizeWorkingStateForTurnInput extends WorkingStateExtractionInput {}
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Utility helpers
172
+ // ---------------------------------------------------------------------------
173
+
174
+ export function now(): number {
175
+ return Date.now()
176
+ }
177
+
178
+ export function itemSortRank(status: WorkingStateItemStatus): number {
179
+ if (status === 'active') return 0
180
+ if (status === 'resolved') return 1
181
+ return 2
182
+ }
183
+
184
+ export function genericCompact<TItem extends TimedWorkingItem>(items: TItem[], max: number): TItem[] {
185
+ return [...items]
186
+ .sort((left, right) => {
187
+ const rankDelta = itemSortRank(left.status) - itemSortRank(right.status)
188
+ if (rankDelta !== 0) return rankDelta
189
+ return (right.updatedAt || 0) - (left.updatedAt || 0)
190
+ })
191
+ .slice(0, max)
192
+ }
193
+
194
+ export function compactPlanSteps(items: WorkingPlanStep[], max: number): WorkingPlanStep[] {
195
+ if (items.length <= max) return items
196
+ const next = [...items]
197
+ while (next.length > max) {
198
+ const removableIndex = next.findIndex((step) => step.status !== 'active')
199
+ if (removableIndex >= 0) {
200
+ next.splice(removableIndex, 1)
201
+ continue
202
+ }
203
+ next.shift()
204
+ }
205
+ return next
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Normalize functions
210
+ // ---------------------------------------------------------------------------
211
+
212
+ export function normalizeItemStatus(value: unknown, fallback: WorkingStateItemStatus = ACTIVE_STATUS): WorkingStateItemStatus {
213
+ return value === 'active' || value === 'resolved' || value === 'superseded'
214
+ ? value
215
+ : fallback
216
+ }
217
+
218
+ export function normalizeStateStatus(value: unknown, fallback: WorkingStateStatus = 'idle'): WorkingStateStatus {
219
+ return value === 'idle' || value === 'progress' || value === 'blocked' || value === 'waiting' || value === 'completed'
220
+ ? value
221
+ : fallback
222
+ }
223
+
224
+ export function normalizeEvidenceIds(input: unknown): string[] | undefined {
225
+ const cleaned = normalizeList(input, 12, 120)
226
+ return cleaned.length > 0 ? cleaned : undefined
227
+ }
228
+
229
+ export function normalizeEvidenceRef(input: unknown): EvidenceRef | null {
230
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null
231
+ const record = input as Record<string, unknown>
232
+ const summary = cleanText(record.summary, 280)
233
+ if (!summary) return null
234
+ const type = record.type === 'tool'
235
+ || record.type === 'message'
236
+ || record.type === 'mission'
237
+ || record.type === 'task'
238
+ || record.type === 'artifact'
239
+ || record.type === 'error'
240
+ || record.type === 'approval'
241
+ ? record.type
242
+ : 'message'
243
+ return {
244
+ id: cleanText(record.id, 120) || genId(12),
245
+ type,
246
+ summary,
247
+ value: cleanText(record.value, 240) || null,
248
+ toolName: cleanText(record.toolName, 120) || null,
249
+ toolCallId: cleanText(record.toolCallId, 120) || null,
250
+ runId: cleanText(record.runId, 120) || null,
251
+ sessionId: cleanText(record.sessionId, 120) || null,
252
+ missionId: cleanText(record.missionId, 120) || null,
253
+ taskId: cleanText(record.taskId, 120) || null,
254
+ createdAt: typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
255
+ ? Math.trunc(record.createdAt)
256
+ : now(),
257
+ }
258
+ }
259
+
260
+ export function normalizePlanStep(input: unknown): WorkingPlanStep | null {
261
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null
262
+ const record = input as Record<string, unknown>
263
+ const text = cleanText(record.text, 240)
264
+ if (!text) return null
265
+ const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
266
+ ? Math.trunc(record.createdAt)
267
+ : now()
268
+ return {
269
+ id: cleanText(record.id, 120) || genId(12),
270
+ text,
271
+ status: normalizeItemStatus(record.status),
272
+ createdAt,
273
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
274
+ ? Math.trunc(record.updatedAt)
275
+ : createdAt,
276
+ }
277
+ }
278
+
279
+ export function normalizeFact(input: unknown): WorkingFact | null {
280
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null
281
+ const record = input as Record<string, unknown>
282
+ const statement = cleanText(record.statement, 280)
283
+ if (!statement) return null
284
+ const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
285
+ ? Math.trunc(record.createdAt)
286
+ : now()
287
+ return {
288
+ id: cleanText(record.id, 120) || genId(12),
289
+ statement,
290
+ source: record.source === 'user'
291
+ || record.source === 'tool'
292
+ || record.source === 'assistant'
293
+ || record.source === 'mission'
294
+ || record.source === 'system'
295
+ ? record.source
296
+ : 'assistant',
297
+ status: normalizeItemStatus(record.status),
298
+ evidenceIds: normalizeEvidenceIds(record.evidenceIds),
299
+ createdAt,
300
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
301
+ ? Math.trunc(record.updatedAt)
302
+ : createdAt,
303
+ }
304
+ }
305
+
306
+ export function normalizeArtifact(input: unknown): WorkingArtifact | null {
307
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null
308
+ const record = input as Record<string, unknown>
309
+ const label = cleanText(record.label, 240)
310
+ if (!label) return null
311
+ const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
312
+ ? Math.trunc(record.createdAt)
313
+ : now()
314
+ return {
315
+ id: cleanText(record.id, 120) || genId(12),
316
+ label,
317
+ kind: record.kind === 'file'
318
+ || record.kind === 'url'
319
+ || record.kind === 'approval'
320
+ || record.kind === 'message'
321
+ || record.kind === 'other'
322
+ ? record.kind
323
+ : 'other',
324
+ path: cleanText(record.path, 320) || null,
325
+ url: cleanText(record.url, 320) || null,
326
+ sourceTool: cleanText(record.sourceTool, 120) || null,
327
+ status: normalizeItemStatus(record.status),
328
+ evidenceIds: normalizeEvidenceIds(record.evidenceIds),
329
+ createdAt,
330
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
331
+ ? Math.trunc(record.updatedAt)
332
+ : createdAt,
333
+ }
334
+ }
335
+
336
+ export function normalizeDecision(input: unknown): WorkingDecision | null {
337
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null
338
+ const record = input as Record<string, unknown>
339
+ const summary = cleanText(record.summary, 280)
340
+ if (!summary) return null
341
+ const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
342
+ ? Math.trunc(record.createdAt)
343
+ : now()
344
+ return {
345
+ id: cleanText(record.id, 120) || genId(12),
346
+ summary,
347
+ rationale: cleanText(record.rationale, 320) || null,
348
+ status: normalizeItemStatus(record.status),
349
+ evidenceIds: normalizeEvidenceIds(record.evidenceIds),
350
+ createdAt,
351
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
352
+ ? Math.trunc(record.updatedAt)
353
+ : createdAt,
354
+ }
355
+ }
356
+
357
+ export function normalizeBlocker(input: unknown): WorkingBlocker | null {
358
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null
359
+ const record = input as Record<string, unknown>
360
+ const summary = cleanText(record.summary, 280)
361
+ if (!summary) return null
362
+ const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
363
+ ? Math.trunc(record.createdAt)
364
+ : now()
365
+ return {
366
+ id: cleanText(record.id, 120) || genId(12),
367
+ summary,
368
+ kind: record.kind === 'approval'
369
+ || record.kind === 'credential'
370
+ || record.kind === 'human_input'
371
+ || record.kind === 'external_dependency'
372
+ || record.kind === 'error'
373
+ || record.kind === 'other'
374
+ ? record.kind
375
+ : null,
376
+ nextAction: cleanText(record.nextAction, 240) || null,
377
+ status: normalizeItemStatus(record.status),
378
+ evidenceIds: normalizeEvidenceIds(record.evidenceIds),
379
+ createdAt,
380
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
381
+ ? Math.trunc(record.updatedAt)
382
+ : createdAt,
383
+ }
384
+ }
385
+
386
+ export function normalizeQuestion(input: unknown): WorkingQuestion | null {
387
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null
388
+ const record = input as Record<string, unknown>
389
+ const question = cleanText(record.question, 280)
390
+ if (!question) return null
391
+ const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
392
+ ? Math.trunc(record.createdAt)
393
+ : now()
394
+ return {
395
+ id: cleanText(record.id, 120) || genId(12),
396
+ question,
397
+ status: normalizeItemStatus(record.status),
398
+ evidenceIds: normalizeEvidenceIds(record.evidenceIds),
399
+ createdAt,
400
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
401
+ ? Math.trunc(record.updatedAt)
402
+ : createdAt,
403
+ }
404
+ }
405
+
406
+ export function normalizeHypothesis(input: unknown): WorkingHypothesis | null {
407
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null
408
+ const record = input as Record<string, unknown>
409
+ const statement = cleanText(record.statement, 280)
410
+ if (!statement) return null
411
+ const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
412
+ ? Math.trunc(record.createdAt)
413
+ : now()
414
+ return {
415
+ id: cleanText(record.id, 120) || genId(12),
416
+ statement,
417
+ confidence: record.confidence === 'low' || record.confidence === 'medium' || record.confidence === 'high'
418
+ ? record.confidence
419
+ : null,
420
+ status: normalizeItemStatus(record.status),
421
+ evidenceIds: normalizeEvidenceIds(record.evidenceIds),
422
+ createdAt,
423
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
424
+ ? Math.trunc(record.updatedAt)
425
+ : createdAt,
426
+ }
427
+ }
428
+
429
+ export function normalizeMatchKey(value: string): string {
430
+ return value.replace(/\s+/g, ' ').trim().toLowerCase()
431
+ }
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // defaultWorkingState & normalizeWorkingState
435
+ // ---------------------------------------------------------------------------
436
+
437
+ export function defaultWorkingState(sessionId: string, mission?: Mission | null): SessionWorkingState {
438
+ const nowTs = now()
439
+ return {
440
+ sessionId,
441
+ missionId: mission?.id || null,
442
+ objective: cleanMultiline(mission?.objective, 900) || null,
443
+ summary: cleanMultiline(mission?.verifierSummary || mission?.plannerSummary, 600) || null,
444
+ constraints: [],
445
+ successCriteria: normalizeList(mission?.successCriteria, 12, 240),
446
+ status: mission ? missionStatusToWorkingStateStatus(mission) : 'idle',
447
+ nextAction: cleanText(mission?.currentStep, 240) || null,
448
+ planSteps: [],
449
+ confirmedFacts: [],
450
+ artifacts: [],
451
+ decisions: [],
452
+ blockers: [],
453
+ openQuestions: [],
454
+ hypotheses: [],
455
+ evidenceRefs: [],
456
+ createdAt: nowTs,
457
+ updatedAt: nowTs,
458
+ lastCompactedAt: null,
459
+ }
460
+ }
461
+
462
+ export function normalizeWorkingState(
463
+ input: unknown,
464
+ sessionId: string,
465
+ mission?: Mission | null,
466
+ ): SessionWorkingState {
467
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
468
+ return defaultWorkingState(sessionId, mission)
469
+ }
470
+ const record = input as Record<string, unknown>
471
+ const base = defaultWorkingState(sessionId, mission)
472
+ const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
473
+ ? Math.trunc(record.createdAt)
474
+ : base.createdAt
475
+ const normalized: SessionWorkingState = {
476
+ sessionId: cleanText(record.sessionId, 120) || sessionId,
477
+ missionId: cleanText(record.missionId, 120) || mission?.id || null,
478
+ objective: cleanMultiline(record.objective, 900) || base.objective,
479
+ summary: cleanMultiline(record.summary, 600) || base.summary,
480
+ constraints: normalizeList(record.constraints, 12, 240),
481
+ successCriteria: normalizeList(record.successCriteria, 12, 240),
482
+ status: normalizeStateStatus(record.status, base.status),
483
+ nextAction: cleanText(record.nextAction, 240) || base.nextAction,
484
+ planSteps: (Array.isArray(record.planSteps) ? record.planSteps.map(normalizePlanStep).filter(Boolean) : []) as WorkingPlanStep[],
485
+ confirmedFacts: (Array.isArray(record.confirmedFacts) ? record.confirmedFacts.map(normalizeFact).filter(Boolean) : []) as WorkingFact[],
486
+ artifacts: (Array.isArray(record.artifacts) ? record.artifacts.map(normalizeArtifact).filter(Boolean) : []) as WorkingArtifact[],
487
+ decisions: (Array.isArray(record.decisions) ? record.decisions.map(normalizeDecision).filter(Boolean) : []) as WorkingDecision[],
488
+ blockers: (Array.isArray(record.blockers) ? record.blockers.map(normalizeBlocker).filter(Boolean) : []) as WorkingBlocker[],
489
+ openQuestions: (Array.isArray(record.openQuestions) ? record.openQuestions.map(normalizeQuestion).filter(Boolean) : []) as WorkingQuestion[],
490
+ hypotheses: (Array.isArray(record.hypotheses) ? record.hypotheses.map(normalizeHypothesis).filter(Boolean) : []) as WorkingHypothesis[],
491
+ evidenceRefs: (Array.isArray(record.evidenceRefs) ? record.evidenceRefs.map(normalizeEvidenceRef).filter(Boolean) : []) as EvidenceRef[],
492
+ createdAt,
493
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
494
+ ? Math.trunc(record.updatedAt)
495
+ : createdAt,
496
+ lastCompactedAt: typeof record.lastCompactedAt === 'number' && Number.isFinite(record.lastCompactedAt)
497
+ ? Math.trunc(record.lastCompactedAt)
498
+ : null,
499
+ }
500
+ return compactWorkingStateObject(syncWorkingStateWithMission(normalized, mission))
501
+ }
502
+
503
+ // ---------------------------------------------------------------------------
504
+ // compactWorkingStateObject
505
+ // ---------------------------------------------------------------------------
506
+
507
+ export function compactWorkingStateObject(state: SessionWorkingState): SessionWorkingState {
508
+ return {
509
+ ...state,
510
+ planSteps: compactPlanSteps(state.planSteps, MAX_PLAN_STEPS),
511
+ confirmedFacts: genericCompact(state.confirmedFacts, MAX_CONFIRMED_FACTS),
512
+ artifacts: genericCompact(state.artifacts, MAX_ARTIFACTS),
513
+ decisions: genericCompact(state.decisions, MAX_DECISIONS),
514
+ blockers: genericCompact(state.blockers, MAX_BLOCKERS),
515
+ openQuestions: genericCompact(state.openQuestions, MAX_OPEN_QUESTIONS),
516
+ hypotheses: genericCompact(state.hypotheses, MAX_HYPOTHESES),
517
+ evidenceRefs: [...state.evidenceRefs]
518
+ .sort((left, right) => (right.createdAt || 0) - (left.createdAt || 0))
519
+ .slice(0, MAX_EVIDENCE_REFS),
520
+ lastCompactedAt: now(),
521
+ }
522
+ }
523
+
524
+ // ---------------------------------------------------------------------------
525
+ // missionStatusToWorkingStateStatus & syncWorkingStateWithMission
526
+ // ---------------------------------------------------------------------------
527
+
528
+ export function missionStatusToWorkingStateStatus(mission: Mission): WorkingStateStatus {
529
+ if (mission.status === 'completed') return 'completed'
530
+ if (mission.status === 'waiting') return 'waiting'
531
+ if (mission.status === 'failed' || mission.status === 'cancelled') return 'blocked'
532
+ return 'progress'
533
+ }
534
+
535
+ export function syncWorkingStateWithMission(
536
+ state: SessionWorkingState,
537
+ mission?: Mission | null,
538
+ ): SessionWorkingState {
539
+ if (!mission) return state
540
+ const next = { ...state }
541
+ next.missionId = mission.id
542
+ next.objective = cleanMultiline(mission.objective, 900) || next.objective
543
+ next.successCriteria = normalizeList(mission.successCriteria, 12, 240)
544
+ next.summary = next.summary || cleanMultiline(mission.verifierSummary || mission.plannerSummary, 600) || null
545
+ const missionStatus = missionStatusToWorkingStateStatus(mission)
546
+ if (missionStatus === 'completed' || missionStatus === 'waiting' || missionStatus === 'blocked') {
547
+ next.status = missionStatus
548
+ } else if (next.status === 'idle') {
549
+ next.status = missionStatus
550
+ }
551
+ next.nextAction = next.nextAction || cleanText(mission.currentStep, 240) || null
552
+
553
+ if (mission.currentStep) {
554
+ next.planSteps = upsertItems(next.planSteps, [{
555
+ id: null,
556
+ text: mission.currentStep,
557
+ status: mission.status === 'completed' ? 'resolved' : 'active',
558
+ } satisfies WorkingPlanStepPatch], {
559
+ max: MAX_PLAN_STEPS,
560
+ getPatchId: (patch) => cleanText(patch.id, 120) || null,
561
+ getPatchKey: (patch) => cleanText(patch.text, 240),
562
+ getItemKey: (item) => item.text,
563
+ create: (patch, nowTs) => ({
564
+ id: genId(12),
565
+ text: cleanText(patch.text, 240),
566
+ status: normalizeItemStatus(patch.status),
567
+ createdAt: nowTs,
568
+ updatedAt: nowTs,
569
+ }),
570
+ merge: (current, patch, nowTs) => ({
571
+ ...current,
572
+ text: cleanText(patch.text, 240) || current.text,
573
+ status: normalizeItemStatus(patch.status, current.status),
574
+ updatedAt: nowTs,
575
+ }),
576
+ compact: compactPlanSteps,
577
+ })
578
+ }
579
+
580
+ if (mission.waitState?.reason || mission.blockerSummary) {
581
+ const blockerSummary = cleanText(mission.waitState?.reason || mission.blockerSummary, 280)
582
+ if (blockerSummary) {
583
+ next.blockers = upsertItems(next.blockers, [{
584
+ summary: blockerSummary,
585
+ kind: mission.waitState?.kind === 'approval'
586
+ ? 'approval'
587
+ : mission.waitState?.kind === 'human_reply'
588
+ ? 'human_input'
589
+ : mission.waitState?.kind === 'external_dependency' || mission.waitState?.kind === 'provider'
590
+ ? 'external_dependency'
591
+ : mission.status === 'failed'
592
+ ? 'error'
593
+ : 'other',
594
+ nextAction: mission.currentStep || null,
595
+ status: mission.status === 'completed' ? 'resolved' : 'active',
596
+ }], blockerUpsertConfig())
597
+ }
598
+ }
599
+
600
+ if (mission.status === 'completed') {
601
+ next.blockers = next.blockers.map((blocker) => blocker.status === 'active'
602
+ ? { ...blocker, status: 'resolved', updatedAt: now() }
603
+ : blocker)
604
+ }
605
+
606
+ return next
607
+ }
608
+
609
+ // ---------------------------------------------------------------------------
610
+ // upsertItems & upsertConfig factories
611
+ // ---------------------------------------------------------------------------
612
+
613
+ export function upsertItems<TItem extends TimedWorkingItem, TPatch>(
614
+ items: TItem[],
615
+ patches: TPatch[] | undefined,
616
+ config: UpsertConfig<TItem, TPatch>,
617
+ ): TItem[] {
618
+ if (!Array.isArray(patches) || patches.length === 0) return items
619
+ const next = [...items]
620
+ const nowTs = now()
621
+ for (const patch of patches) {
622
+ const key = normalizeMatchKey(config.getPatchKey(patch))
623
+ if (!key) continue
624
+ const patchId = config.getPatchId(patch)
625
+ const index = next.findIndex((item) => {
626
+ if (patchId && item.id === patchId) return true
627
+ return normalizeMatchKey(config.getItemKey(item)) === key
628
+ })
629
+ if (index >= 0) {
630
+ next[index] = config.merge(next[index], patch, nowTs)
631
+ } else {
632
+ next.push(config.create(patch, nowTs))
633
+ }
634
+ }
635
+ return (config.compact || genericCompact)(next, config.max)
636
+ }
637
+
638
+ export function factUpsertConfig(): UpsertConfig<WorkingFact, WorkingFactPatch> {
639
+ return {
640
+ max: MAX_CONFIRMED_FACTS,
641
+ getPatchId: (patch) => cleanText(patch.id, 120) || null,
642
+ getPatchKey: (patch) => cleanText(patch.statement, 280),
643
+ getItemKey: (item) => item.statement,
644
+ create: (patch, nowTs) => ({
645
+ id: cleanText(patch.id, 120) || genId(12),
646
+ statement: cleanText(patch.statement, 280),
647
+ source: patch.source === 'user'
648
+ || patch.source === 'tool'
649
+ || patch.source === 'assistant'
650
+ || patch.source === 'mission'
651
+ || patch.source === 'system'
652
+ ? patch.source
653
+ : 'assistant',
654
+ status: normalizeItemStatus(patch.status),
655
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds),
656
+ createdAt: nowTs,
657
+ updatedAt: nowTs,
658
+ }),
659
+ merge: (current, patch, nowTs) => ({
660
+ ...current,
661
+ statement: cleanText(patch.statement, 280) || current.statement,
662
+ source: patch.source === 'user'
663
+ || patch.source === 'tool'
664
+ || patch.source === 'assistant'
665
+ || patch.source === 'mission'
666
+ || patch.source === 'system'
667
+ ? patch.source
668
+ : current.source,
669
+ status: normalizeItemStatus(patch.status, current.status),
670
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds) || current.evidenceIds,
671
+ updatedAt: nowTs,
672
+ }),
673
+ }
674
+ }
675
+
676
+ export function artifactUpsertConfig(): UpsertConfig<WorkingArtifact, WorkingArtifactPatch> {
677
+ return {
678
+ max: MAX_ARTIFACTS,
679
+ getPatchId: (patch) => cleanText(patch.id, 120) || null,
680
+ getPatchKey: (patch) => cleanText(patch.path || patch.url || patch.label, 320),
681
+ getItemKey: (item) => cleanText(item.path || item.url || item.label, 320),
682
+ create: (patch, nowTs) => ({
683
+ id: cleanText(patch.id, 120) || genId(12),
684
+ label: cleanText(patch.label, 240),
685
+ kind: patch.kind === 'file'
686
+ || patch.kind === 'url'
687
+ || patch.kind === 'approval'
688
+ || patch.kind === 'message'
689
+ || patch.kind === 'other'
690
+ ? patch.kind
691
+ : 'other',
692
+ path: cleanText(patch.path, 320) || null,
693
+ url: cleanText(patch.url, 320) || null,
694
+ sourceTool: cleanText(patch.sourceTool, 120) || null,
695
+ status: normalizeItemStatus(patch.status),
696
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds),
697
+ createdAt: nowTs,
698
+ updatedAt: nowTs,
699
+ }),
700
+ merge: (current, patch, nowTs) => ({
701
+ ...current,
702
+ label: cleanText(patch.label, 240) || current.label,
703
+ kind: patch.kind === 'file'
704
+ || patch.kind === 'url'
705
+ || patch.kind === 'approval'
706
+ || patch.kind === 'message'
707
+ || patch.kind === 'other'
708
+ ? patch.kind
709
+ : current.kind,
710
+ path: cleanText(patch.path, 320) || current.path,
711
+ url: cleanText(patch.url, 320) || current.url,
712
+ sourceTool: cleanText(patch.sourceTool, 120) || current.sourceTool,
713
+ status: normalizeItemStatus(patch.status, current.status),
714
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds) || current.evidenceIds,
715
+ updatedAt: nowTs,
716
+ }),
717
+ }
718
+ }
719
+
720
+ export function decisionUpsertConfig(): UpsertConfig<WorkingDecision, WorkingDecisionPatch> {
721
+ return {
722
+ max: MAX_DECISIONS,
723
+ getPatchId: (patch) => cleanText(patch.id, 120) || null,
724
+ getPatchKey: (patch) => cleanText(patch.summary, 280),
725
+ getItemKey: (item) => item.summary,
726
+ create: (patch, nowTs) => ({
727
+ id: cleanText(patch.id, 120) || genId(12),
728
+ summary: cleanText(patch.summary, 280),
729
+ rationale: cleanText(patch.rationale, 320) || null,
730
+ status: normalizeItemStatus(patch.status),
731
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds),
732
+ createdAt: nowTs,
733
+ updatedAt: nowTs,
734
+ }),
735
+ merge: (current, patch, nowTs) => ({
736
+ ...current,
737
+ summary: cleanText(patch.summary, 280) || current.summary,
738
+ rationale: cleanText(patch.rationale, 320) || current.rationale,
739
+ status: normalizeItemStatus(patch.status, current.status),
740
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds) || current.evidenceIds,
741
+ updatedAt: nowTs,
742
+ }),
743
+ }
744
+ }
745
+
746
+ export function blockerUpsertConfig(): UpsertConfig<WorkingBlocker, WorkingBlockerPatch> {
747
+ return {
748
+ max: MAX_BLOCKERS,
749
+ getPatchId: (patch) => cleanText(patch.id, 120) || null,
750
+ getPatchKey: (patch) => cleanText(patch.summary, 280),
751
+ getItemKey: (item) => item.summary,
752
+ create: (patch, nowTs) => ({
753
+ id: cleanText(patch.id, 120) || genId(12),
754
+ summary: cleanText(patch.summary, 280),
755
+ kind: patch.kind || null,
756
+ nextAction: cleanText(patch.nextAction, 240) || null,
757
+ status: normalizeItemStatus(patch.status),
758
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds),
759
+ createdAt: nowTs,
760
+ updatedAt: nowTs,
761
+ }),
762
+ merge: (current, patch, nowTs) => ({
763
+ ...current,
764
+ summary: cleanText(patch.summary, 280) || current.summary,
765
+ kind: patch.kind || current.kind,
766
+ nextAction: cleanText(patch.nextAction, 240) || current.nextAction,
767
+ status: normalizeItemStatus(patch.status, current.status),
768
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds) || current.evidenceIds,
769
+ updatedAt: nowTs,
770
+ }),
771
+ }
772
+ }
773
+
774
+ export function questionUpsertConfig(): UpsertConfig<WorkingQuestion, WorkingQuestionPatch> {
775
+ return {
776
+ max: MAX_OPEN_QUESTIONS,
777
+ getPatchId: (patch) => cleanText(patch.id, 120) || null,
778
+ getPatchKey: (patch) => cleanText(patch.question, 280),
779
+ getItemKey: (item) => item.question,
780
+ create: (patch, nowTs) => ({
781
+ id: cleanText(patch.id, 120) || genId(12),
782
+ question: cleanText(patch.question, 280),
783
+ status: normalizeItemStatus(patch.status),
784
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds),
785
+ createdAt: nowTs,
786
+ updatedAt: nowTs,
787
+ }),
788
+ merge: (current, patch, nowTs) => ({
789
+ ...current,
790
+ question: cleanText(patch.question, 280) || current.question,
791
+ status: normalizeItemStatus(patch.status, current.status),
792
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds) || current.evidenceIds,
793
+ updatedAt: nowTs,
794
+ }),
795
+ }
796
+ }
797
+
798
+ export function hypothesisUpsertConfig(): UpsertConfig<WorkingHypothesis, WorkingHypothesisPatch> {
799
+ return {
800
+ max: MAX_HYPOTHESES,
801
+ getPatchId: (patch) => cleanText(patch.id, 120) || null,
802
+ getPatchKey: (patch) => cleanText(patch.statement, 280),
803
+ getItemKey: (item) => item.statement,
804
+ create: (patch, nowTs) => ({
805
+ id: cleanText(patch.id, 120) || genId(12),
806
+ statement: cleanText(patch.statement, 280),
807
+ confidence: patch.confidence === 'low' || patch.confidence === 'medium' || patch.confidence === 'high'
808
+ ? patch.confidence
809
+ : null,
810
+ status: normalizeItemStatus(patch.status),
811
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds),
812
+ createdAt: nowTs,
813
+ updatedAt: nowTs,
814
+ }),
815
+ merge: (current, patch, nowTs) => ({
816
+ ...current,
817
+ statement: cleanText(patch.statement, 280) || current.statement,
818
+ confidence: patch.confidence === 'low' || patch.confidence === 'medium' || patch.confidence === 'high'
819
+ ? patch.confidence
820
+ : current.confidence,
821
+ status: normalizeItemStatus(patch.status, current.status),
822
+ evidenceIds: normalizeEvidenceIds(patch.evidenceIds) || current.evidenceIds,
823
+ updatedAt: nowTs,
824
+ }),
825
+ }
826
+ }
827
+
828
+ // ---------------------------------------------------------------------------
829
+ // appendEvidenceRefs & markSuperseded
830
+ // ---------------------------------------------------------------------------
831
+
832
+ export function appendEvidenceRefs(current: EvidenceRef[], additions: EvidenceRef[] | undefined): EvidenceRef[] {
833
+ if (!Array.isArray(additions) || additions.length === 0) return current
834
+ const merged = [...current]
835
+ for (const addition of additions) {
836
+ const normalized = normalizeEvidenceRef(addition)
837
+ if (!normalized) continue
838
+ const matchIndex = merged.findIndex((entry) => {
839
+ if (normalized.toolCallId && entry.toolCallId && entry.toolCallId === normalized.toolCallId) return true
840
+ return entry.type === normalized.type
841
+ && normalizeMatchKey(entry.summary) === normalizeMatchKey(normalized.summary)
842
+ && normalizeMatchKey(entry.value || '') === normalizeMatchKey(normalized.value || '')
843
+ })
844
+ if (matchIndex >= 0) {
845
+ merged[matchIndex] = {
846
+ ...merged[matchIndex],
847
+ ...normalized,
848
+ }
849
+ } else {
850
+ merged.push(normalized)
851
+ }
852
+ }
853
+ return merged
854
+ .sort((left, right) => (right.createdAt || 0) - (left.createdAt || 0))
855
+ .slice(0, MAX_EVIDENCE_REFS)
856
+ }
857
+
858
+ export function markSuperseded<TItem extends TimedWorkingItem>(items: TItem[], ids: string[] | undefined): TItem[] {
859
+ if (!Array.isArray(ids) || ids.length === 0) return items
860
+ const idSet = new Set(ids.map((id) => cleanText(id, 120)).filter(Boolean))
861
+ if (idSet.size === 0) return items
862
+ const nowTs = now()
863
+ return items.map((item) => (idSet.has(item.id)
864
+ ? { ...item, status: 'superseded' as WorkingStateItemStatus, updatedAt: nowTs }
865
+ : item))
866
+ }