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 +1 -1
- package/core/terminal/__init__.py +2 -0
- package/core/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/connections.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
- package/core/terminal/connections.py +46 -0
- package/dashboard/app/layouts/default.vue +7 -0
- package/dashboard/app/pages/cognition.vue +311 -0
- package/dashboard/app/pages/settings.vue +215 -51
- package/dashboard/package.json +3 -1
- package/installer/autostart.js +178 -0
- package/installer/cli.js +7 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +178 -5
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
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
|
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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>
|