handmux 0.23.0 → 0.25.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/README.md +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
const STATES = new Set(['working', 'waiting', 'done', 'error']);
|
|
2
|
+
const AVAILABILITIES = new Set(['ready', 'degraded', 'unavailable']);
|
|
3
|
+
const EVENT_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/;
|
|
4
|
+
export class PiInboxBridgeError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'PiInboxBridgeError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function isControlOperation(operation) {
|
|
11
|
+
return operation.kind === 'superseded' || operation.kind === 'gap';
|
|
12
|
+
}
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function optionalText(value, max, nullable = false) {
|
|
17
|
+
return value === undefined || (nullable && value === null)
|
|
18
|
+
|| (typeof value === 'string' && value.length > 0 && value.length <= max);
|
|
19
|
+
}
|
|
20
|
+
function optionalEventId(value) {
|
|
21
|
+
return value === undefined || (typeof value === 'string' && EVENT_ID_RE.test(value));
|
|
22
|
+
}
|
|
23
|
+
function optionalTime(value) {
|
|
24
|
+
return value === undefined || (typeof value === 'number' && Number.isFinite(value));
|
|
25
|
+
}
|
|
26
|
+
function copyRun(ref) {
|
|
27
|
+
return {
|
|
28
|
+
agentId: ref.agentId,
|
|
29
|
+
paneId: ref.paneId,
|
|
30
|
+
runId: ref.runId,
|
|
31
|
+
...(ref.sessionId === undefined ? {} : { sessionId: ref.sessionId }),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function sameRun(first, second) {
|
|
35
|
+
return first.agentId === second.agentId
|
|
36
|
+
&& first.paneId === second.paneId
|
|
37
|
+
&& first.runId === second.runId
|
|
38
|
+
&& first.sessionId === second.sessionId;
|
|
39
|
+
}
|
|
40
|
+
function parseItem(value) {
|
|
41
|
+
if (!isRecord(value) || typeof value.state !== 'string' || !STATES.has(value.state)
|
|
42
|
+
|| !optionalText(value.message, 4096) || !optionalText(value.reason, 1024)
|
|
43
|
+
|| !optionalText(value.correlationId, 256) || !optionalEventId(value.eventId)
|
|
44
|
+
|| !optionalTime(value.sourceOccurredAt))
|
|
45
|
+
return null;
|
|
46
|
+
return {
|
|
47
|
+
state: value.state,
|
|
48
|
+
...(value.message === undefined ? {} : { message: value.message }),
|
|
49
|
+
...(value.reason === undefined ? {} : { reason: value.reason }),
|
|
50
|
+
...(value.correlationId === undefined ? {} : { correlationId: value.correlationId }),
|
|
51
|
+
...(value.eventId === undefined ? {} : { eventId: value.eventId }),
|
|
52
|
+
...(value.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: value.sourceOccurredAt }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function parseSnapshot(value, availability, run, sourceId, sequence, label = 'Pi') {
|
|
56
|
+
if (availability === 'unavailable') {
|
|
57
|
+
return {
|
|
58
|
+
current: undefined,
|
|
59
|
+
result: { availability: 'unavailable', message: `${label} Inbox snapshot is unavailable` },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!isRecord(value) || typeof value.availability !== 'string'
|
|
63
|
+
|| !AVAILABILITIES.has(value.availability)
|
|
64
|
+
|| !optionalText(value.message, 1024)) {
|
|
65
|
+
throw new PiInboxBridgeError(`${label} Inbox Bridge returned an invalid snapshot`);
|
|
66
|
+
}
|
|
67
|
+
const snapshotAvailability = value.availability;
|
|
68
|
+
if (snapshotAvailability === 'unavailable' && value.current !== undefined) {
|
|
69
|
+
throw new PiInboxBridgeError(`Unavailable ${label} Inbox snapshot cannot contain current state`);
|
|
70
|
+
}
|
|
71
|
+
const current = value.current === undefined ? undefined : parseItem(value.current);
|
|
72
|
+
if (value.current !== undefined && !current) {
|
|
73
|
+
throw new PiInboxBridgeError(`${label} Inbox Bridge snapshot contains an invalid current state`);
|
|
74
|
+
}
|
|
75
|
+
const baseline = current ? [{
|
|
76
|
+
run: copyRun(run),
|
|
77
|
+
source: { sourceId, cursor: `bridge:${sequence}` },
|
|
78
|
+
...current,
|
|
79
|
+
}] : [];
|
|
80
|
+
return {
|
|
81
|
+
current: current ?? undefined,
|
|
82
|
+
result: {
|
|
83
|
+
availability: snapshotAvailability,
|
|
84
|
+
...(snapshotAvailability === 'unavailable' ? {} : { snapshot: baseline }),
|
|
85
|
+
...(value.message === undefined ? {} : { message: value.message }),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function parseOperation(envelope, sourceId) {
|
|
90
|
+
const value = envelope.payload;
|
|
91
|
+
if (!isRecord(value)
|
|
92
|
+
|| (value.kind !== 'set' && value.kind !== 'clear'
|
|
93
|
+
&& value.kind !== 'superseded' && value.kind !== 'gap')
|
|
94
|
+
|| !optionalText(value.correlationId, 256) || !optionalEventId(value.eventId)
|
|
95
|
+
|| !optionalTime(value.sourceOccurredAt)
|
|
96
|
+
|| (envelope.eventId !== undefined && value.eventId !== undefined
|
|
97
|
+
&& envelope.eventId !== value.eventId))
|
|
98
|
+
return null;
|
|
99
|
+
const eventId = typeof value.eventId === 'string'
|
|
100
|
+
? value.eventId : envelope.eventId;
|
|
101
|
+
const base = {
|
|
102
|
+
source: { sourceId, cursor: `bridge:${envelope.sequence}` },
|
|
103
|
+
...(value.correlationId === undefined ? {} : { correlationId: value.correlationId }),
|
|
104
|
+
...(eventId === undefined ? {} : { eventId }),
|
|
105
|
+
...(value.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: value.sourceOccurredAt }),
|
|
106
|
+
};
|
|
107
|
+
if (value.kind === 'superseded' || value.kind === 'gap') {
|
|
108
|
+
if (!eventId)
|
|
109
|
+
return null;
|
|
110
|
+
return { kind: value.kind, ...base, eventId };
|
|
111
|
+
}
|
|
112
|
+
if (value.kind === 'clear')
|
|
113
|
+
return { kind: 'clear', ...base };
|
|
114
|
+
if (typeof value.state !== 'string' || !STATES.has(value.state)
|
|
115
|
+
|| !optionalText(value.message, 4096, true) || !optionalText(value.reason, 1024, true))
|
|
116
|
+
return null;
|
|
117
|
+
const state = value.state;
|
|
118
|
+
if ((state === 'waiting' || state === 'done' || state === 'error') && eventId === undefined)
|
|
119
|
+
return null;
|
|
120
|
+
return {
|
|
121
|
+
kind: 'set',
|
|
122
|
+
state,
|
|
123
|
+
...base,
|
|
124
|
+
...(value.message === undefined ? {} : { message: value.message }),
|
|
125
|
+
...(value.reason === undefined ? {} : { reason: value.reason }),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Owns only Bridge source ordering and semantic arbitration. Bridge sequence remains a source cursor;
|
|
129
|
+
// Inbox Core allocates the public run-scoped sequence and authoritative acceptedAt.
|
|
130
|
+
export class BridgeInboxCoordinator {
|
|
131
|
+
#host;
|
|
132
|
+
#projector;
|
|
133
|
+
#agentId;
|
|
134
|
+
#sourceId;
|
|
135
|
+
#label;
|
|
136
|
+
#bindings = new Map();
|
|
137
|
+
#stopConsumer;
|
|
138
|
+
#restoreTail = Promise.resolve();
|
|
139
|
+
#closed = false;
|
|
140
|
+
constructor({ host, projector, agentId = 'pi', sourceId = 'pi.bridge.inbox', label = 'Pi', }) {
|
|
141
|
+
this.#host = host;
|
|
142
|
+
this.#projector = projector;
|
|
143
|
+
this.#agentId = agentId;
|
|
144
|
+
this.#sourceId = sourceId;
|
|
145
|
+
this.#label = label;
|
|
146
|
+
}
|
|
147
|
+
start() {
|
|
148
|
+
if (this.#closed)
|
|
149
|
+
throw new PiInboxBridgeError(`${this.#label} Inbox Bridge coordinator is closed`);
|
|
150
|
+
if (this.#stopConsumer)
|
|
151
|
+
return;
|
|
152
|
+
this.#stopConsumer = this.#host.consumeDurableReplays('inbox', (replay) => this.#consumeDurable(replay));
|
|
153
|
+
}
|
|
154
|
+
async bind(lease) {
|
|
155
|
+
if (this.#closed || lease.signal.aborted || lease.ref.agentId !== this.#agentId) {
|
|
156
|
+
throw new PiInboxBridgeError(`${this.#label} Inbox Bridge requires a live run lease`);
|
|
157
|
+
}
|
|
158
|
+
this.start();
|
|
159
|
+
const existing = this.#bindings.get(lease.ref.runId);
|
|
160
|
+
if (existing) {
|
|
161
|
+
if (existing.lease !== lease || existing.phase === 'closed') {
|
|
162
|
+
throw new PiInboxBridgeError(`${this.#label} Inbox Bridge runId is already bound to another lease`);
|
|
163
|
+
}
|
|
164
|
+
return existing.exposed;
|
|
165
|
+
}
|
|
166
|
+
const binding = {};
|
|
167
|
+
const exposed = Object.freeze({
|
|
168
|
+
run: copyRun(lease.ref),
|
|
169
|
+
close: () => this.#closeBinding(binding),
|
|
170
|
+
});
|
|
171
|
+
Object.assign(binding, {
|
|
172
|
+
lease,
|
|
173
|
+
projector: this.#projector.forRun(lease),
|
|
174
|
+
exposed,
|
|
175
|
+
phase: 'opening',
|
|
176
|
+
baselineSequence: 0,
|
|
177
|
+
baseline: undefined,
|
|
178
|
+
baselineReady: false,
|
|
179
|
+
handle: undefined,
|
|
180
|
+
buffered: [],
|
|
181
|
+
tail: Promise.resolve(),
|
|
182
|
+
abort: new AbortController(),
|
|
183
|
+
detachAbort: () => { },
|
|
184
|
+
});
|
|
185
|
+
const onAbort = () => this.#closeBinding(binding);
|
|
186
|
+
lease.signal.addEventListener('abort', onAbort, { once: true });
|
|
187
|
+
binding.detachAbort = () => lease.signal.removeEventListener('abort', onAbort);
|
|
188
|
+
this.#bindings.set(lease.ref.runId, binding);
|
|
189
|
+
try {
|
|
190
|
+
const handle = await this.#host.openChannel(lease, 'inbox', async (event) => {
|
|
191
|
+
if (binding.phase === 'closed')
|
|
192
|
+
return;
|
|
193
|
+
if (binding.phase !== 'live') {
|
|
194
|
+
binding.buffered.push(event);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
await this.#enqueue(binding, () => this.#processHostEvent(binding, event));
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
this.#closeBinding(binding);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
binding.handle = handle;
|
|
206
|
+
binding.baselineSequence = handle.streamSequence;
|
|
207
|
+
binding.baseline = parseSnapshot(handle.snapshot, handle.snapshotAvailability, lease.ref, this.#sourceId, handle.streamSequence, this.#label);
|
|
208
|
+
binding.phase = 'prebaseline';
|
|
209
|
+
await this.#host.drainDurable(lease.ref, 'inbox', handle.streamSequence, {
|
|
210
|
+
signal: binding.abort.signal,
|
|
211
|
+
});
|
|
212
|
+
if (lease.signal.aborted || this.#bindings.get(lease.ref.runId) !== binding) {
|
|
213
|
+
throw new PiInboxBridgeError(`${this.#label} Inbox Bridge binding closed during restore`);
|
|
214
|
+
}
|
|
215
|
+
binding.baselineReady = true;
|
|
216
|
+
await this.#restoreBindings();
|
|
217
|
+
const buffered = binding.buffered.splice(0);
|
|
218
|
+
let initialization = binding.tail;
|
|
219
|
+
for (const event of buffered) {
|
|
220
|
+
initialization = initialization.then(() => this.#processHostEvent(binding, event));
|
|
221
|
+
}
|
|
222
|
+
binding.tail = initialization.catch(() => { });
|
|
223
|
+
binding.phase = 'live';
|
|
224
|
+
await initialization;
|
|
225
|
+
return exposed;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
this.#closeBinding(binding);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
close() {
|
|
233
|
+
if (this.#closed)
|
|
234
|
+
return;
|
|
235
|
+
this.#closed = true;
|
|
236
|
+
this.#stopConsumer?.();
|
|
237
|
+
this.#stopConsumer = undefined;
|
|
238
|
+
for (const binding of [...this.#bindings.values()])
|
|
239
|
+
this.#closeBinding(binding);
|
|
240
|
+
}
|
|
241
|
+
async #consumeDurable(replay) {
|
|
242
|
+
const operation = parseOperation(replay.event, this.#sourceId);
|
|
243
|
+
if (!operation)
|
|
244
|
+
return 'invalid';
|
|
245
|
+
if (isControlOperation(operation)) {
|
|
246
|
+
return replay.runStatus === 'current' ? 'accepted' : 'invalid';
|
|
247
|
+
}
|
|
248
|
+
if (replay.runStatus === 'revoked') {
|
|
249
|
+
if (operation.kind !== 'set' || (operation.state !== 'done' && operation.state !== 'error')
|
|
250
|
+
|| !operation.eventId)
|
|
251
|
+
return 'invalid';
|
|
252
|
+
const result = await this.#projector.submitTerminalReplay({
|
|
253
|
+
run: replay.run,
|
|
254
|
+
source: operation.source,
|
|
255
|
+
state: operation.state,
|
|
256
|
+
eventId: operation.eventId,
|
|
257
|
+
...(typeof operation.message === 'string' ? { message: operation.message } : {}),
|
|
258
|
+
...(typeof operation.reason === 'string' ? { reason: operation.reason } : {}),
|
|
259
|
+
...(operation.correlationId === undefined ? {} : { correlationId: operation.correlationId }),
|
|
260
|
+
...(operation.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: operation.sourceOccurredAt }),
|
|
261
|
+
});
|
|
262
|
+
return this.#replayResult(result.accepted, result.reason);
|
|
263
|
+
}
|
|
264
|
+
const binding = this.#bindings.get(replay.run.runId);
|
|
265
|
+
if (!binding || !sameRun(binding.lease.ref, replay.run) || binding.phase === 'opening'
|
|
266
|
+
|| binding.phase === 'closed')
|
|
267
|
+
return 'retry';
|
|
268
|
+
if (binding.phase === 'prebaseline') {
|
|
269
|
+
if (replay.event.sequence > binding.baselineSequence)
|
|
270
|
+
return 'retry';
|
|
271
|
+
if (operation.kind === 'set' && operation.state === 'waiting'
|
|
272
|
+
&& binding.baseline?.result.availability === 'ready') {
|
|
273
|
+
const current = binding.baseline.current;
|
|
274
|
+
if (!current || current.state !== 'waiting' || current.eventId !== operation.eventId)
|
|
275
|
+
return 'invalid';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const result = await binding.projector.submit(operation);
|
|
279
|
+
return this.#replayResult(result.accepted, result.reason);
|
|
280
|
+
}
|
|
281
|
+
#replayResult(accepted, reason) {
|
|
282
|
+
if (accepted)
|
|
283
|
+
return 'accepted';
|
|
284
|
+
if (reason === 'persistence_failed' || reason === 'stale_lease')
|
|
285
|
+
return 'retry';
|
|
286
|
+
return 'invalid';
|
|
287
|
+
}
|
|
288
|
+
#enqueue(binding, operation) {
|
|
289
|
+
const pending = binding.tail.then(operation);
|
|
290
|
+
binding.tail = pending.catch(() => { });
|
|
291
|
+
return pending;
|
|
292
|
+
}
|
|
293
|
+
#restore(result) {
|
|
294
|
+
const pending = this.#restoreTail.then(() => this.#projector.restore(result()).then(() => undefined));
|
|
295
|
+
this.#restoreTail = pending.catch(() => { });
|
|
296
|
+
return pending;
|
|
297
|
+
}
|
|
298
|
+
#restoreBindings() {
|
|
299
|
+
return this.#restore(() => {
|
|
300
|
+
const bindings = [...this.#bindings.values()].filter((binding) => binding.phase !== 'closed');
|
|
301
|
+
const available = bindings.filter((binding) => (binding.baselineReady && binding.baseline?.result.availability !== 'unavailable'));
|
|
302
|
+
const pending = bindings.filter((binding) => !binding.baselineReady || !binding.baseline);
|
|
303
|
+
const unavailable = bindings.filter((binding) => (binding.baselineReady && binding.baseline?.result.availability === 'unavailable'));
|
|
304
|
+
if ((pending.length || unavailable.length) && available.length === 0) {
|
|
305
|
+
return {
|
|
306
|
+
availability: 'unavailable',
|
|
307
|
+
message: unavailable[0]?.baseline?.result.message
|
|
308
|
+
?? `${this.#label} Inbox baseline is still restoring`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const baselines = available.map((binding) => binding.baseline);
|
|
312
|
+
const degraded = baselines.find((baseline) => baseline.result.availability === 'degraded');
|
|
313
|
+
return {
|
|
314
|
+
availability: degraded || pending.length || unavailable.length ? 'degraded' : 'ready',
|
|
315
|
+
snapshot: baselines.flatMap((baseline) => baseline.result.snapshot ?? []),
|
|
316
|
+
...((degraded?.result.message ?? unavailable[0]?.baseline?.result.message) === undefined
|
|
317
|
+
? {} : { message: degraded?.result.message ?? unavailable[0].baseline.result.message }),
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async #processHostEvent(binding, event) {
|
|
322
|
+
if (binding.phase === 'closed')
|
|
323
|
+
return;
|
|
324
|
+
if (event.type === 'gap') {
|
|
325
|
+
await this.#restore(() => ({
|
|
326
|
+
availability: 'unavailable',
|
|
327
|
+
message: `${this.#label} Inbox stream gap after Bridge sequence ${event.afterSequence}`,
|
|
328
|
+
}));
|
|
329
|
+
this.#closeBinding(binding);
|
|
330
|
+
throw new PiInboxBridgeError(`${this.#label} Inbox Bridge stream continuity was lost`);
|
|
331
|
+
}
|
|
332
|
+
if (event.type === 'snapshot') {
|
|
333
|
+
const snapshot = parseSnapshot(event.value, 'ready', binding.lease.ref, this.#sourceId, event.sequence, this.#label);
|
|
334
|
+
binding.baseline = snapshot;
|
|
335
|
+
binding.baselineReady = true;
|
|
336
|
+
await this.#restoreBindings();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const operation = parseOperation(event.event, this.#sourceId);
|
|
340
|
+
if (!operation)
|
|
341
|
+
throw new PiInboxBridgeError(`${this.#label} Inbox Bridge returned an invalid live event`);
|
|
342
|
+
if (isControlOperation(operation))
|
|
343
|
+
return;
|
|
344
|
+
const result = await binding.projector.submit(operation);
|
|
345
|
+
if (!result.accepted) {
|
|
346
|
+
throw new PiInboxBridgeError(`${this.#label} Inbox live operation rejected: ${result.reason ?? 'unknown'}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
#closeBinding(binding) {
|
|
350
|
+
if (!binding || binding.phase === 'closed')
|
|
351
|
+
return;
|
|
352
|
+
binding.phase = 'closed';
|
|
353
|
+
binding.abort.abort('binding_closed');
|
|
354
|
+
binding.detachAbort();
|
|
355
|
+
binding.handle?.close();
|
|
356
|
+
binding.buffered = [];
|
|
357
|
+
if (this.#bindings.get(binding.lease.ref.runId) === binding) {
|
|
358
|
+
this.#bindings.delete(binding.lease.ref.runId);
|
|
359
|
+
if (!this.#closed)
|
|
360
|
+
void this.#restoreBindings().catch(() => { });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
export class PiInboxBridgeCoordinator extends BridgeInboxCoordinator {
|
|
365
|
+
constructor(options) {
|
|
366
|
+
super({ ...options, agentId: 'pi', label: 'Pi' });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const PI_SESSION_CONTROL_IMPLEMENTATION_VERSION = 3;
|
|
2
|
+
function isSnapshot(value) {
|
|
3
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
export function createPiSessionControlAdapter({ host, }) {
|
|
6
|
+
if (!host || typeof host.request !== 'function') {
|
|
7
|
+
throw new TypeError('Pi Session Control adapter requires LocalAgentBridgeHost');
|
|
8
|
+
}
|
|
9
|
+
const read = async (run, refresh) => {
|
|
10
|
+
// Connector v2 knows nothing about Session Control. Treat absence as an optional capability instead
|
|
11
|
+
// of probing an unregistered Bridge method and delaying or disabling its otherwise healthy chat.
|
|
12
|
+
if ((run.ref.implementationVersion ?? 1) < PI_SESSION_CONTROL_IMPLEMENTATION_VERSION)
|
|
13
|
+
return null;
|
|
14
|
+
const value = await host.request(run, 'session-control', 'read', { refresh }, { timeoutMs: refresh ? 30_000 : 8_000, signal: run.signal });
|
|
15
|
+
if (!isSnapshot(value))
|
|
16
|
+
throw new Error('Pi Extension returned an invalid Session Control snapshot');
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
apiVersion: 1,
|
|
21
|
+
readModelControl: (run, options) => read(run, options?.refresh === true),
|
|
22
|
+
async updateModelControl(run, patch) {
|
|
23
|
+
if ((run.ref.implementationVersion ?? 1) < PI_SESSION_CONTROL_IMPLEMENTATION_VERSION) {
|
|
24
|
+
throw new Error('Pi Session Control requires /reload');
|
|
25
|
+
}
|
|
26
|
+
const value = await host.request(run, 'session-control', 'update', patch, { timeoutMs: 8_000, signal: run.signal });
|
|
27
|
+
if (!isSnapshot(value))
|
|
28
|
+
throw new Error('Pi Extension returned an invalid Session Control snapshot');
|
|
29
|
+
return value;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { normTty } from './scanUtils.js';
|
|
4
|
+
const SUCCESS_TTL_MS = 30_000;
|
|
5
|
+
const FAILURE_TTL_MS = 3_000;
|
|
6
|
+
// Real executable path of a pid. lsof resolves launch symlinks on macOS; /proc is the cheap Linux path.
|
|
7
|
+
// A transient lsof failure is inconclusive, not a negative identity verdict that should escape the call.
|
|
8
|
+
export async function executablePath(run, pid) {
|
|
9
|
+
let out = '';
|
|
10
|
+
try {
|
|
11
|
+
out = await run('lsof', ['-a', '-p', String(pid), '-d', 'txt', '-Fn']);
|
|
12
|
+
}
|
|
13
|
+
catch { /* try /proc */ }
|
|
14
|
+
for (const line of String(out).split('\n'))
|
|
15
|
+
if (line[0] === 'n')
|
|
16
|
+
return line.slice(1).trim();
|
|
17
|
+
try {
|
|
18
|
+
return await fsp.readlink(`/proc/${pid}/exe`);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function parseForegroundProcesses(out) {
|
|
25
|
+
const rows = [];
|
|
26
|
+
for (const line of String(out).split('\n')) {
|
|
27
|
+
const m = line.match(/^\s*(\S+)\s+(\d+)\s+(\S+)\s*$/);
|
|
28
|
+
if (!m)
|
|
29
|
+
continue;
|
|
30
|
+
const [, rawTty, pid, stat] = m;
|
|
31
|
+
if (!rawTty || !pid || !stat)
|
|
32
|
+
continue;
|
|
33
|
+
const tty = normTty(rawTty);
|
|
34
|
+
if (tty && stat.includes('+'))
|
|
35
|
+
rows.push({ tty, pid });
|
|
36
|
+
}
|
|
37
|
+
return rows;
|
|
38
|
+
}
|
|
39
|
+
async function foregroundProcesses(run, ttys) {
|
|
40
|
+
let targeted = '';
|
|
41
|
+
try {
|
|
42
|
+
targeted = await run('ps', ['-t', ttys.join(','), '-o', 'tty=,pid=,stat=']);
|
|
43
|
+
}
|
|
44
|
+
catch { /* fall back to the portable full snapshot below */ }
|
|
45
|
+
if (String(targeted).trim())
|
|
46
|
+
return parseForegroundProcesses(targeted);
|
|
47
|
+
return parseForegroundProcesses(await run('ps', ['-Ao', 'tty=,pid=,stat=']));
|
|
48
|
+
}
|
|
49
|
+
// Normalize ambiguous tmux pane_current_command values only after tying the pane's TTY to a foreground
|
|
50
|
+
// process whose REAL executable proves the agent identity. Every call refreshes the candidate TTYs with ps;
|
|
51
|
+
// the cache only skips lsof while the exact foreground pid set is unchanged and its short TTL is live.
|
|
52
|
+
// This makes TTY reuse/process replacement invalidate immediately, and failed probes retry quickly. If a
|
|
53
|
+
// platform rejects the targeted query (defaultRun returns ''), retain the old full-system scan as fallback.
|
|
54
|
+
export async function resolveByExecutable(panes, run, verdicts, { candidate, normalized, matches, now = () => Date.now(), successTtlMs = SUCCESS_TTL_MS, failureTtlMs = FAILURE_TTL_MS, }) {
|
|
55
|
+
const candidates = panes.filter((p) => p && p.tty && candidate(p.cmd || ''));
|
|
56
|
+
if (!candidates.length)
|
|
57
|
+
return panes;
|
|
58
|
+
const ttys = [...new Set(candidates.map((pane) => normTty(pane.tty)).filter(Boolean))];
|
|
59
|
+
if (!ttys.length)
|
|
60
|
+
return panes;
|
|
61
|
+
const rows = await foregroundProcesses(run, ttys);
|
|
62
|
+
for (const pane of candidates) {
|
|
63
|
+
const tty = normTty(pane.tty);
|
|
64
|
+
const procs = rows.filter((r) => r.tty === tty);
|
|
65
|
+
const signature = procs.map((r) => r.pid).sort().join(',');
|
|
66
|
+
const key = `${normalized}|${tty}|${pane.cmd}`;
|
|
67
|
+
const cached = verdicts.get(key);
|
|
68
|
+
if (cached && cached.signature === signature && cached.expiresAt > now()) {
|
|
69
|
+
if (cached.ok)
|
|
70
|
+
pane.cmd = normalized;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
let ok = false;
|
|
74
|
+
let matchedPid = null;
|
|
75
|
+
for (const proc of procs) {
|
|
76
|
+
const exe = await executablePath(run, proc.pid);
|
|
77
|
+
if (matches(exe, pane.cmd || '')) {
|
|
78
|
+
ok = true;
|
|
79
|
+
matchedPid = proc.pid;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
verdicts.set(key, {
|
|
84
|
+
ok,
|
|
85
|
+
pid: matchedPid,
|
|
86
|
+
signature,
|
|
87
|
+
expiresAt: now() + (ok ? successTtlMs : failureTtlMs),
|
|
88
|
+
});
|
|
89
|
+
if (ok)
|
|
90
|
+
pane.cmd = normalized;
|
|
91
|
+
}
|
|
92
|
+
return panes;
|
|
93
|
+
}
|
|
94
|
+
export const executableBasename = (file) => path.basename(String(file || ''));
|