arkaos 3.71.0 → 3.72.0

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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.71.0
1
+ 3.72.0
@@ -16,6 +16,7 @@ from core.terminal.session import (
16
16
  )
17
17
  from core.terminal.audit import log_end, log_start
18
18
  from core.terminal.token import current_token
19
+ from core.terminal.connections import ConnectionRegistry
19
20
 
20
21
  __all__ = [
21
22
  "TerminalSession",
@@ -24,4 +25,5 @@ __all__ = [
24
25
  "log_start",
25
26
  "log_end",
26
27
  "current_token",
28
+ "ConnectionRegistry",
27
29
  ]
@@ -0,0 +1,46 @@
1
+ """Single active WebSocket connection per terminal session (v3.71.1).
2
+
3
+ asyncio allows only one reader per fd, so two concurrent WebSockets on the
4
+ same session would fight over the PTY master fd and the scrollback replay
5
+ could duplicate output. This registry enforces "latest wins": a new
6
+ connection supersedes the previous one (which the endpoint then closes),
7
+ and release is guarded so a superseded connection's teardown cannot evict
8
+ its replacement.
9
+
10
+ Pure bookkeeping — no FastAPI import, so it's unit-testable standalone.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Optional
16
+
17
+
18
+ class ConnectionRegistry:
19
+ """Tracks the one live connection per session id."""
20
+
21
+ def __init__(self) -> None:
22
+ self._active: dict[str, Any] = {}
23
+
24
+ def acquire(self, session_id: str, conn: Any) -> Optional[Any]:
25
+ """Make ``conn`` the active connection for ``session_id``.
26
+
27
+ Returns the connection it superseded (for the caller to close), or
28
+ ``None`` when there was none / it was already active.
29
+ """
30
+ old = self._active.get(session_id)
31
+ self._active[session_id] = conn
32
+ return old if old is not conn else None
33
+
34
+ def release(self, session_id: str, conn: Any) -> bool:
35
+ """Drop ``conn`` iff it is still the active connection.
36
+
37
+ Returns whether it was — the caller should only tear down shared
38
+ resources (the fd reader) when this is ``True``.
39
+ """
40
+ if self._active.get(session_id) is conn:
41
+ del self._active[session_id]
42
+ return True
43
+ return False
44
+
45
+ def is_active(self, session_id: str, conn: Any) -> bool:
46
+ return self._active.get(session_id) is conn
@@ -80,6 +80,13 @@ const links = [[{
80
80
  onSelect: () => {
81
81
  open.value = false
82
82
  }
83
+ }, {
84
+ label: 'Dreaming',
85
+ icon: 'i-lucide-sparkles',
86
+ to: '/cognition',
87
+ onSelect: () => {
88
+ open.value = false
89
+ }
83
90
  }], [{
84
91
  label: 'Health',
85
92
  icon: 'i-lucide-heart-pulse',
@@ -0,0 +1,311 @@
1
+ <script setup lang="ts">
2
+ // v3.72.0 — Cognition page: monitor what Dreaming has been learning.
3
+ // Read-only view over the insights the Cognitive Layer already writes to
4
+ // <vault>/Projects/ArkaOS/Dreams. Backend: /api/cognition/{insights,status}.
5
+
6
+ import { marked } from 'marked'
7
+
8
+ definePageMeta({ layout: 'default' })
9
+
10
+ interface Insight {
11
+ date: string
12
+ title: string
13
+ confidence: string
14
+ sources: string[]
15
+ tags: string[]
16
+ body: string
17
+ }
18
+ interface Status {
19
+ today: number
20
+ week: number
21
+ total: number
22
+ by_confidence: { high: number, medium: number, low: number }
23
+ vault_configured: boolean
24
+ last_date: string | null
25
+ }
26
+
27
+ const { apiBase } = useApi()
28
+
29
+ const days = ref(7)
30
+ const confidenceFilter = ref<'all' | 'high' | 'medium' | 'low'>('all')
31
+ const tagFilter = ref('')
32
+ const insights = ref<Insight[]>([])
33
+ const status = ref<Status | null>(null)
34
+ const available = ref(true)
35
+ const loading = ref(true)
36
+ const expanded = ref<Set<string>>(new Set())
37
+
38
+ const windowOptions = [
39
+ { label: 'Today', value: 1 },
40
+ { label: 'Last 7 days', value: 7 },
41
+ { label: 'Last 30 days', value: 30 }
42
+ ]
43
+ const confidenceOptions = [
44
+ { label: 'All confidence', value: 'all' },
45
+ { label: 'High', value: 'high' },
46
+ { label: 'Medium', value: 'medium' },
47
+ { label: 'Low', value: 'low' }
48
+ ]
49
+
50
+ async function refresh() {
51
+ loading.value = true
52
+ try {
53
+ const [s, i] = await Promise.all([
54
+ $fetch<Status>(`${apiBase}/api/cognition/status`),
55
+ $fetch<{ insights: Insight[], available: boolean }>(
56
+ `${apiBase}/api/cognition/insights?days=${days.value}`
57
+ )
58
+ ])
59
+ status.value = s
60
+ insights.value = i.insights
61
+ available.value = i.available
62
+ } catch {
63
+ available.value = false
64
+ } finally {
65
+ loading.value = false
66
+ }
67
+ }
68
+
69
+ onMounted(refresh)
70
+ // Reset the tag filter when the window changes — a tag may not exist in the
71
+ // new window, which would otherwise strand the user on an empty result.
72
+ watch(days, () => {
73
+ tagFilter.value = ''
74
+ refresh()
75
+ })
76
+
77
+ const allTags = computed(() => {
78
+ const set = new Set<string>()
79
+ for (const i of insights.value) for (const t of i.tags) set.add(t)
80
+ return [...set].sort()
81
+ })
82
+
83
+ const filtered = computed(() => insights.value.filter((i) => {
84
+ if (confidenceFilter.value !== 'all' && i.confidence !== confidenceFilter.value) return false
85
+ if (tagFilter.value && !i.tags.includes(tagFilter.value)) return false
86
+ return true
87
+ }))
88
+
89
+ function confidenceColor(c: string): 'success' | 'warning' | 'neutral' {
90
+ if (c === 'high') return 'success'
91
+ if (c === 'low') return 'neutral'
92
+ return 'warning'
93
+ }
94
+
95
+ function toggle(key: string) {
96
+ const next = new Set(expanded.value)
97
+ if (next.has(key)) next.delete(key)
98
+ else next.add(key)
99
+ expanded.value = next
100
+ }
101
+
102
+ function renderBody(body: string): string {
103
+ if (!body) return ''
104
+ try {
105
+ return marked.parse(body, { async: false }) as string
106
+ } catch {
107
+ return body
108
+ }
109
+ }
110
+
111
+ const isActive = computed(() => {
112
+ if (!status.value?.last_date) return false
113
+ const last = new Date(status.value.last_date + 'T00:00:00Z').getTime()
114
+ return Date.now() - last < 3 * 86_400_000
115
+ })
116
+ </script>
117
+
118
+ <template>
119
+ <UDashboardPanel id="cognition">
120
+ <template #header>
121
+ <UDashboardNavbar title="Dreaming">
122
+ <template #leading>
123
+ <UDashboardSidebarCollapse />
124
+ </template>
125
+ <template #right>
126
+ <UBadge
127
+ :color="isActive ? 'success' : 'neutral'"
128
+ variant="soft"
129
+ size="sm"
130
+ >
131
+ <UIcon
132
+ :name="isActive ? 'i-lucide-activity' : 'i-lucide-moon'"
133
+ class="size-3 mr-1"
134
+ />
135
+ {{ isActive ? 'Active' : 'Idle' }}
136
+ <span v-if="status?.last_date" class="ml-1 opacity-70">· {{ status.last_date }}</span>
137
+ </UBadge>
138
+ </template>
139
+ </UDashboardNavbar>
140
+ </template>
141
+
142
+ <template #body>
143
+ <div class="flex flex-col gap-4 p-4">
144
+ <p class="text-sm text-muted -mt-1">
145
+ What the Cognitive Layer has been learning — insights surfaced by
146
+ Dreaming from your vault and sessions.
147
+ </p>
148
+
149
+ <!-- Stats -->
150
+ <div v-if="status?.vault_configured" class="grid grid-cols-2 sm:grid-cols-4 gap-3">
151
+ <div class="rounded-lg border border-default bg-elevated/10 p-3">
152
+ <div class="text-2xl font-semibold tabular-nums">
153
+ {{ status.today }}
154
+ </div>
155
+ <div class="text-xs text-muted">
156
+ today
157
+ </div>
158
+ </div>
159
+ <div class="rounded-lg border border-default bg-elevated/10 p-3">
160
+ <div class="text-2xl font-semibold tabular-nums">
161
+ {{ status.week }}
162
+ </div>
163
+ <div class="text-xs text-muted">
164
+ last 7 days
165
+ </div>
166
+ </div>
167
+ <div class="rounded-lg border border-default bg-elevated/10 p-3">
168
+ <div class="text-2xl font-semibold tabular-nums">
169
+ {{ status.total }}
170
+ </div>
171
+ <div class="text-xs text-muted">
172
+ total insights
173
+ </div>
174
+ </div>
175
+ <div class="rounded-lg border border-default bg-elevated/10 p-3 flex flex-col justify-center gap-1">
176
+ <div class="flex items-center gap-2 text-xs">
177
+ <UBadge color="success" variant="soft" size="xs">
178
+ high {{ status.by_confidence.high }}
179
+ </UBadge>
180
+ <UBadge color="warning" variant="soft" size="xs">
181
+ med {{ status.by_confidence.medium }}
182
+ </UBadge>
183
+ <UBadge color="neutral" variant="soft" size="xs">
184
+ low {{ status.by_confidence.low }}
185
+ </UBadge>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ <!-- Filters -->
191
+ <div class="flex flex-wrap items-center gap-2">
192
+ <USelect
193
+ v-model="days"
194
+ :items="windowOptions"
195
+ size="sm"
196
+ class="w-40"
197
+ />
198
+ <USelect
199
+ v-model="confidenceFilter"
200
+ :items="confidenceOptions"
201
+ size="sm"
202
+ class="w-44"
203
+ />
204
+ <USelect
205
+ v-if="allTags.length"
206
+ v-model="tagFilter"
207
+ :items="[{ label: 'All tags', value: '' }, ...allTags.map(t => ({ label: '#' + t, value: t }))]"
208
+ size="sm"
209
+ class="w-40"
210
+ />
211
+ <UButton
212
+ size="sm"
213
+ variant="ghost"
214
+ icon="i-lucide-refresh-cw"
215
+ :loading="loading"
216
+ @click="refresh"
217
+ >
218
+ Refresh
219
+ </UButton>
220
+ </div>
221
+
222
+ <!-- Feed -->
223
+ <div v-if="loading" class="text-sm text-muted py-8 text-center">
224
+ <UIcon name="i-lucide-loader" class="animate-spin size-5 mx-auto mb-2" />
225
+ Loading insights…
226
+ </div>
227
+ <div
228
+ v-else-if="!available || !status?.vault_configured"
229
+ class="rounded-lg border border-dashed border-default p-10 text-center text-muted"
230
+ >
231
+ <UIcon name="i-lucide-moon-star" class="size-8 mx-auto mb-3 opacity-40" />
232
+ <p class="text-sm">
233
+ No vault connected, or Dreaming hasn't run yet.
234
+ </p>
235
+ <p class="text-xs mt-1 opacity-70">
236
+ Configure a vault and let the Cognitive Layer dream — insights show up here.
237
+ </p>
238
+ </div>
239
+ <div
240
+ v-else-if="filtered.length === 0"
241
+ class="rounded-lg border border-dashed border-default p-10 text-center text-muted"
242
+ >
243
+ <UIcon name="i-lucide-search-x" class="size-7 mx-auto mb-3 opacity-40" />
244
+ <p class="text-sm">
245
+ No insights match these filters.
246
+ </p>
247
+ </div>
248
+ <ul v-else class="flex flex-col gap-3">
249
+ <li
250
+ v-for="(ins, idx) in filtered"
251
+ :key="`${ins.date}-${idx}`"
252
+ class="rounded-lg border border-default bg-elevated/10 overflow-hidden"
253
+ >
254
+ <button
255
+ class="w-full flex items-start gap-3 p-3 text-left hover:bg-elevated/20 transition-colors"
256
+ @click="toggle(`${ins.date}-${idx}`)"
257
+ >
258
+ <UBadge
259
+ :color="confidenceColor(ins.confidence)"
260
+ variant="soft"
261
+ size="xs"
262
+ class="mt-0.5 shrink-0 uppercase"
263
+ >
264
+ {{ ins.confidence }}
265
+ </UBadge>
266
+ <div class="flex-1 min-w-0">
267
+ <div class="font-medium truncate">
268
+ {{ ins.title }}
269
+ </div>
270
+ <div class="flex items-center gap-2 mt-1 flex-wrap">
271
+ <span class="text-xs text-muted tabular-nums">{{ ins.date }}</span>
272
+ <UBadge
273
+ v-for="t in ins.tags"
274
+ :key="t"
275
+ variant="subtle"
276
+ size="xs"
277
+ >
278
+ #{{ t }}
279
+ </UBadge>
280
+ </div>
281
+ </div>
282
+ <UIcon
283
+ :name="expanded.has(`${ins.date}-${idx}`) ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
284
+ class="size-4 shrink-0 text-muted mt-0.5"
285
+ />
286
+ </button>
287
+ <div v-if="expanded.has(`${ins.date}-${idx}`)" class="px-3 pb-3 border-t border-default/60 pt-3">
288
+ <!-- Trusted content: the operator's own Dreaming output from
289
+ their local vault, served over the localhost-only API —
290
+ same trust boundary as the persona bio renderer. -->
291
+ <!-- eslint-disable-next-line vue/no-v-html -->
292
+ <div class="prose prose-sm dark:prose-invert max-w-none text-sm" v-html="renderBody(ins.body)" />
293
+ <div v-if="ins.sources.length" class="flex items-center gap-1.5 flex-wrap mt-3 pt-2 border-t border-default/40">
294
+ <span class="text-[11px] text-muted">sources:</span>
295
+ <UBadge
296
+ v-for="s in ins.sources"
297
+ :key="s"
298
+ variant="subtle"
299
+ size="xs"
300
+ color="info"
301
+ >
302
+ {{ s }}
303
+ </UBadge>
304
+ </div>
305
+ </div>
306
+ </li>
307
+ </ul>
308
+ </div>
309
+ </template>
310
+ </UDashboardPanel>
311
+ </template>