dsh-plugin-mobile-gateway 0.10.3 → 0.11.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/PROTOCOL.md +75 -2
- package/README.md +9 -1
- package/docs/agent-team-app-integration.md +945 -0
- package/docs/dsh-rc2-mobile-integration.md +10 -10
- package/docs/turn-outline-app-integration.md +196 -0
- package/lib/dsh-host-adapter.mjs +25 -5
- package/lib/index.mjs +130 -44
- package/lib/session-follower.mjs +5 -4
- package/lib/session-target.mjs +29 -0
- package/package.json +3 -3
package/lib/index.mjs
CHANGED
|
@@ -116,6 +116,7 @@ import { assistantFinish, failureDetails, toolResultDetails, turnReasonDetails }
|
|
|
116
116
|
// { "kind": "goal-clear", "sessionId", "cleared": true }
|
|
117
117
|
// { "kind": "tasks-updated", "sessionId", "asOfSeq", "todos" }
|
|
118
118
|
// { "kind": "goal-updated", "sessionId", "asOfSeq", "goal" }
|
|
119
|
+
// { "kind": "turn-outline-updated", "sessionId", "asOfSeq", "turnOutline": [...] | null }
|
|
119
120
|
// { "kind": "question-requested", "rpcId", "sessionId", "questions", "replay"? }
|
|
120
121
|
// { "kind": "question-response", "rpcId", "sessionId", "action", "accepted", "reason"? }
|
|
121
122
|
// { "kind": "question-resolved", "rpcId", "sessionId", "outcome" }
|
|
@@ -138,6 +139,7 @@ import { createDshHostAdapter, DSH_VERSION, SESSION_FORMAT_VERSION } from './dsh
|
|
|
138
139
|
import { createSpeechInputManager, SPEECH_REQUEST_TYPES } from './speech-input.mjs'
|
|
139
140
|
import { handlePluginQuery, PLUGIN_REQUEST_TYPES } from './plugin-management.mjs'
|
|
140
141
|
import { createSessionFollower } from './session-follower.mjs'
|
|
142
|
+
import { parseSessionTarget, TEAM_PROJECTION_KINDS } from './session-target.mjs'
|
|
141
143
|
import { createGatewayState, GATEWAY_MODES } from './gateway-state.mjs'
|
|
142
144
|
import { createCloudflareTunnel } from './cloudflare-tunnel.mjs'
|
|
143
145
|
import { gatewayProbeHeader } from './gateway-probe.mjs'
|
|
@@ -534,18 +536,26 @@ async function admitCommand(host, msg) {
|
|
|
534
536
|
// Handle one mobile -> agent message through the official host API. Returns
|
|
535
537
|
// the wire frame to send back, or null when nothing should be sent.
|
|
536
538
|
async function admitMessage(api, msg) {
|
|
539
|
+
const target = parseSessionTarget(msg, { optional: true })
|
|
540
|
+
if (target.error) return target.error
|
|
541
|
+
const requestIdentity = typeof msg.requestId === 'string' ? { requestId: msg.requestId } : {}
|
|
542
|
+
const rejectMessage = frame => ({ ...frame, requestType: 'message', ...target.value, ...requestIdentity })
|
|
543
|
+
if (target.value.parentSessionId !== undefined && target.value.subagentMode !== 'continuable') {
|
|
544
|
+
return { kind: 'error', code: 'subagent/not-resumable', requestType: 'message', ...target.value,
|
|
545
|
+
...requestIdentity, message: 'only continuable subagents accept messages' }
|
|
546
|
+
}
|
|
537
547
|
const preset = parseAgentPreset(msg, 'message')
|
|
538
|
-
if (preset.error) return preset.error
|
|
548
|
+
if (preset.error) return rejectMessage(preset.error)
|
|
539
549
|
if (preset.value !== undefined && typeof msg.sessionId === 'string' && msg.sessionId.trim()) {
|
|
540
|
-
return { kind: 'error', code: 'bad-request',
|
|
541
|
-
message: 'use select-agent-preset before sending a message to an existing session' }
|
|
550
|
+
return rejectMessage({ kind: 'error', code: 'bad-request',
|
|
551
|
+
message: 'use select-agent-preset before sending a message to an existing session' })
|
|
542
552
|
}
|
|
543
553
|
const text = typeof msg.text === 'string' ? msg.text.trim() : ''
|
|
544
554
|
const parsedImages = parseWireImages(msg.images, 'message')
|
|
545
|
-
if (parsedImages.error) return parsedImages.error
|
|
555
|
+
if (parsedImages.error) return rejectMessage(parsedImages.error)
|
|
546
556
|
const imageParts = parsedImages.value
|
|
547
557
|
if (!text && imageParts.length === 0) {
|
|
548
|
-
return { kind: 'error', code: 'bad-request', message: 'message requires non-empty text or at least one image' }
|
|
558
|
+
return rejectMessage({ kind: 'error', code: 'bad-request', message: 'message requires non-empty text or at least one image' })
|
|
549
559
|
}
|
|
550
560
|
const mode = msg.mode === 'steer' ? 'steer' : 'queue'
|
|
551
561
|
|
|
@@ -569,8 +579,10 @@ async function admitMessage(api, msg) {
|
|
|
569
579
|
}
|
|
570
580
|
|
|
571
581
|
const resp = await api.sessions.prompt({
|
|
582
|
+
...target.value,
|
|
572
583
|
sessionId,
|
|
573
584
|
mode,
|
|
585
|
+
...(target.value.parentSessionId === undefined ? {} : requestIdentity),
|
|
574
586
|
// Match the official WebUI ordering: images first, optional text last.
|
|
575
587
|
content: [...imageParts, ...(text ? [{ type: 'text', text }] : [])],
|
|
576
588
|
...(typeof msg.clientTimeZone === 'string' && msg.clientTimeZone.trim() ? { clientTimeZone: msg.clientTimeZone.trim() } : {}),
|
|
@@ -580,13 +592,19 @@ async function admitMessage(api, msg) {
|
|
|
580
592
|
kind: 'sent',
|
|
581
593
|
sessionId,
|
|
582
594
|
mode,
|
|
595
|
+
...requestIdentity,
|
|
596
|
+
...(target.value.parentSessionId === undefined ? {} : {
|
|
597
|
+
parentSessionId: target.value.parentSessionId, subagentMode: target.value.subagentMode,
|
|
598
|
+
messageId: resp.messageId,
|
|
599
|
+
}),
|
|
583
600
|
...(resp.command ? { command: resp.command } : {}),
|
|
584
601
|
}
|
|
585
602
|
} catch (error) {
|
|
586
603
|
const code = error && error.code ? error.code : 'internal'
|
|
587
604
|
const message = error && error.message ? error.message : String(error)
|
|
588
605
|
log(`mobile message failed: ${message}`)
|
|
589
|
-
return { kind: 'error', code, message,
|
|
606
|
+
return { kind: 'error', code, message, requestType: 'message', ...target.value,
|
|
607
|
+
...requestIdentity, ...(sessionId ? { sessionId } : {}) }
|
|
590
608
|
}
|
|
591
609
|
}
|
|
592
610
|
|
|
@@ -1079,13 +1097,13 @@ function requireGoalRef(msg, sessionId) {
|
|
|
1079
1097
|
return { value: { id, revision } }
|
|
1080
1098
|
}
|
|
1081
1099
|
|
|
1082
|
-
async function readSessionProjection(api, type, sessionId, key) {
|
|
1083
|
-
const history = await proxyQuery(api, 'history', api.sessions.history.bind(api.sessions), {
|
|
1084
|
-
if (history.kind !== 'history') return history
|
|
1100
|
+
async function readSessionProjection(api, type, sessionId, key, target = { sessionId }) {
|
|
1101
|
+
const history = await proxyQuery(api, 'history', api.sessions.history.bind(api.sessions), { ...target, maxMessages: 1 })
|
|
1102
|
+
if (history.kind !== 'history') return { ...history, ...target, requestType: type }
|
|
1085
1103
|
const values = (history.projections && history.projections.values) || {}
|
|
1086
1104
|
return {
|
|
1087
1105
|
kind: type,
|
|
1088
|
-
|
|
1106
|
+
...target,
|
|
1089
1107
|
asOfSeq: history.projections ? history.projections.asOfSeq : undefined,
|
|
1090
1108
|
[key]: Object.prototype.hasOwnProperty.call(values, key) ? values[key] : null,
|
|
1091
1109
|
}
|
|
@@ -1402,6 +1420,31 @@ function parseAgentPreset(msg, requestType, required = false) {
|
|
|
1402
1420
|
// Dispatch one mobile query frame; returns the wire frame to send back.
|
|
1403
1421
|
async function handleQuery(api, host, agentDefaultModel, msg, pluginContext) {
|
|
1404
1422
|
if (PLUGIN_REQUEST_TYPES.has(msg.type)) return handlePluginQuery(api, msg, pluginContext)
|
|
1423
|
+
if (msg.type === 'agent-team' || msg.type === 'subagents') {
|
|
1424
|
+
const session = requireSessionId(msg)
|
|
1425
|
+
const identity = typeof msg.requestId === 'string' ? { requestId: msg.requestId } : {}
|
|
1426
|
+
if (session.error) return { ...session.error, ...identity }
|
|
1427
|
+
const sessionId = session.value
|
|
1428
|
+
const key = msg.type === 'agent-team' ? 'agentTeam' : 'subagentCatalog'
|
|
1429
|
+
try {
|
|
1430
|
+
const snapshot = await api.sessions.projections({ sessionId })
|
|
1431
|
+
if (snapshot === null) throw Object.assign(new Error('session not found'), { code: 'session/not-found' })
|
|
1432
|
+
if (!['sequenced', 'migration-required'].includes(snapshot?.kind)
|
|
1433
|
+
|| !snapshot.values || (snapshot.kind === 'sequenced' && !Number.isSafeInteger(snapshot.asOfSeq))) {
|
|
1434
|
+
throw new Error('invalid session projections')
|
|
1435
|
+
}
|
|
1436
|
+
const value = snapshot.values[key] ?? null
|
|
1437
|
+
if (value !== null && (key === 'subagentCatalog' ? !Array.isArray(value)
|
|
1438
|
+
: !Array.isArray(value.members) || !Array.isArray(value.tasks))) throw new Error('invalid team projection')
|
|
1439
|
+
return { kind: msg.type, sessionId, ...identity,
|
|
1440
|
+
projectionState: snapshot.kind === 'migration-required' ? 'migration-required'
|
|
1441
|
+
: value === null ? 'unavailable' : 'ready',
|
|
1442
|
+
...(snapshot.kind === 'sequenced' ? { asOfSeq: snapshot.asOfSeq } : {}), [key]: value }
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
return { kind: 'error', code: error.code || 'internal', message: error.message,
|
|
1445
|
+
requestType: msg.type, sessionId, ...identity }
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1405
1448
|
if (msg.type === 'session-agent-preset' || msg.type === 'select-agent-preset') {
|
|
1406
1449
|
const session = requireSessionId(msg)
|
|
1407
1450
|
const preset = parseAgentPreset(msg, msg.type, msg.type === 'select-agent-preset')
|
|
@@ -1433,21 +1476,32 @@ async function handleQuery(api, host, agentDefaultModel, msg, pluginContext) {
|
|
|
1433
1476
|
return proxyQuery(api, 'workspaces', api.workspace.list.bind(api.workspace), {})
|
|
1434
1477
|
}
|
|
1435
1478
|
if (msg.type === 'sessions') {
|
|
1436
|
-
|
|
1479
|
+
const frame = await proxyQuery(api, 'sessions', api.sessions.list.bind(api.sessions), {})
|
|
1480
|
+
if (frame.kind !== 'sessions' || !Array.isArray(frame.items)) return frame
|
|
1481
|
+
// Lists need only titles; full projections can make tunnel frames many MiB.
|
|
1482
|
+
// Copy at the mobile boundary so Host data and history projections stay intact.
|
|
1483
|
+
return { ...frame, items: frame.items.map(item => {
|
|
1484
|
+
if (!item?.projections) return item
|
|
1485
|
+
const { asOfSeq, values } = item.projections
|
|
1486
|
+
return { ...item, projections: {
|
|
1487
|
+
...(asOfSeq === undefined ? {} : { asOfSeq }),
|
|
1488
|
+
values: values && Object.hasOwn(values, 'title') ? { title: values.title } : {},
|
|
1489
|
+
} }
|
|
1490
|
+
}) }
|
|
1437
1491
|
}
|
|
1438
1492
|
if (msg.type === 'history') {
|
|
1439
|
-
const
|
|
1440
|
-
if (
|
|
1441
|
-
|
|
1442
|
-
}
|
|
1493
|
+
const target = parseSessionTarget(msg)
|
|
1494
|
+
if (target.error) return target.error
|
|
1495
|
+
const { sessionId } = target.value
|
|
1496
|
+
const identity = typeof msg.requestId === 'string' ? { requestId: msg.requestId } : {}
|
|
1443
1497
|
const cursorError = validateHistoryCursor(msg, sessionId, 'beforeSeq')
|
|
1444
|
-
if (cursorError) return cursorError
|
|
1445
|
-
const payload = {
|
|
1498
|
+
if (cursorError) return { ...cursorError, ...target.value, ...identity }
|
|
1499
|
+
const payload = { ...target.value }
|
|
1446
1500
|
if (typeof msg.beforeSeq === 'number' && Number.isFinite(msg.beforeSeq)) payload.beforeSeq = msg.beforeSeq
|
|
1447
1501
|
if (typeof msg.maxMessages === 'number' && Number.isFinite(msg.maxMessages)) payload.maxMessages = msg.maxMessages
|
|
1448
1502
|
const frame = await proxyQuery(api, 'history', api.sessions.history.bind(api.sessions), payload)
|
|
1449
|
-
if (frame.kind === 'history') return historyPage(frame, { ...msg, sessionId })
|
|
1450
|
-
return frame
|
|
1503
|
+
if (frame.kind === 'history') return { ...historyPage(frame, { ...msg, sessionId }), ...target.value, ...identity }
|
|
1504
|
+
return { ...frame, ...target.value, ...identity }
|
|
1451
1505
|
}
|
|
1452
1506
|
if (msg.type === 'attachment') {
|
|
1453
1507
|
const sessionId = requireSessionId(msg)
|
|
@@ -1515,16 +1569,17 @@ async function handleQuery(api, host, agentDefaultModel, msg, pluginContext) {
|
|
|
1515
1569
|
return frame
|
|
1516
1570
|
}
|
|
1517
1571
|
if (msg.type === 'session-cancel') {
|
|
1518
|
-
const
|
|
1519
|
-
if (
|
|
1572
|
+
const target = parseSessionTarget(msg)
|
|
1573
|
+
if (target.error) return target.error
|
|
1574
|
+
const identity = typeof msg.requestId === 'string' ? { requestId: msg.requestId } : {}
|
|
1520
1575
|
const frame = await proxyQuery(
|
|
1521
1576
|
api,
|
|
1522
1577
|
'session-cancel',
|
|
1523
1578
|
api.sessions.cancel.bind(api.sessions),
|
|
1524
|
-
|
|
1579
|
+
target.value,
|
|
1525
1580
|
)
|
|
1526
|
-
if (frame.kind === 'error') return { ...frame,
|
|
1527
|
-
return { ...frame, kind: 'session-cancelled',
|
|
1581
|
+
if (frame.kind === 'error') return { ...frame, ...target.value, ...identity }
|
|
1582
|
+
return { ...frame, kind: 'session-cancelled', ...target.value, ...identity }
|
|
1528
1583
|
}
|
|
1529
1584
|
if (msg.type === 'queue-update') {
|
|
1530
1585
|
const sessionId = requireSessionId(msg)
|
|
@@ -1827,15 +1882,12 @@ async function handleQuery(api, host, agentDefaultModel, msg, pluginContext) {
|
|
|
1827
1882
|
contextPressure: values.contextPressure || null,
|
|
1828
1883
|
}
|
|
1829
1884
|
}
|
|
1830
|
-
if (msg.type === 'tasks') {
|
|
1831
|
-
const
|
|
1832
|
-
if (
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const sessionId = requireSessionId(msg)
|
|
1837
|
-
if (sessionId.error) return sessionId.error
|
|
1838
|
-
return readSessionProjection(api, 'goal', sessionId.value, 'goal')
|
|
1885
|
+
if (msg.type === 'tasks' || msg.type === 'goal') {
|
|
1886
|
+
const target = parseSessionTarget(msg)
|
|
1887
|
+
if (target.error) return target.error
|
|
1888
|
+
const frame = await readSessionProjection(api, msg.type, target.value.sessionId,
|
|
1889
|
+
msg.type === 'tasks' ? 'todos' : 'goal', target.value)
|
|
1890
|
+
return { ...frame, ...(typeof msg.requestId === 'string' ? { requestId: msg.requestId } : {}) }
|
|
1839
1891
|
}
|
|
1840
1892
|
if (msg.type === 'goal-edit') {
|
|
1841
1893
|
const sessionId = requireSessionId(msg)
|
|
@@ -2984,6 +3036,8 @@ const plugin = {
|
|
|
2984
3036
|
}
|
|
2985
3037
|
void speechInput.handle(ws, msg, sendFrame).then(sendFrame)
|
|
2986
3038
|
} else if (msg.type === 'subscribe') {
|
|
3039
|
+
const target = parseSessionTarget(msg, { optional: true })
|
|
3040
|
+
if (target.error) { sendFrame(target.error); return }
|
|
2987
3041
|
if (msg.assistantStream !== undefined && typeof msg.assistantStream !== 'boolean') {
|
|
2988
3042
|
sendFrame({ kind: 'error', code: 'bad-request', requestType: 'subscribe', message: 'assistantStream must be a boolean' })
|
|
2989
3043
|
return
|
|
@@ -2997,9 +3051,9 @@ const plugin = {
|
|
|
2997
3051
|
ws.filterSessionId = sessionId
|
|
2998
3052
|
ws.sessionFollowerActive = msg.assistantStream === true
|
|
2999
3053
|
const subscriptionId = crypto.randomUUID()
|
|
3000
|
-
sendFrame({ kind: 'subscribed', sessionId: sessionId || null, subscriptionId, assistantStream: ws.sessionFollowerActive })
|
|
3054
|
+
sendFrame({ kind: 'subscribed', ...target.value, sessionId: sessionId || null, subscriptionId, assistantStream: ws.sessionFollowerActive })
|
|
3001
3055
|
if (ws.sessionFollowerActive) {
|
|
3002
|
-
void ws.sessionFollower.start(sessionId, subscriptionId).catch(error => {
|
|
3056
|
+
void ws.sessionFollower.start(sessionId, subscriptionId, target.value).catch(error => {
|
|
3003
3057
|
log(`session follower failed: ${error.message}`)
|
|
3004
3058
|
if (ws.readyState === 1) ws.close(1011, 'session follower failed')
|
|
3005
3059
|
})
|
|
@@ -3027,7 +3081,8 @@ const plugin = {
|
|
|
3027
3081
|
admitMessage(api, msg).then((frame) => {
|
|
3028
3082
|
if (frame && ws.readyState === 1) ws.send(stringifyWireFrame(frame))
|
|
3029
3083
|
})
|
|
3030
|
-
} else if (msg.type === '
|
|
3084
|
+
} else if (msg.type === 'agent-team' || msg.type === 'subagents' ||
|
|
3085
|
+
msg.type === 'workspaces' || msg.type === 'sessions' || msg.type === 'history' || msg.type === 'attachment' ||
|
|
3031
3086
|
msg.type === 'search' || msg.type === 'host' || msg.type === 'directories' || msg.type === 'directory-create' ||
|
|
3032
3087
|
msg.type === 'workspace-create' || msg.type === 'session-create' || msg.type === 'models' || msg.type === 'commands' || msg.type === 'command-execute' ||
|
|
3033
3088
|
msg.type === 'command-options' || msg.type === 'command-select' || msg.type === 'select-model' ||
|
|
@@ -3096,9 +3151,12 @@ const plugin = {
|
|
|
3096
3151
|
'assistant-stream-v1',
|
|
3097
3152
|
'history-format-version',
|
|
3098
3153
|
'projection-baseline',
|
|
3154
|
+
'turn-outline-v1',
|
|
3099
3155
|
'images',
|
|
3100
3156
|
'session-create',
|
|
3101
3157
|
'session-agent-preset',
|
|
3158
|
+
'agent-team-v1',
|
|
3159
|
+
'subagent-conversations-v1',
|
|
3102
3160
|
'commands',
|
|
3103
3161
|
'tasks',
|
|
3104
3162
|
'goals',
|
|
@@ -3224,6 +3282,15 @@ const plugin = {
|
|
|
3224
3282
|
const disposeScheduleChanged = ctx.on('schedule/changed', () => {
|
|
3225
3283
|
if (clients.size > 0) broadcastSessionMetadataFrame({ kind: 'schedule-changed' })
|
|
3226
3284
|
})
|
|
3285
|
+
const disposeSessionStatus = ctx.on('api-session/status', (sessionId, running) => {
|
|
3286
|
+
broadcastSessionMetadataFrame({ kind: 'session-status', sessionId: String(sessionId), running })
|
|
3287
|
+
})
|
|
3288
|
+
const disposeSessionAdded = ctx.on('api-session/added', summary => {
|
|
3289
|
+
broadcastSessionMetadataFrame({ kind: 'session-status', sessionId: String(summary.sessionId), running: summary.running })
|
|
3290
|
+
})
|
|
3291
|
+
const disposeSessionRemoved = ctx.on('api-session/removed', sessionId => {
|
|
3292
|
+
broadcastSessionMetadataFrame({ kind: 'session-status', sessionId: String(sessionId), running: false })
|
|
3293
|
+
})
|
|
3227
3294
|
|
|
3228
3295
|
const disposePluginsChanged = ctx.on('plugin-manager/changed', (change) => {
|
|
3229
3296
|
if (clients.size > 0) broadcastSessionMetadataFrame({ kind: 'plugins-changed', reason: change?.reason })
|
|
@@ -3393,10 +3460,24 @@ const plugin = {
|
|
|
3393
3460
|
log(`session queue forwarded: session=${sessionId} items=${items.length}`)
|
|
3394
3461
|
}
|
|
3395
3462
|
|
|
3463
|
+
const projectionKinds = {
|
|
3464
|
+
todos: 'tasks-updated',
|
|
3465
|
+
goal: 'goal-updated',
|
|
3466
|
+
turnOutline: 'turn-outline-updated',
|
|
3467
|
+
...TEAM_PROJECTION_KINDS,
|
|
3468
|
+
}
|
|
3469
|
+
const projectionValues = (values = {}) => Object.fromEntries(
|
|
3470
|
+
Object.keys(projectionKinds).map(key => [key, values[key] ?? null]),
|
|
3471
|
+
)
|
|
3396
3472
|
const projectionFrame = (sessionId, key, value, seq) => ({
|
|
3397
|
-
kind: key
|
|
3473
|
+
kind: projectionKinds[key], sessionId, asOfSeq: seq,
|
|
3398
3474
|
[key]: value,
|
|
3399
3475
|
})
|
|
3476
|
+
const broadcastProjection = (sessionId, key, value, seq) => {
|
|
3477
|
+
const broadcast = key === 'turnOutline' || Object.hasOwn(TEAM_PROJECTION_KINDS, key)
|
|
3478
|
+
? broadcastSessionMetadataFrame : broadcastInteractionFrame
|
|
3479
|
+
broadcast(projectionFrame(sessionId, key, value, seq))
|
|
3480
|
+
}
|
|
3400
3481
|
|
|
3401
3482
|
const installProjectionBaseline = value => {
|
|
3402
3483
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid projection baseline')
|
|
@@ -3406,16 +3487,17 @@ const plugin = {
|
|
|
3406
3487
|
throw new Error('invalid session projection baseline')
|
|
3407
3488
|
}
|
|
3408
3489
|
Object.defineProperty(next, sessionId, { enumerable: true, configurable: true, writable: true,
|
|
3409
|
-
value: { asOfSeq: baseline.asOfSeq,
|
|
3490
|
+
value: { asOfSeq: baseline.asOfSeq,
|
|
3491
|
+
values: projectionValues(baseline.values) } })
|
|
3410
3492
|
}
|
|
3411
3493
|
const previous = sessionProjectionBaselines
|
|
3412
3494
|
sessionProjectionBaselines = next
|
|
3413
3495
|
broadcastSessionMetadataFrame({ kind: 'projection-baseline', projections: next })
|
|
3414
3496
|
for (const sessionId of new Set([...Object.keys(previous), ...Object.keys(next)])) {
|
|
3415
3497
|
const baseline = Object.hasOwn(next, sessionId) ? next[sessionId] : null
|
|
3416
|
-
for (const key of
|
|
3417
|
-
|
|
3418
|
-
baseline?.asOfSeq ?? previous[sessionId].asOfSeq)
|
|
3498
|
+
for (const key of Object.keys(projectionKinds)) {
|
|
3499
|
+
broadcastProjection(sessionId, key, baseline?.values[key] ?? null,
|
|
3500
|
+
baseline?.asOfSeq ?? previous[sessionId].asOfSeq)
|
|
3419
3501
|
}
|
|
3420
3502
|
}
|
|
3421
3503
|
}
|
|
@@ -3452,12 +3534,13 @@ const plugin = {
|
|
|
3452
3534
|
installSessionQueueBaseline(frame.value?.projections)
|
|
3453
3535
|
} else if (frame?.type === 'projection' && frame.key === 'inbox') {
|
|
3454
3536
|
installSessionQueue(frame.sessionId, frame.value)
|
|
3455
|
-
} else if (frame?.type === 'projection' && (
|
|
3537
|
+
} else if (frame?.type === 'projection' && Object.hasOwn(projectionKinds, frame.key)) {
|
|
3456
3538
|
const sessionId = String(frame.sessionId)
|
|
3457
3539
|
const previous = Object.hasOwn(sessionProjectionBaselines, sessionId) ? sessionProjectionBaselines[sessionId] : null
|
|
3540
|
+
const value = frame.value ?? null
|
|
3458
3541
|
Object.defineProperty(sessionProjectionBaselines, sessionId, { enumerable: true, configurable: true, writable: true,
|
|
3459
|
-
value: { asOfSeq: frame.seq, values: { ...previous?.values, [frame.key]:
|
|
3460
|
-
|
|
3542
|
+
value: { asOfSeq: frame.seq, values: { ...projectionValues(previous?.values), [frame.key]: value } } })
|
|
3543
|
+
broadcastProjection(sessionId, frame.key, value, frame.seq)
|
|
3461
3544
|
log(`projection forwarded: key=${frame.key} session=${sessionId} seq=${frame.seq}`)
|
|
3462
3545
|
}
|
|
3463
3546
|
}
|
|
@@ -3490,6 +3573,9 @@ const plugin = {
|
|
|
3490
3573
|
disposeMgmt()
|
|
3491
3574
|
disposeEvents()
|
|
3492
3575
|
disposeScheduleChanged()
|
|
3576
|
+
disposeSessionStatus()
|
|
3577
|
+
disposeSessionAdded()
|
|
3578
|
+
disposeSessionRemoved()
|
|
3493
3579
|
disposePluginsChanged()
|
|
3494
3580
|
disposePluginSettingsChanged()
|
|
3495
3581
|
disposePluginInstallState()
|
package/lib/session-follower.mjs
CHANGED
|
@@ -91,7 +91,7 @@ export function createSessionFollower(api, { onFrame, onError, retryMs = 1000 })
|
|
|
91
91
|
current = undefined
|
|
92
92
|
previous?.abort.abort()
|
|
93
93
|
}
|
|
94
|
-
const start = (sessionId, subscriptionId) => {
|
|
94
|
+
const start = (sessionId, subscriptionId, target = { sessionId }) => {
|
|
95
95
|
stop()
|
|
96
96
|
const state = { abort: new AbortController() }
|
|
97
97
|
current = state
|
|
@@ -101,12 +101,12 @@ export function createSessionFollower(api, { onFrame, onError, retryMs = 1000 })
|
|
|
101
101
|
const attemptAbort = new AbortController()
|
|
102
102
|
const signal = AbortSignal.any([state.abort.signal, attemptAbort.signal])
|
|
103
103
|
const streamId = crypto.randomUUID()
|
|
104
|
-
const context = { sessionId, subscriptionId, streamId }
|
|
104
|
+
const context = { ...target, sessionId, subscriptionId, streamId }
|
|
105
105
|
const decode = createFollowDecoder(sessionId)
|
|
106
106
|
let iterator
|
|
107
107
|
let permanent = false
|
|
108
108
|
try {
|
|
109
|
-
const stream = await api.openSessionStream(sessionId, signal)
|
|
109
|
+
const stream = await api.openSessionStream(sessionId, signal, target)
|
|
110
110
|
iterator = stream[Symbol.asyncIterator]()
|
|
111
111
|
while (current === state && !signal.aborted) {
|
|
112
112
|
const item = await iterator.next()
|
|
@@ -117,7 +117,8 @@ export function createSessionFollower(api, { onFrame, onError, retryMs = 1000 })
|
|
|
117
117
|
}
|
|
118
118
|
} catch (error) {
|
|
119
119
|
if (current !== state || signal.aborted) break
|
|
120
|
-
permanent = ['unsupported-session-format', 'session/not-found', 'gateway/bad-request'
|
|
120
|
+
permanent = ['unsupported-session-format', 'session/not-found', 'gateway/bad-request',
|
|
121
|
+
'subagent/unauthorized', 'subagent/not-found', 'subagent/catalog-diagnostic'].includes(error.code)
|
|
121
122
|
|| (error.code === 'SESSION_QUERY_PERSISTENCE_FAILED'
|
|
122
123
|
&& /refuses this format .*Session/.test(error.message || ''))
|
|
123
124
|
onError(error, { ...context, retrying: !permanent })
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// 移动端保留 sessionId 为当前视图 ID,父子关系只作为显式路由地址。
|
|
2
|
+
export function parseSessionTarget(msg, { optional = false } = {}) {
|
|
3
|
+
const fail = message => ({ error: { kind: 'error', code: 'bad-request', requestType: msg.type, message,
|
|
4
|
+
...(typeof msg.requestId === 'string' ? { requestId: msg.requestId } : {}) } })
|
|
5
|
+
const sessionId = typeof msg.sessionId === 'string' ? msg.sessionId.trim() : ''
|
|
6
|
+
const hasChildAddress = msg.parentSessionId !== undefined || msg.subagentMode !== undefined
|
|
7
|
+
if (!sessionId && (!optional || hasChildAddress)) return fail('sessionId must be a non-empty string')
|
|
8
|
+
if (!hasChildAddress) return { value: sessionId ? { sessionId } : {} }
|
|
9
|
+
const parentSessionId = typeof msg.parentSessionId === 'string' ? msg.parentSessionId.trim() : ''
|
|
10
|
+
if (!parentSessionId || parentSessionId === sessionId) return fail('parentSessionId must identify a different direct parent')
|
|
11
|
+
if (!['continuable', 'one-shot', 'unknown'].includes(msg.subagentMode)) {
|
|
12
|
+
return fail('subagentMode must be continuable, one-shot, or unknown')
|
|
13
|
+
}
|
|
14
|
+
return { value: { sessionId, parentSessionId, subagentMode: msg.subagentMode } }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sessionAddress(target) {
|
|
18
|
+
return target.parentSessionId === undefined
|
|
19
|
+
? { kind: 'session', sessionId: target.sessionId }
|
|
20
|
+
: { kind: 'subagent', parentSessionId: target.parentSessionId,
|
|
21
|
+
childSessionId: target.sessionId, mode: target.subagentMode }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const TEAM_PROJECTION_KINDS = {
|
|
25
|
+
agentTeam: 'agent-team-updated',
|
|
26
|
+
subagentCatalog: 'subagents-updated',
|
|
27
|
+
subagent: 'subagent-identity-updated',
|
|
28
|
+
subagentTiming: 'subagent-timing-updated',
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-mobile-gateway",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "DeepSeek Harness 移动网关,支持安全设备配对、实时会话同步、插件管理、官方语音输入、文件卡片与安全预览、局域网及 Cloudflare Tunnel。0.11.0 新增智能体团队与子会话协议,支持团队成员、共享任务看板、多层子智能体目录和运行状态实时同步,以及子智能体独立历史、视图切换、继续对话、图片输入与停止执行;提供完整 APP 接入文档,客户端需按能力发现接入。",
|
|
5
5
|
"main": "lib/index.mjs",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"bin": "bin/setup-ip.mjs",
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "node test/wire-json.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/speech-input.test.mjs && node test/plugin-management.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node test/session-event-details.test.mjs && node test/gateway.test.mjs && node --test test/approval-replay.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node --test test/public-endpoint-check.test.mjs && node test/gateway-tools.test.mjs && node test/multi-gateway.test.mjs"
|
|
29
|
+
"test": "node test/wire-json.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/speech-input.test.mjs && node test/plugin-management.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node --test test/agent-team.test.mjs && node test/session-event-details.test.mjs && node test/gateway.test.mjs && node --test test/approval-replay.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node --test test/public-endpoint-check.test.mjs && node test/gateway-tools.test.mjs && node test/multi-gateway.test.mjs"
|
|
30
30
|
},
|
|
31
31
|
"exports": {
|
|
32
32
|
".": "./lib/index.mjs",
|