@huynle/hub 0.7.4 → 0.8.1
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/CHANGELOG.md +430 -1
- package/Dockerfile +7 -2
- package/README.md +230 -32
- package/package.json +5 -3
- package/packages/claude-code/package.json +1 -1
- package/packages/claude-code/src/daemon.ts +8 -1
- package/packages/claude-code/src/launcher.ts +29 -9
- package/packages/claude-code/src/paths.ts +3 -3
- package/packages/claude-code/src/profile.ts +173 -0
- package/packages/claude-code/src/sessions.ts +22 -4
- package/packages/cli/dist/hub-cli.js +6871 -3460
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
- package/packages/cli/src/artifacts-mcp.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +2 -2
- package/packages/cli/src/bootstrap/discovery/host.ts +15 -1
- package/packages/cli/src/bootstrap/discovery/probe.ts +1 -1
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +21 -9
- package/packages/cli/src/bootstrap/enrollment/policy.ts +138 -12
- package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
- package/packages/cli/src/cli.ts +96 -14
- package/packages/cli/src/format.ts +48 -10
- package/packages/cli/src/http.ts +2 -0
- package/packages/cli/src/intents.ts +23 -0
- package/packages/cli/src/launches.ts +9 -3
- package/packages/cli/src/login.ts +20 -6
- package/packages/cli/src/mcp.ts +385 -10
- package/packages/cli/src/messaging.ts +7 -3
- package/packages/cli/src/profiles-mcp.ts +301 -0
- package/packages/cli/src/setup/commands.ts +23 -4
- package/packages/cli/src/setup/runtime.ts +3 -2
- package/packages/cli/src/spoke-services.ts +1 -1
- package/packages/cli/src/team-intake-mcp.ts +158 -0
- package/packages/cli/src/teams-mcp.ts +460 -0
- package/packages/cli/src/watchlist.ts +135 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/paths.ts +3 -3
- package/packages/codex/src/profile.ts +163 -0
- package/packages/codex/src/sessions.ts +12 -0
- package/packages/codex/src/spoke.ts +19 -1
- package/packages/node/package.json +1 -1
- package/packages/node/src/config.ts +104 -2
- package/packages/node/src/opencode-warm.ts +344 -0
- package/packages/node/src/run.ts +28 -1
- package/packages/preset/package.json +1 -1
- package/packages/preset/src/profile.ts +34 -0
- package/packages/preset/src/spoke.ts +4 -0
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/artifacts.ts +142 -0
- package/packages/protocol/src/concierge.ts +366 -0
- package/packages/protocol/src/contracts.ts +104 -0
- package/packages/protocol/src/graph.ts +70 -0
- package/packages/protocol/src/hub-workspace.ts +201 -0
- package/packages/protocol/src/index.ts +175 -6
- package/packages/protocol/src/launch.ts +36 -0
- package/packages/protocol/src/profiles.ts +301 -0
- package/packages/protocol/src/teams.ts +204 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/address.ts +50 -2
- package/packages/sdk/src/artifact-jobs.ts +194 -0
- package/packages/sdk/src/artifacts.ts +187 -0
- package/packages/sdk/src/client.ts +226 -5
- package/packages/sdk/src/concierge.ts +88 -0
- package/packages/sdk/src/errors.ts +4 -1
- package/packages/sdk/src/index.ts +6 -0
- package/packages/sdk/src/team-intake.ts +122 -0
- package/packages/sdk/src/teams.ts +346 -0
- package/packages/sdk/src/types.ts +568 -1
- package/packages/sdk/src/wake.ts +20 -0
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/index.ts +1 -0
- package/packages/spoke-sdk/src/profile-apply.ts +245 -0
- package/src/central/actions.ts +7 -0
- package/src/central/agent/acting-routes.ts +2 -1
- package/src/central/agent/acting-switch.ts +2 -1
- package/src/central/agent/acting.ts +218 -10
- package/src/central/agent/app-tokens.ts +3 -2
- package/src/central/agent/audit.ts +18 -1
- package/src/central/agent/body.ts +163 -0
- package/src/central/agent/decisions.ts +125 -2
- package/src/central/agent/delivery.ts +194 -40
- package/src/central/agent/directory-routes.ts +37 -7
- package/src/central/agent/directory.ts +70 -14
- package/src/central/agent/errors.ts +7 -0
- package/src/central/agent/escalation.ts +10 -2
- package/src/central/agent/events-routes.ts +21 -4
- package/src/central/agent/events.ts +18 -4
- package/src/central/agent/form-check.ts +85 -0
- package/src/central/agent/grant-requests.ts +2 -1
- package/src/central/agent/grant-routes.ts +4 -3
- package/src/central/agent/grants.ts +29 -2
- package/src/central/agent/idempotency.ts +133 -0
- package/src/central/agent/intent-routes.ts +116 -0
- package/src/central/agent/intents.ts +505 -0
- package/src/central/agent/interaction-graph-routes.ts +212 -0
- package/src/central/agent/interaction-graph.ts +865 -0
- package/src/central/agent/launch-engine.ts +189 -32
- package/src/central/agent/launch-routes.ts +26 -10
- package/src/central/agent/launch-spec.ts +40 -3
- package/src/central/agent/launch-store.ts +55 -2
- package/src/central/agent/message-limits.ts +89 -11
- package/src/central/agent/message-routes.ts +10 -4
- package/src/central/agent/messages.ts +28 -2
- package/src/central/agent/names.ts +12 -2
- package/src/central/agent/placement.ts +93 -18
- package/src/central/agent/read-routes.ts +89 -19
- package/src/central/agent/routes.ts +178 -5
- package/src/central/agent/webhook-routes.ts +2 -1
- package/src/central/agent/webhooks.ts +17 -2
- package/src/central/agent-data.ts +4 -0
- package/src/central/artifacts/access.ts +44 -0
- package/src/central/artifacts/blob-store.ts +113 -0
- package/src/central/artifacts/blob-support.ts +98 -0
- package/src/central/artifacts/events.ts +27 -0
- package/src/central/artifacts/host-caps.ts +162 -0
- package/src/central/artifacts/host.ts +315 -0
- package/src/central/artifacts/idempotency.ts +109 -0
- package/src/central/artifacts/jobs-routes.ts +776 -0
- package/src/central/artifacts/jobs.ts +344 -0
- package/src/central/artifacts/maintenance.ts +103 -0
- package/src/central/artifacts/routes.ts +360 -0
- package/src/central/artifacts/store.ts +776 -0
- package/src/central/artifacts/workspaces.ts +31 -0
- package/src/central/backend/file-blobs.ts +377 -0
- package/src/central/backend/index.ts +12 -3
- package/src/central/backend/memory-blobs.ts +116 -0
- package/src/central/backend/postgres-blobs.ts +513 -0
- package/src/central/backend/postgres-migrate.ts +33 -0
- package/src/central/backend/postgres-state.ts +19 -0
- package/src/central/backend/types.ts +8 -0
- package/src/central/concierge/activity.ts +142 -0
- package/src/central/concierge/arm.ts +63 -0
- package/src/central/concierge/board-render.ts +190 -0
- package/src/central/concierge/board-results.ts +169 -0
- package/src/central/concierge/board-routes.ts +350 -0
- package/src/central/concierge/board.ts +249 -0
- package/src/central/concierge/context.ts +221 -0
- package/src/central/concierge/entitlements.ts +13 -0
- package/src/central/concierge/guard.ts +120 -0
- package/src/central/concierge/inbound.ts +86 -0
- package/src/central/concierge/index.ts +84 -0
- package/src/central/concierge/notes.ts +41 -0
- package/src/central/concierge/notifier.ts +986 -0
- package/src/central/concierge/prompt.ts +131 -0
- package/src/central/concierge/proposals.ts +268 -0
- package/src/central/concierge/push-remap.ts +49 -0
- package/src/central/concierge/routes.ts +122 -0
- package/src/central/concierge/schedule.ts +113 -0
- package/src/central/concierge/service.ts +243 -0
- package/src/central/concierge/session-routes.ts +125 -0
- package/src/central/concierge/settings.ts +217 -0
- package/src/central/concierge/supervisor.ts +767 -0
- package/src/central/concierge/tiers.ts +260 -0
- package/src/central/concierge/watchlist-routes.ts +279 -0
- package/src/central/concierge/watchlist.ts +273 -0
- package/src/central/config.ts +28 -6
- package/src/central/http.ts +660 -54
- package/src/central/main.ts +1 -1
- package/src/central/mcp-http.ts +10 -21
- package/src/central/profiles/apply.ts +333 -0
- package/src/central/profiles/content.ts +174 -0
- package/src/central/profiles/events.ts +40 -0
- package/src/central/profiles/idempotency.ts +14 -0
- package/src/central/profiles/import.ts +139 -0
- package/src/central/profiles/routes.ts +356 -0
- package/src/central/profiles/store.ts +597 -0
- package/src/central/profiles/usage-ports.ts +120 -0
- package/src/central/profiles/usage.ts +364 -0
- package/src/central/push.ts +7 -0
- package/src/central/registry.ts +69 -7
- package/src/central/store.ts +4 -0
- package/src/central/teams/collections.ts +34 -0
- package/src/central/teams/discovery.ts +346 -0
- package/src/central/teams/draft-edits.ts +46 -0
- package/src/central/teams/events.ts +46 -0
- package/src/central/teams/intake-routes.ts +100 -0
- package/src/central/teams/intake.ts +786 -0
- package/src/central/teams/policy.ts +231 -0
- package/src/central/teams/routes.ts +516 -0
- package/src/central/teams/seats.ts +272 -0
- package/src/central/teams/staffing.ts +910 -0
- package/src/central/teams/store.ts +667 -0
- package/src/central/teams/validation.ts +256 -0
- package/src/central/workspaces/board.ts +408 -0
- package/src/central/workspaces/checkpoints.ts +316 -0
- package/src/central/workspaces/coordinator.ts +841 -0
- package/src/central/workspaces/events.ts +85 -0
- package/src/central/workspaces/lifecycle.ts +93 -0
- package/src/central/workspaces/master.ts +490 -0
- package/src/central/workspaces/members.ts +632 -0
- package/src/central/workspaces/routes.ts +354 -0
- package/src/central/workspaces/sidechats-routes.ts +410 -0
- package/src/central/workspaces/sidechats.ts +482 -0
- package/src/central/workspaces/store.ts +571 -0
- package/src/central/workspaces/supervision.ts +355 -0
- package/src/index.ts +126 -23
- package/src/migrate.ts +109 -3
- package/src/spoke/concierge-agent.ts +33 -0
- package/src/spoke/deliver.ts +22 -4
- package/src/spoke/env-inject.ts +8 -1
- package/src/spoke/fork-revert.ts +142 -0
- package/src/spoke/hub-rpc.ts +41 -5
- package/src/spoke/hub-tool.ts +404 -25
- package/src/spoke/launchers.ts +6 -4
- package/src/spoke/listing.ts +65 -10
- package/src/spoke/localhub.ts +14 -4
- package/src/spoke/locations.ts +40 -0
- package/src/spoke/profile-apply.ts +298 -0
- package/src/spoke/session-context.ts +256 -0
- package/src/spoke/spoke.ts +330 -23
- package/src/spoke/timeline.ts +6 -1
- package/src/spoke/tracker.ts +23 -1
- package/src/spoke/who-teams.ts +81 -0
- package/src/tui-messaging.ts +32 -18
- package/web/dist/assets/ArtifactPage-mNzjlP_U.js +1 -0
- package/web/dist/assets/ArtifactPane-CPQcuX7c.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
- package/web/dist/assets/ArtifactWorkbench-NoFC5Sb2.js +6 -0
- package/web/dist/assets/Library-DYsFj_5c.css +1 -0
- package/web/dist/assets/Library-LXHsP5Fw.js +2 -0
- package/web/dist/assets/ProfilesDestination-DhVVcAep.js +1 -0
- package/web/dist/assets/TeamsPage-DBrO5hoL.js +1 -0
- package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
- package/web/dist/assets/Watchtower-DxaJYQp_.js +2 -0
- package/web/dist/assets/index-BabrNDB3.css +1 -0
- package/web/dist/assets/index-lIOfSYck.js +34 -0
- package/web/dist/assets/live-BDq0AABg.js +5 -0
- package/web/dist/assets/live-BUofsQpz.css +1 -0
- package/web/dist/assets/pane-layout-DWXfDU-1.js +1 -0
- package/web/dist/assets/plural-Bspvoqfq.js +1 -0
- package/web/dist/assets/renderer2d-DAr4IHM8.js +1 -0
- package/web/dist/assets/renderer3d-DWZj595u.js +5393 -0
- package/web/dist/assets/safe-area-JUb6OeI3.js +1 -0
- package/web/dist/assets/source-CDU04kYw.js +1 -0
- package/web/dist/assets/ui-Dh1-78Cl.js +3 -0
- package/web/dist/index.html +5 -2
- package/web/dist/sw.js +22 -2
- package/web/dist/assets/index-Dm8OmkaO.css +0 -1
- package/web/dist/assets/index-FUJbj_Du.js +0 -20
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// CON-04 session contexts (spoke feature "sessionContext"; contract packages/protocol/src/concierge.ts). The hub pushes
|
|
2
|
+
// a session's per-turn context; the spoke keeps it in this process's memory (like profile layers, profile-apply.ts):
|
|
3
|
+
//
|
|
4
|
+
// - parts: appended by the plugin's context hook after the profile parts, in key order (instructions, board,
|
|
5
|
+
// notes). The whole set replaces the previous one.
|
|
6
|
+
// - tools.allow: the exact tool names the session keeps; the hook deletes every other tool on every turn (unknown
|
|
7
|
+
// future tools included: fail closed). tools.deny: globs of tool names deleted as well.
|
|
8
|
+
// - Revisions: an older revision than the effective one is refused ("stale"); the same one is answered again.
|
|
9
|
+
// `clear` drops the parts. At most maxSessions contexts are kept (the least recently set dropped); lost on a
|
|
10
|
+
// process restart, after which the hub sends them again (the session reports no contextRevision).
|
|
11
|
+
//
|
|
12
|
+
// LOCKED sessions (the Concierge's, R1 of the CON-04 verify): a context with lockAgent locks its session for good.
|
|
13
|
+
// A locked session, WHATEVER agent it runs on and with or without a context (cleared, or not re-sent yet after a
|
|
14
|
+
// restart), keeps only CONCIERGE_TOOLS (intersected with its context's allow-list), never gets conversation mode,
|
|
15
|
+
// voice style or orchestrator parts. Locks survive `clear`, the context bound and a restart: they are persisted in
|
|
16
|
+
// the plugin's state dir (lockFile), so a restarted OpenCode locks the session before the hub reconnects. The agent
|
|
17
|
+
// a locked session was last seen on is reported to the hub (SessionInfo.agent), which suspends a Concierge found on
|
|
18
|
+
// another agent.
|
|
19
|
+
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs"
|
|
20
|
+
import path from "node:path"
|
|
21
|
+
import { CONCIERGE_TOOLS, SESSION_CONTEXT_KEYS, type SessionContextRequest, type SessionContextResult, toolDenied } from "../../packages/protocol/src/concierge.ts"
|
|
22
|
+
|
|
23
|
+
interface Context {
|
|
24
|
+
readonly revision: number
|
|
25
|
+
readonly parts: readonly string[]
|
|
26
|
+
readonly deny: readonly string[]
|
|
27
|
+
readonly allow?: ReadonlySet<string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Locked sessions kept at most (oldest dropped; the hub keeps 20 Concierge sessions per user). */
|
|
31
|
+
const LOCKS_MAX = 1000
|
|
32
|
+
const LOCKED_TOOLS: ReadonlySet<string> = new Set(CONCIERGE_TOOLS)
|
|
33
|
+
/** OpenCode's internal agents (title generation, compaction, summaries): never the session's own agent. */
|
|
34
|
+
/** A file version: atomic writes rename a new inode in, so inode + mtime + size changes with every write. */
|
|
35
|
+
const stampOf = (file: string) => {
|
|
36
|
+
const st = statSync(file)
|
|
37
|
+
return `${st.ino}:${st.mtimeMs}:${st.size}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const INTERNAL_AGENTS: ReadonlySet<string> = new Set(["title", "compaction", "summary"])
|
|
41
|
+
|
|
42
|
+
export interface SessionContextsOptions {
|
|
43
|
+
readonly maxSessions?: number
|
|
44
|
+
/** Where locked session IDs persist (the plugin's state dir); in memory only without it. */
|
|
45
|
+
readonly lockFile?: string
|
|
46
|
+
/** (FX-07) How long a write waits for the other processes' exclusive lock before it writes anyway. */
|
|
47
|
+
readonly lockWaitMs?: number
|
|
48
|
+
/**
|
|
49
|
+
* Other lock files read (never written): the other default state dir places (FX-08: a lock written before an
|
|
50
|
+
* upgrade moved the state dir still locks the session; fail closed).
|
|
51
|
+
*/
|
|
52
|
+
readonly readLockFiles?: readonly string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** (FX-07) A `<lockFile>.lock` older than this belongs to a process that died while writing: it is broken. */
|
|
56
|
+
const WRITE_LOCK_STALE_MS = 10_000
|
|
57
|
+
const WRITE_LOCK_WAIT_MS = 2_000
|
|
58
|
+
const pause = (ms: number) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* (FX-07) Runs `write` holding `<file>.lock` (created exclusively), so the read-merge-write of every OpenCode process
|
|
62
|
+
* on the machine is serialized and none drops another's locks. A stale lock (its holder died) is broken; after
|
|
63
|
+
* `waitMs` the write runs anyway (the lock still holds in this process, and the write merges what it reads).
|
|
64
|
+
*/
|
|
65
|
+
function underWriteLock(file: string, waitMs: number, write: () => void): void {
|
|
66
|
+
const lock = `${file}.lock`
|
|
67
|
+
const deadline = Date.now() + waitMs
|
|
68
|
+
let held = false
|
|
69
|
+
for (;;) {
|
|
70
|
+
try {
|
|
71
|
+
closeSync(openSync(lock, "wx", 0o600))
|
|
72
|
+
held = true
|
|
73
|
+
break
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
if ((cause as NodeJS.ErrnoException).code !== "EEXIST") break
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
if (Date.now() - statSync(lock).mtimeMs > WRITE_LOCK_STALE_MS) {
|
|
79
|
+
unlinkSync(lock)
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
continue // released meanwhile
|
|
84
|
+
}
|
|
85
|
+
if (Date.now() >= deadline) {
|
|
86
|
+
console.error("[hub] the Concierge session lock file stayed busy: writing without it")
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
pause(2 + Math.floor(Math.random() * 5))
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
write()
|
|
93
|
+
} finally {
|
|
94
|
+
if (held) {
|
|
95
|
+
try {
|
|
96
|
+
unlinkSync(lock)
|
|
97
|
+
} catch {
|
|
98
|
+
// already gone
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class SessionContexts {
|
|
105
|
+
private readonly contexts = new Map<string, Context>()
|
|
106
|
+
/** The last cleared revision per session, so a stale set cannot resurrect a cleared context. */
|
|
107
|
+
private readonly cleared = new Map<string, number>()
|
|
108
|
+
private readonly locks: Set<string>
|
|
109
|
+
/** The lock file's mtime when last read (another process may add locks: re-read on change). */
|
|
110
|
+
private lockStamp = ""
|
|
111
|
+
private readonly agents = new Map<string, string>()
|
|
112
|
+
private readonly maxSessions: number
|
|
113
|
+
|
|
114
|
+
constructor(private readonly options: SessionContextsOptions = {}) {
|
|
115
|
+
this.maxSessions = options.maxSessions ?? 200
|
|
116
|
+
this.locks = new Set(this.load())
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Every lock file read: the written one first, then the read-only ones. */
|
|
120
|
+
private files(): string[] {
|
|
121
|
+
return [...(this.options.lockFile ? [this.options.lockFile] : []), ...(this.options.readLockFiles ?? [])]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Adds the locks another process wrote since the last read (locks are never removed). */
|
|
125
|
+
private refresh(): void {
|
|
126
|
+
const files = this.files()
|
|
127
|
+
if (!files.length) return
|
|
128
|
+
const stamp = files
|
|
129
|
+
.map((file) => {
|
|
130
|
+
try {
|
|
131
|
+
return stampOf(file)
|
|
132
|
+
} catch {
|
|
133
|
+
return "-"
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.join("|")
|
|
137
|
+
if (stamp === this.lockStamp) return
|
|
138
|
+
this.lockStamp = stamp
|
|
139
|
+
for (const id of this.load()) this.locks.add(id)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The union of the lock files' sessions (a missing or corrupt file adds none). */
|
|
143
|
+
private load(): string[] {
|
|
144
|
+
return this.files().flatMap((file) => {
|
|
145
|
+
try {
|
|
146
|
+
const raw = JSON.parse(readFileSync(file, "utf8")) as { v?: unknown; sessions?: unknown }
|
|
147
|
+
return raw.v === 1 && Array.isArray(raw.sessions) ? raw.sessions.filter((id): id is string => typeof id === "string" && id.length > 0 && id.length <= 200) : []
|
|
148
|
+
} catch {
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private lock(sessionID: string): void {
|
|
155
|
+
this.refresh()
|
|
156
|
+
if (this.locks.has(sessionID)) return
|
|
157
|
+
this.locks.add(sessionID)
|
|
158
|
+
const file = this.options.lockFile
|
|
159
|
+
if (!file) return this.boundLocks()
|
|
160
|
+
try {
|
|
161
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
|
|
162
|
+
underWriteLock(file, this.options.lockWaitMs ?? WRITE_LOCK_WAIT_MS, () => {
|
|
163
|
+
// Merge with what other processes wrote (read under the write lock): never drop their locks.
|
|
164
|
+
for (const id of this.load()) this.locks.add(id)
|
|
165
|
+
// The newest lock last, so the bound never drops it.
|
|
166
|
+
this.locks.delete(sessionID)
|
|
167
|
+
this.locks.add(sessionID)
|
|
168
|
+
this.boundLocks()
|
|
169
|
+
const tmp = `${file}.${process.pid}.tmp`
|
|
170
|
+
writeFileSync(tmp, `${JSON.stringify({ v: 1, sessions: [...this.locks] })}\n`, { mode: 0o600 })
|
|
171
|
+
renameSync(tmp, file)
|
|
172
|
+
})
|
|
173
|
+
this.lockStamp = ""
|
|
174
|
+
this.refresh()
|
|
175
|
+
} catch (cause) {
|
|
176
|
+
// The lock still holds in this process; the hub re-sends the context after a restart.
|
|
177
|
+
console.error(`[hub] could not persist the Concierge session lock: ${(cause as Error).message}`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Applies a hub request (validated by the protocol parser); throws "stale: …" for an older revision. */
|
|
182
|
+
handle(req: SessionContextRequest): SessionContextResult {
|
|
183
|
+
const effective = this.contexts.get(req.sessionID)?.revision ?? this.cleared.get(req.sessionID)
|
|
184
|
+
if (effective !== undefined && req.revision < effective) throw new Error(`stale: revision ${req.revision} is older than the effective ${effective}`)
|
|
185
|
+
this.contexts.delete(req.sessionID)
|
|
186
|
+
if (req.clear) {
|
|
187
|
+
this.cleared.set(req.sessionID, req.revision)
|
|
188
|
+
this.bound(this.cleared)
|
|
189
|
+
return { revision: req.revision }
|
|
190
|
+
}
|
|
191
|
+
this.cleared.delete(req.sessionID)
|
|
192
|
+
if (req.lockAgent) this.lock(req.sessionID)
|
|
193
|
+
const byKey = new Map(req.parts.map((p) => [p.key, p.text]))
|
|
194
|
+
const parts = SESSION_CONTEXT_KEYS.flatMap((key) => (byKey.has(key) ? [byKey.get(key)!] : []))
|
|
195
|
+
const allow = req.tools?.allow
|
|
196
|
+
this.contexts.set(req.sessionID, { revision: req.revision, parts, deny: [...(req.tools?.deny ?? [])], ...(allow ? { allow: new Set(allow) } : {}) })
|
|
197
|
+
this.bound(this.contexts)
|
|
198
|
+
return { revision: req.revision }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** At most LOCKS_MAX locks (the oldest dropped). */
|
|
202
|
+
private boundLocks(): void {
|
|
203
|
+
while (this.locks.size > LOCKS_MAX) this.locks.delete(this.locks.values().next().value as string)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private bound(map: Map<string, unknown>): void {
|
|
207
|
+
while (map.size > this.maxSessions) map.delete(map.keys().next().value as string)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The session's parts for its system prompt (none without a context). */
|
|
211
|
+
systemParts(sessionID: string): string[] {
|
|
212
|
+
return [...(this.contexts.get(sessionID)?.parts ?? [])]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** A Concierge session: only CONCIERGE_TOOLS, its agent and model kept, no conversation mode. */
|
|
216
|
+
locked(sessionID: string): boolean {
|
|
217
|
+
if (!this.locks.has(sessionID)) this.refresh()
|
|
218
|
+
return this.locks.has(sessionID)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
lockedSessions(): string[] {
|
|
222
|
+
return [...this.locks]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Whether the hub took this tool from the session (a locked session's non-Concierge tool, off its allow-list, or denied). */
|
|
226
|
+
denied(sessionID: string, tool: string): boolean {
|
|
227
|
+
if (this.locks.has(sessionID) && !LOCKED_TOOLS.has(tool)) return true
|
|
228
|
+
const context = this.contexts.get(sessionID)
|
|
229
|
+
if (!context) return false
|
|
230
|
+
return (context.allow !== undefined && !context.allow.has(tool)) || toolDenied(tool, context.deny)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Deletes the tools the hub took from the session from a context event's tools. */
|
|
234
|
+
filterTools(sessionID: string, tools: Record<string, unknown>): void {
|
|
235
|
+
if (!this.contexts.has(sessionID) && !this.locked(sessionID)) return
|
|
236
|
+
for (const name of Object.keys(tools)) if (this.denied(sessionID, name)) delete tools[name]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Records the agent a locked session runs on; true when it changed (a new report is due). */
|
|
240
|
+
seenAgent(sessionID: string, agent: string | undefined): boolean {
|
|
241
|
+
if (!this.locks.has(sessionID) || !agent || INTERNAL_AGENTS.has(agent) || this.agents.get(sessionID) === agent) return false
|
|
242
|
+
this.agents.set(sessionID, agent)
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* What the sessions frame says about the session (locked ones only): locked (FX-07: from the very first frame, even
|
|
248
|
+
* after a restart, before its agent is read or the hub re-sends a context), its agent and context revision.
|
|
249
|
+
*/
|
|
250
|
+
report(sessionID: string): { locked?: true; agent?: string; contextRevision?: number } {
|
|
251
|
+
if (!this.locks.has(sessionID)) return {}
|
|
252
|
+
const agent = this.agents.get(sessionID)
|
|
253
|
+
const revision = this.contexts.get(sessionID)?.revision
|
|
254
|
+
return { locked: true, ...(agent ? { agent } : {}), ...(revision !== undefined ? { contextRevision: revision } : {}) }
|
|
255
|
+
}
|
|
256
|
+
}
|