mixdog 0.9.87 → 0.9.89
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/package.json +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read-only session summary catalog for cold desktop startup.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* and atomic-lock writers.
|
|
6
|
-
*
|
|
4
|
+
* The summary path intentionally avoids store.mjs, config/provider loading,
|
|
5
|
+
* workers, and atomic-lock writers. A transcript read stays lightweight too
|
|
6
|
+
* unless a durable turn checkpoint exists; only then does it lazily enter the
|
|
7
|
+
* reconnect recovery boundary so restored panes never paint a stale prompt.
|
|
7
8
|
*/
|
|
8
|
-
import {
|
|
9
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
9
10
|
import { homedir } from 'os';
|
|
10
11
|
import { join } from 'path';
|
|
12
|
+
// Leaf helpers only (no store.mjs, no workers, no config): the three-way
|
|
13
|
+
// present/absent/unreadable classification and the strict record parser the
|
|
14
|
+
// authoritative store uses, so the cold catalog cannot disagree with it.
|
|
15
|
+
import { probePath, readTextFile, PROBE_PRESENT, PROBE_ABSENT } from './store/fs-probe.mjs';
|
|
16
|
+
import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from './lifecycle-scan.mjs';
|
|
11
17
|
|
|
12
18
|
const SESSION_SUMMARY_INDEX_VERSION = 2;
|
|
19
|
+
const TERMINAL_AGENT_STATUS = /idle|done|complete|success|closed|error|fail|cancel|killed|timeout/i;
|
|
20
|
+
const ACTIVE_AGENT_STATUS = /^(?:connecting|requesting|streaming|tool[-_\s]?running|running|queued|pending|starting|cancelling)$/i;
|
|
21
|
+
const AGENT_POOL_HEARTBEAT_FRESH_MS = 2 * 60 * 1000;
|
|
13
22
|
|
|
14
23
|
// Mirror of lifecycle-api.mjs listLeadSessions visibility: the cold catalog
|
|
15
24
|
// must never surface worker/agent dispatches (memory ingest chunks, judges,
|
|
@@ -46,6 +55,125 @@ function positiveNumber(value, fallback = 0) {
|
|
|
46
55
|
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
function cleanValue(value) {
|
|
59
|
+
return String(value || '').trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function activeAgentWorker(row) {
|
|
63
|
+
const statuses = [row?.stage, row?.status]
|
|
64
|
+
.map(cleanValue)
|
|
65
|
+
.filter((status, index, values) => Boolean(status) && values.indexOf(status) === index);
|
|
66
|
+
return statuses.length > 0
|
|
67
|
+
&& !statuses.some((status) => TERMINAL_AGENT_STATUS.test(status))
|
|
68
|
+
&& statuses.some((status) => ACTIVE_AGENT_STATUS.test(status));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function storedAgentWorkerIndexPath() {
|
|
72
|
+
return join(dataDir(), 'agent-workers.json');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Process-global active agent pool. Fresh child heartbeat sidecars are the
|
|
76
|
+
* cross-process running source even when their durable session is detached
|
|
77
|
+
* (`closed`) and a terminal reaper has already removed the worker-index row.
|
|
78
|
+
* The index remains additive for active rows published before the heartbeat or
|
|
79
|
+
* by runtimes without a sidecar. No runtime starts. */
|
|
80
|
+
export function listStoredAgentWorkers() {
|
|
81
|
+
let parsed = null;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(readFileSync(storedAgentWorkerIndexPath(), 'utf8'));
|
|
84
|
+
} catch { /* heartbeat-backed rows remain authoritative without the index */ }
|
|
85
|
+
const source = Array.isArray(parsed?.workers)
|
|
86
|
+
? parsed.workers
|
|
87
|
+
: parsed?.workers && typeof parsed.workers === 'object'
|
|
88
|
+
? Object.values(parsed.workers)
|
|
89
|
+
: [];
|
|
90
|
+
const bySessionId = new Map();
|
|
91
|
+
for (const row of source) {
|
|
92
|
+
if (!row || typeof row !== 'object' || !activeAgentWorker(row)) continue;
|
|
93
|
+
const sessionId = cleanValue(row.sessionId);
|
|
94
|
+
const tag = cleanValue(row.tag);
|
|
95
|
+
if (!sessionId || !tag || !/^[A-Za-z0-9_-]+$/.test(sessionId)) continue;
|
|
96
|
+
let session = null;
|
|
97
|
+
if (!cleanValue(row.ownerSessionId || row.parentSessionId)) {
|
|
98
|
+
try {
|
|
99
|
+
session = JSON.parse(readFileSync(
|
|
100
|
+
join(dataDir(), 'sessions', `${sessionId}.json`),
|
|
101
|
+
'utf8',
|
|
102
|
+
));
|
|
103
|
+
} catch { /* a new row may precede its first session save */ }
|
|
104
|
+
}
|
|
105
|
+
bySessionId.set(sessionId, {
|
|
106
|
+
tag,
|
|
107
|
+
sessionId,
|
|
108
|
+
ownerSessionId: cleanValue(
|
|
109
|
+
row.ownerSessionId || row.parentSessionId
|
|
110
|
+
|| session?.ownerSessionId || session?.parentSessionId,
|
|
111
|
+
) || null,
|
|
112
|
+
agent: cleanValue(row.agent || session?.agent) || null,
|
|
113
|
+
provider: cleanValue(row.provider || session?.provider) || null,
|
|
114
|
+
model: cleanValue(row.model || session?.model) || null,
|
|
115
|
+
status: cleanValue(row.status) || 'running',
|
|
116
|
+
stage: cleanValue(row.stage || row.status) || 'running',
|
|
117
|
+
startedAt: row.startedAt || row.createdAt || session?.createdAt || null,
|
|
118
|
+
turnStartedAt: row.turnStartedAt || null,
|
|
119
|
+
createdAt: row.createdAt || session?.createdAt || null,
|
|
120
|
+
updatedAt: row.updatedAt || session?.updatedAt || null,
|
|
121
|
+
cwd: cleanValue(row.cwd || session?.cwd) || null,
|
|
122
|
+
clientHostPid: positiveNumber(row.clientHostPid || session?.clientHostPid, 0) || null,
|
|
123
|
+
taskId: cleanValue(row.task_id || row.taskId) || null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
for (const [sessionId, heartbeatAt] of sessionHeartbeatMtimes()) {
|
|
128
|
+
if (now - heartbeatAt > AGENT_POOL_HEARTBEAT_FRESH_MS) continue;
|
|
129
|
+
let session;
|
|
130
|
+
try {
|
|
131
|
+
session = JSON.parse(readFileSync(
|
|
132
|
+
join(dataDir(), 'sessions', `${sessionId}.json`),
|
|
133
|
+
'utf8',
|
|
134
|
+
));
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const ownerSessionId = cleanValue(
|
|
139
|
+
session?.ownerSessionId || session?.parentSessionId,
|
|
140
|
+
);
|
|
141
|
+
const owner = cleanValue(session?.owner).toLowerCase();
|
|
142
|
+
const agent = cleanValue(session?.agent);
|
|
143
|
+
if (!ownerSessionId || (owner !== 'agent' && (!agent || agent === 'lead'))) continue;
|
|
144
|
+
const current = bySessionId.get(sessionId) || {};
|
|
145
|
+
bySessionId.set(sessionId, {
|
|
146
|
+
...current,
|
|
147
|
+
tag: cleanValue(session?.agentTag) || cleanValue(current.tag)
|
|
148
|
+
|| `${agent || 'agent'}:${sessionId}`,
|
|
149
|
+
sessionId,
|
|
150
|
+
ownerSessionId,
|
|
151
|
+
agent: agent || current.agent || null,
|
|
152
|
+
provider: cleanValue(session?.provider) || current.provider || null,
|
|
153
|
+
model: cleanValue(session?.model) || current.model || null,
|
|
154
|
+
// The sidecar is the live lease. Durable child sessions are
|
|
155
|
+
// intentionally detached/closed while their external owner runs.
|
|
156
|
+
status: 'running',
|
|
157
|
+
stage: 'running',
|
|
158
|
+
startedAt: session?.createdAt || current.startedAt || heartbeatAt,
|
|
159
|
+
turnStartedAt: current.turnStartedAt || null,
|
|
160
|
+
createdAt: session?.createdAt || current.createdAt || null,
|
|
161
|
+
updatedAt: heartbeatAt,
|
|
162
|
+
cwd: cleanValue(session?.cwd) || current.cwd || null,
|
|
163
|
+
clientHostPid: positiveNumber(session?.clientHostPid, 0)
|
|
164
|
+
|| current.clientHostPid || null,
|
|
165
|
+
taskId: cleanValue(session?.task_id || session?.taskId)
|
|
166
|
+
|| current.taskId || null,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const rows = [...bySessionId.values()];
|
|
170
|
+
return rows.sort((left, right) => {
|
|
171
|
+
const leftTime = Date.parse(String(left.startedAt || '')) || 0;
|
|
172
|
+
const rightTime = Date.parse(String(right.startedAt || '')) || 0;
|
|
173
|
+
return leftTime - rightTime || left.tag.localeCompare(right.tag);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
49
177
|
function cleanText(value, maximum = 240) {
|
|
50
178
|
return String(value || '')
|
|
51
179
|
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, ' ')
|
|
@@ -113,6 +241,7 @@ function normalizedRow(row, heartbeatAt = 0) {
|
|
|
113
241
|
permission: row.permission || null,
|
|
114
242
|
toolPermission: row.toolPermission || null,
|
|
115
243
|
messageCount: Math.max(0, Math.floor(Number(row.messageCount) || 0)),
|
|
244
|
+
title: cleanText(row.title, 100),
|
|
116
245
|
preview: cleanText(row.preview),
|
|
117
246
|
generation: typeof row.generation === 'number' ? row.generation : 0,
|
|
118
247
|
implicitBashSessionId: row.implicitBashSessionId || null,
|
|
@@ -120,6 +249,28 @@ function normalizedRow(row, heartbeatAt = 0) {
|
|
|
120
249
|
};
|
|
121
250
|
}
|
|
122
251
|
|
|
252
|
+
function leadRowsWithAgentHeartbeat(rows) {
|
|
253
|
+
const agentHeartbeatByOwner = new Map();
|
|
254
|
+
for (const row of rows) {
|
|
255
|
+
const heartbeatAt = positiveNumber(row?.heartbeatAt, 0);
|
|
256
|
+
const ownerSessionId = String(row?.ownerSessionId || '').trim();
|
|
257
|
+
const owner = String(row?.owner || '').trim().toLowerCase();
|
|
258
|
+
const agent = String(row?.agent || '').trim().toLowerCase();
|
|
259
|
+
if (!heartbeatAt || !ownerSessionId
|
|
260
|
+
|| (owner !== 'agent' && (!agent || agent === 'lead'))) continue;
|
|
261
|
+
agentHeartbeatByOwner.set(
|
|
262
|
+
ownerSessionId,
|
|
263
|
+
Math.max(agentHeartbeatByOwner.get(ownerSessionId) || 0, heartbeatAt),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return rows
|
|
267
|
+
.filter((row) => row && isLeadVisibleRow(row))
|
|
268
|
+
.map((row) => {
|
|
269
|
+
const agentHeartbeatAt = agentHeartbeatByOwner.get(row.id) || 0;
|
|
270
|
+
return agentHeartbeatAt > 0 ? { ...row, agentHeartbeatAt } : row;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
123
274
|
function rowFromSession(session, heartbeatAt = 0) {
|
|
124
275
|
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
125
276
|
const preview = messages
|
|
@@ -141,80 +292,353 @@ function rowFromSession(session, heartbeatAt = 0) {
|
|
|
141
292
|
function sessionHeartbeatMtimes() {
|
|
142
293
|
const directory = join(dataDir(), 'sessions');
|
|
143
294
|
const result = new Map();
|
|
144
|
-
if (
|
|
145
|
-
|
|
295
|
+
if (probePath(directory).state !== PROBE_PRESENT) return result;
|
|
296
|
+
let entries = [];
|
|
297
|
+
try { entries = readdirSync(directory); } catch { return result; }
|
|
298
|
+
for (const filename of entries) {
|
|
146
299
|
if (!filename.endsWith('.hb')) continue;
|
|
147
300
|
const id = filename.slice(0, -3);
|
|
148
301
|
if (!/^[A-Za-z0-9_-]+$/.test(id)) continue;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
302
|
+
// Liveness is additive: a sidecar that is absent OR unreadable simply
|
|
303
|
+
// contributes nothing, and can never remove a row.
|
|
304
|
+
const probe = probePath(join(directory, filename));
|
|
305
|
+
if (probe.state === PROBE_PRESENT && probe.mtimeMs > 0) result.set(id, probe.mtimeMs);
|
|
153
306
|
}
|
|
154
307
|
return result;
|
|
155
308
|
}
|
|
156
309
|
|
|
157
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Read-only scan of the authoritative session files.
|
|
312
|
+
* Returns `null` when the directory itself is NOT enumerable (unreadable
|
|
313
|
+
* stat / readdir): that is not "no sessions", and the caller must keep the
|
|
314
|
+
* authority it already has (the index rows) instead of publishing an empty
|
|
315
|
+
* catalog. `indexRowsById` carries those rows so a single UNREADABLE session
|
|
316
|
+
* file also retains its last known row instead of vanishing.
|
|
317
|
+
*/
|
|
318
|
+
function scanSessionFiles(heartbeatMtimes = sessionHeartbeatMtimes(), indexRowsById = new Map()) {
|
|
158
319
|
const directory = join(dataDir(), 'sessions');
|
|
159
|
-
|
|
320
|
+
const dirProbe = probePath(directory);
|
|
321
|
+
if (dirProbe.state === PROBE_ABSENT) return [];
|
|
322
|
+
if (dirProbe.state !== PROBE_PRESENT) return null;
|
|
323
|
+
let entries;
|
|
324
|
+
try { entries = readdirSync(directory); } catch { return null; }
|
|
160
325
|
const rows = [];
|
|
161
|
-
for (const filename of
|
|
326
|
+
for (const filename of entries) {
|
|
162
327
|
if (!filename.endsWith('.json')) continue;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
//
|
|
328
|
+
const storageId = filename.slice(0, -5);
|
|
329
|
+
const read = readTextFile(join(directory, filename));
|
|
330
|
+
// Provably gone: the row legitimately disappears with the file.
|
|
331
|
+
if (read.state === PROBE_ABSENT) continue;
|
|
332
|
+
if (read.state !== PROBE_PRESENT) {
|
|
333
|
+
// Exists and could not be read (EACCES/EIO): retain the last known
|
|
334
|
+
// row rather than let a transient IO error delete it from view.
|
|
335
|
+
const retained = indexRowsById.get(storageId);
|
|
336
|
+
if (retained) rows.push(retained);
|
|
337
|
+
continue;
|
|
170
338
|
}
|
|
339
|
+
// Same strict authority as the store: a duplicate/ambiguous top-level
|
|
340
|
+
// record or a foreign identity is not this file's session.
|
|
341
|
+
const record = readTopLevelLifecycleRecord(read.text);
|
|
342
|
+
if (isLifecycleUnreadable(record) || record.id !== storageId) continue;
|
|
343
|
+
const row = rowFromSession(record.doc, heartbeatMtimes.get(record.id) || 0);
|
|
344
|
+
if (row) rows.push(row);
|
|
171
345
|
}
|
|
172
|
-
return rows.sort((left, right) =>
|
|
346
|
+
return leadRowsWithAgentHeartbeat(rows).sort((left, right) =>
|
|
173
347
|
(right.lastUsedAt || right.updatedAt || 0) - (left.lastUsedAt || left.updatedAt || 0));
|
|
174
348
|
}
|
|
175
349
|
|
|
176
350
|
function sessionFilesAreNewerThan(indexPath) {
|
|
177
351
|
const directory = join(dataDir(), 'sessions');
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
352
|
+
if (probePath(directory).state !== PROBE_PRESENT) return false;
|
|
353
|
+
const indexProbe = probePath(indexPath);
|
|
354
|
+
// Only a PROVABLY absent index makes the files newer by definition. An
|
|
355
|
+
// index that exists but cannot be stat'ed (EACCES/EIO) is still the
|
|
356
|
+
// authority we failed to consult — claiming "files are newer" there would
|
|
357
|
+
// replace the whole catalog on the strength of an IO error.
|
|
358
|
+
if (indexProbe.state === PROBE_ABSENT) return true;
|
|
359
|
+
if (indexProbe.state !== PROBE_PRESENT) return false;
|
|
360
|
+
const indexMtime = indexProbe.mtimeMs || 0;
|
|
361
|
+
let entries;
|
|
362
|
+
try { entries = readdirSync(directory); } catch { return false; }
|
|
363
|
+
for (const filename of entries) {
|
|
364
|
+
if (!filename.endsWith('.json')) continue;
|
|
365
|
+
// Absent (raced unlink) and unreadable both prove nothing about
|
|
366
|
+
// freshness: only an observed newer mtime does.
|
|
367
|
+
const probe = probePath(join(directory, filename));
|
|
368
|
+
if (probe.state === PROBE_PRESENT && (probe.mtimeMs || 0) > indexMtime) return true;
|
|
191
369
|
}
|
|
192
370
|
return false;
|
|
193
371
|
}
|
|
194
372
|
|
|
195
373
|
export function listStoredSessionSummaries(options = {}) {
|
|
196
374
|
const heartbeatMtimes = sessionHeartbeatMtimes();
|
|
197
|
-
if (options.refreshFromStorage === true) return scanSessionFiles(heartbeatMtimes);
|
|
198
375
|
const indexPath = join(dataDir(), 'session-summaries.json');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
376
|
+
// The index is read on EVERY path (it is one small file): its rows are the
|
|
377
|
+
// authority that must be retained whenever storage cannot be enumerated or
|
|
378
|
+
// an individual session file cannot be read.
|
|
379
|
+
const indexRowsById = new Map();
|
|
380
|
+
let indexRows = null;
|
|
381
|
+
const indexRead = readTextFile(indexPath);
|
|
382
|
+
if (indexRead.state === PROBE_PRESENT) {
|
|
383
|
+
try {
|
|
384
|
+
const index = JSON.parse(indexRead.text);
|
|
385
|
+
if (Number(index?.version) === SESSION_SUMMARY_INDEX_VERSION) {
|
|
386
|
+
const normalizedRows = (Array.isArray(index.rows) ? index.rows : [])
|
|
387
|
+
.map((row) => normalizedRow(row, heartbeatMtimes.get(row?.id) || 0))
|
|
388
|
+
.filter(Boolean);
|
|
389
|
+
for (const row of normalizedRows) indexRowsById.set(row.id, row);
|
|
390
|
+
indexRows = leadRowsWithAgentHeartbeat(normalizedRows)
|
|
391
|
+
.sort((left, right) =>
|
|
392
|
+
(right.lastUsedAt || right.updatedAt || 0) - (left.lastUsedAt || left.updatedAt || 0));
|
|
393
|
+
}
|
|
394
|
+
} catch { /* malformed sidecar: the files below are the authority */ }
|
|
395
|
+
}
|
|
396
|
+
// An index that EXISTS but is unreadable (EACCES/EIO) is neither missing
|
|
397
|
+
// nor empty: the scan below still runs, and when IT cannot enumerate
|
|
398
|
+
// either, the catalog reports nothing rather than inventing an empty truth.
|
|
399
|
+
const scan = () => scanSessionFiles(heartbeatMtimes, indexRowsById);
|
|
400
|
+
if (options.refreshFromStorage === true) return scan() ?? indexRows ?? [];
|
|
401
|
+
if (indexRows) {
|
|
402
|
+
// A crashed/non-blocking sidecar writer can leave a valid but old,
|
|
403
|
+
// non-empty index indefinitely. Detect only newer session JSONs
|
|
404
|
+
// (heartbeat churn does not count), then recover read-only from the
|
|
405
|
+
// authoritative files without trying to rewrite the stuck index.
|
|
406
|
+
if (options.rebuildIfMissing !== false && sessionFilesAreNewerThan(indexPath)) {
|
|
407
|
+
return scan() ?? indexRows;
|
|
408
|
+
}
|
|
409
|
+
if (indexRows.length > 0 || options.rebuildIfMissing === false) return indexRows;
|
|
410
|
+
}
|
|
411
|
+
return options.rebuildIfMissing === false ? [] : (scan() ?? indexRows ?? []);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Read exactly one persisted session for a visible desktop pane. This never
|
|
415
|
+
* enumerates siblings. Normal reads stay independent of runtime ownership;
|
|
416
|
+
* interrupted turns conditionally use the same durable reconnect recovery as
|
|
417
|
+
* resumeSession before projecting the transcript. */
|
|
418
|
+
export async function readStoredSessionTranscript(id, options = {}) {
|
|
419
|
+
const sessionId = String(id || '').trim();
|
|
420
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) return null;
|
|
421
|
+
// Same strict authority as the store, and the same fail-closed rule: an
|
|
422
|
+
// absent, unreadable, ambiguous or foreign record yields no transcript.
|
|
423
|
+
const read = readTextFile(join(dataDir(), 'sessions', `${sessionId}.json`));
|
|
424
|
+
if (read.state !== PROBE_PRESENT) return null;
|
|
425
|
+
const record = readTopLevelLifecycleRecord(read.text);
|
|
426
|
+
if (isLifecycleUnreadable(record) || record.id !== sessionId) return null;
|
|
427
|
+
let session = record.doc;
|
|
428
|
+
const owner = cleanValue(session.owner).toLowerCase();
|
|
429
|
+
const agent = cleanValue(session.agent).toLowerCase();
|
|
430
|
+
const liveDetachedAgent = session.closed === true
|
|
431
|
+
&& (owner === 'agent' || (agent && agent !== 'lead'))
|
|
432
|
+
&& Boolean(cleanValue(session.ownerSessionId || session.parentSessionId));
|
|
433
|
+
// Only a PROVABLY absent checkpoint skips recovery: an unreadable probe
|
|
434
|
+
// must not silently downgrade an interrupted turn to a plain cold read.
|
|
435
|
+
if (probePath(join(dataDir(), 'turn-checkpoints', `${sessionId}.json`)).state !== PROBE_ABSENT) {
|
|
436
|
+
if (liveDetachedAgent) {
|
|
437
|
+
const {
|
|
438
|
+
projectTurnCheckpointMessages,
|
|
439
|
+
readTurnCheckpoint,
|
|
440
|
+
} = await import('./manager/turn-checkpoint.mjs');
|
|
441
|
+
const checkpoint = readTurnCheckpoint(sessionId);
|
|
442
|
+
if (checkpoint) {
|
|
443
|
+
session = {
|
|
444
|
+
...session,
|
|
445
|
+
messages: projectTurnCheckpointMessages(session, checkpoint),
|
|
446
|
+
};
|
|
213
447
|
}
|
|
214
|
-
|
|
448
|
+
} else {
|
|
449
|
+
const { recoverSessionAfterProcessRestart } = await import('./manager.mjs');
|
|
450
|
+
session = recoverSessionAfterProcessRestart(sessionId) || session;
|
|
215
451
|
}
|
|
452
|
+
}
|
|
453
|
+
const {
|
|
454
|
+
restoreTranscriptItems,
|
|
455
|
+
sessionContextSnapshotProjection,
|
|
456
|
+
} = await import(
|
|
457
|
+
'../../../../tui/engine/session-api-ext.mjs'
|
|
458
|
+
);
|
|
459
|
+
let preparedContextProjection = null;
|
|
460
|
+
try {
|
|
461
|
+
const [
|
|
462
|
+
{ prepareSessionProjection },
|
|
463
|
+
{ createContextStatus },
|
|
464
|
+
] = await Promise.all([
|
|
465
|
+
import('./manager.mjs'),
|
|
466
|
+
import('../../../../session-runtime/context-status.mjs'),
|
|
467
|
+
]);
|
|
468
|
+
const prepared = prepareSessionProjection(session, 'full');
|
|
469
|
+
if (prepared) session = prepared;
|
|
470
|
+
const { contextStatus } = createContextStatus({
|
|
471
|
+
getSession: () => session,
|
|
472
|
+
getRoute: () => ({
|
|
473
|
+
provider: session.provider || '',
|
|
474
|
+
model: session.model || '',
|
|
475
|
+
contextWindow: session.contextWindow || null,
|
|
476
|
+
}),
|
|
477
|
+
getCurrentCwd: () => session.cwd || '',
|
|
478
|
+
getMode: () => 'full',
|
|
479
|
+
});
|
|
480
|
+
preparedContextProjection = sessionContextSnapshotProjection(session, contextStatus());
|
|
216
481
|
} catch {
|
|
217
|
-
//
|
|
482
|
+
// Cold context projection is presentation-only. The transcript and
|
|
483
|
+
// legacy estimator below remain available if provider/tool prep fails.
|
|
218
484
|
}
|
|
219
|
-
|
|
485
|
+
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
486
|
+
const hasConversationActivity = messages.some((message) => message?.role === 'user');
|
|
487
|
+
let currentEstimatedContextTokens = 0;
|
|
488
|
+
if (hasConversationActivity) {
|
|
489
|
+
try {
|
|
490
|
+
const { estimateTranscriptContextUsage } = await import('./context-utils.mjs');
|
|
491
|
+
currentEstimatedContextTokens = estimateTranscriptContextUsage(
|
|
492
|
+
messages,
|
|
493
|
+
Array.isArray(session.tools) ? session.tools : [],
|
|
494
|
+
{ provider: session.provider },
|
|
495
|
+
);
|
|
496
|
+
} catch {
|
|
497
|
+
// Context metering is presentation-only. A cold transcript remains
|
|
498
|
+
// readable even if its optional estimator cannot be loaded.
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const contextWindow = positiveNumber(session.contextWindow);
|
|
502
|
+
const rawContextWindow = positiveNumber(session.rawContextWindow, contextWindow);
|
|
503
|
+
const displayContextWindow = positiveNumber(session.compactBoundaryTokens, contextWindow);
|
|
504
|
+
const rawAutoCompactTokenLimit = positiveNumber(session.autoCompactTokenLimit);
|
|
505
|
+
const autoCompactTokenLimit = rawAutoCompactTokenLimit
|
|
506
|
+
&& (!displayContextWindow || rawAutoCompactTokenLimit < displayContextWindow)
|
|
507
|
+
? rawAutoCompactTokenLimit
|
|
508
|
+
: 0;
|
|
509
|
+
const requestedLimit = Number(options.transcriptItemLimit);
|
|
510
|
+
return {
|
|
511
|
+
sessionId,
|
|
512
|
+
items: restoreTranscriptItems(messages, {
|
|
513
|
+
sessionId,
|
|
514
|
+
itemLimit: Number.isFinite(requestedLimit) && requestedLimit > 0
|
|
515
|
+
? requestedLimit
|
|
516
|
+
: Number.POSITIVE_INFINITY,
|
|
517
|
+
}),
|
|
518
|
+
provider: session.provider || '',
|
|
519
|
+
model: session.model || '',
|
|
520
|
+
effort: session.effort || '',
|
|
521
|
+
fast: session.fast === true,
|
|
522
|
+
cwd: session.cwd || '',
|
|
523
|
+
desktopSession: desktopSession(session.desktopSession, session.cwd),
|
|
524
|
+
workflow: session.workflow || null,
|
|
525
|
+
...(preparedContextProjection ? {
|
|
526
|
+
...preparedContextProjection,
|
|
527
|
+
preparedContextProjection: true,
|
|
528
|
+
} : {
|
|
529
|
+
stats: {
|
|
530
|
+
currentContextTokens: 0,
|
|
531
|
+
currentEstimatedContextTokens,
|
|
532
|
+
currentContextSource: currentEstimatedContextTokens > 0 ? 'estimated' : null,
|
|
533
|
+
},
|
|
534
|
+
contextWindow: contextWindow || null,
|
|
535
|
+
rawContextWindow: rawContextWindow || null,
|
|
536
|
+
displayContextWindow: displayContextWindow || null,
|
|
537
|
+
autoCompactTokenLimit: autoCompactTokenLimit || null,
|
|
538
|
+
}),
|
|
539
|
+
// Desktop must not ask its unrelated active engine to recover/peek an
|
|
540
|
+
// externally owned child: that runtime can only return the detached
|
|
541
|
+
// Task row and masks this checkpoint projection.
|
|
542
|
+
readOnlyDetachedAgent: liveDetachedAgent,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Lightweight desktop background viewer. It reuses the owner's existing
|
|
548
|
+
* live-share pipe without creating a second engine/runtime or claiming session
|
|
549
|
+
* ownership. The caller has already published the cold disk snapshot; the
|
|
550
|
+
* owner's first full frame atomically replaces it, then deltas keep the pane
|
|
551
|
+
* current while it remains visible.
|
|
552
|
+
*/
|
|
553
|
+
export async function createStoredSessionLiveViewer(id, options = {}) {
|
|
554
|
+
const sessionId = String(id || '').trim();
|
|
555
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)
|
|
556
|
+
|| typeof options.onSnapshot !== 'function') return null;
|
|
557
|
+
const initial = options.initialSnapshot && typeof options.initialSnapshot === 'object'
|
|
558
|
+
? options.initialSnapshot
|
|
559
|
+
: {};
|
|
560
|
+
let state = {
|
|
561
|
+
...initial,
|
|
562
|
+
sessionId,
|
|
563
|
+
items: Array.isArray(initial.items) ? initial.items : [],
|
|
564
|
+
queued: Array.isArray(initial.queued) ? initial.queued : [],
|
|
565
|
+
streamingTail: initial.streamingTail || null,
|
|
566
|
+
spinner: initial.spinner || null,
|
|
567
|
+
};
|
|
568
|
+
let disposed = false;
|
|
569
|
+
let publishQueued = false;
|
|
570
|
+
const publish = () => {
|
|
571
|
+
if (disposed || publishQueued) return;
|
|
572
|
+
publishQueued = true;
|
|
573
|
+
queueMicrotask(() => {
|
|
574
|
+
publishQueued = false;
|
|
575
|
+
if (disposed) return;
|
|
576
|
+
try { options.onSnapshot(state); } catch { /* host lane isolation */ }
|
|
577
|
+
});
|
|
578
|
+
};
|
|
579
|
+
const commit = (patch) => {
|
|
580
|
+
state = { ...state, ...patch, sessionId };
|
|
581
|
+
publish();
|
|
582
|
+
};
|
|
583
|
+
const viewerApply = {
|
|
584
|
+
getState: () => state,
|
|
585
|
+
set: (patch) => commit(patch && typeof patch === 'object' ? patch : {}),
|
|
586
|
+
replaceItems: (items, replaceOptions = {}) => commit({
|
|
587
|
+
items: Array.isArray(items) ? [...items] : [],
|
|
588
|
+
...(replaceOptions.preserveStreamingTail === true
|
|
589
|
+
? {}
|
|
590
|
+
: { streamingTail: null }),
|
|
591
|
+
}),
|
|
592
|
+
patchItem: (itemId, patch) => {
|
|
593
|
+
const index = state.items.findIndex((item) => item?.id === itemId);
|
|
594
|
+
if (index < 0) return false;
|
|
595
|
+
const items = [...state.items];
|
|
596
|
+
items[index] = { ...items[index], ...patch };
|
|
597
|
+
commit({ items });
|
|
598
|
+
return true;
|
|
599
|
+
},
|
|
600
|
+
appendItems: (items) => {
|
|
601
|
+
if (!Array.isArray(items) || items.length === 0) return;
|
|
602
|
+
commit({ items: [...state.items, ...items] });
|
|
603
|
+
},
|
|
604
|
+
updateStreamingTail: (itemId, patch, _unused = {}, updateOptions = {}) => {
|
|
605
|
+
const current = updateOptions.resetText !== true
|
|
606
|
+
&& state.streamingTail?.id === itemId
|
|
607
|
+
? state.streamingTail
|
|
608
|
+
: { id: itemId, text: '' };
|
|
609
|
+
commit({ streamingTail: { ...current, ...patch, id: itemId } });
|
|
610
|
+
},
|
|
611
|
+
clearStreamingTail: () => commit({ streamingTail: null }),
|
|
612
|
+
};
|
|
613
|
+
const { createLiveShare, liveSharePipePath } = await import(
|
|
614
|
+
'../../../../tui/engine/live-share.mjs'
|
|
615
|
+
);
|
|
616
|
+
const share = createLiveShare({
|
|
617
|
+
ownerSessionId: () => '',
|
|
618
|
+
viewerSessionId: () => disposed ? '' : sessionId,
|
|
619
|
+
socketPathFor: (targetId) => liveSharePipePath(
|
|
620
|
+
targetId,
|
|
621
|
+
join(dataDir(), 'sessions', `${targetId}.json`),
|
|
622
|
+
),
|
|
623
|
+
getPublishedState: () => state,
|
|
624
|
+
listeners: new Set(),
|
|
625
|
+
onRemoteSubmit: () => {},
|
|
626
|
+
onRemoteAbort: () => {},
|
|
627
|
+
onOwnerClosed: () => {
|
|
628
|
+
if (!disposed) options.onOwnerClosed?.();
|
|
629
|
+
},
|
|
630
|
+
viewerApply,
|
|
631
|
+
// A visible desktop pane may remain open before its external owner
|
|
632
|
+
// starts. Keep one bounded retry per pane instead of the TUI's
|
|
633
|
+
// latency-first 160ms retry ceiling.
|
|
634
|
+
viewerRetryMaxMs: 1_000,
|
|
635
|
+
});
|
|
636
|
+
share.ensure();
|
|
637
|
+
return {
|
|
638
|
+
dispose() {
|
|
639
|
+
if (disposed) return;
|
|
640
|
+
disposed = true;
|
|
641
|
+
share.dispose();
|
|
642
|
+
},
|
|
643
|
+
};
|
|
220
644
|
}
|