dsh-monkeyai-sessions 0.1.0 → 0.1.2

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.
@@ -390,6 +390,45 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
390
390
  return this.exchangeAuthorization(auth, callbackUrl)
391
391
  }
392
392
 
393
+ // The runtime requires an expectedRevision on every configuration write and
394
+ // rejects a stale one with configuration_conflict. The MonkeyAI client (or
395
+ // another panel) can advance the revision at any time, so read the current
396
+ // value from the conflict payload — or from the HTTP session state when we
397
+ // have none — and retry instead of failing the switch.
398
+ async setSessionModel({ acp, sessionId, model, httpBase, ticket, revision, signal, onRevision }) {
399
+ let expected = revision
400
+ for (let attempt = 0; attempt < 3; attempt += 1) {
401
+ if (!Number.isSafeInteger(expected)) {
402
+ expected = await readSessionConfigurationRevision({ httpBase, ticket, sessionId, signal })
403
+ }
404
+ if (!Number.isSafeInteger(expected)) throw new Error('无法读取会话配置版本,暂时不能切换模型')
405
+ try {
406
+ const result = await acp.request('_monkeyai/session/model/set', {
407
+ sessionId,
408
+ model,
409
+ _meta: { expectedRevision: expected },
410
+ }, { signal, timeoutMs: 120_000 })
411
+ const next = result?._meta?.configurationRevision
412
+ if (Number.isSafeInteger(next)) onRevision(next)
413
+ return typeof result?.model === 'string' ? result.model : model
414
+ } catch (error) {
415
+ if (signal?.aborted) throw error
416
+ const current = error?.data?.configurationRevision
417
+ if (error?.data?.reason === 'configuration_conflict' && Number.isSafeInteger(current) && current !== expected) {
418
+ expected = current
419
+ onRevision(current)
420
+ continue
421
+ }
422
+ if (error?.data?.reason === 'configuration_version_required') {
423
+ expected = undefined
424
+ continue
425
+ }
426
+ throw error
427
+ }
428
+ }
429
+ throw new Error('会话配置持续被其他客户端修改,请重试切换模型')
430
+ }
431
+
393
432
  async exchangeAuthorization(auth, callbackUrl) {
394
433
  const callback = parseAuthorizationCallback(callbackUrl, auth)
395
434
  const tokens = await requestJson(auth.serverOrigin, '/oauth/token', {
@@ -620,7 +659,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
620
659
  }
621
660
  return {
622
661
  switched: false,
623
- message: 'This MonkeyAI runtime does not support switching models on an active session. Create a new session to use a different model.',
662
+ message: '该 MonkeyAI runtime 不支持在活跃会话上切换模型,请新建会话使用其他模型。',
624
663
  }
625
664
  } finally {
626
665
  acp.close()
@@ -665,7 +704,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
665
704
  // oversized single frame; both end the stream.
666
705
  const pushed = events.push(frame)
667
706
  if (pushed === false) {
668
- const reason = new Error('Remote session event queue exceeded its limit')
707
+ const reason = new Error('远程会话事件队列超出上限')
669
708
  controller.abort(reason)
670
709
  currentAcp?.close(reason)
671
710
  return false
@@ -793,7 +832,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
793
832
  return
794
833
  }
795
834
  reconnectsUsed += 1
796
- emit({ type: 'reconnecting', sessionId: selectedSession, message: `History replay interrupted - reconnecting to continue loading (${reconnectsUsed}/${MAX_REPLAY_RECONNECTS})` })
835
+ emit({ type: 'reconnecting', sessionId: selectedSession, message: `历史回放中断,正在重连以继续加载(${reconnectsUsed}/${MAX_REPLAY_RECONNECTS})` })
797
836
  void (async () => {
798
837
  try {
799
838
  await establishAcp(true)
@@ -802,7 +841,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
802
841
  type: 'error',
803
842
  sessionId: selectedSession,
804
843
  code: 'acp_connection_closed',
805
- message: reconnectError instanceof Error ? reconnectError.message : 'Remote session connection closed',
844
+ message: reconnectError instanceof Error ? reconnectError.message : '远程会话连接已关闭',
806
845
  })
807
846
  controller.abort(reason)
808
847
  events.close()
@@ -814,7 +853,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
814
853
  // keep working (the runtime binds sessions per connection).
815
854
  if (loadCompleted && reconnectsUsed < MAX_REPLAY_RECONNECTS) {
816
855
  reconnectsUsed += 1
817
- emit({ type: 'reconnecting', sessionId: selectedSession, message: 'Remote connection dropped - reconnecting' })
856
+ emit({ type: 'reconnecting', sessionId: selectedSession, message: '远程连接中断,正在重连' })
818
857
  void (async () => {
819
858
  try {
820
859
  await establishAcp(true)
@@ -824,7 +863,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
824
863
  type: 'error',
825
864
  sessionId: selectedSession,
826
865
  code: 'acp_connection_closed',
827
- message: reconnectError instanceof Error ? reconnectError.message : 'Remote session connection closed',
866
+ message: reconnectError instanceof Error ? reconnectError.message : '远程会话连接已关闭',
828
867
  })
829
868
  controller.abort(reason)
830
869
  events.close()
@@ -836,14 +875,14 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
836
875
  type: 'error',
837
876
  sessionId: selectedSession,
838
877
  code: 'acp_connection_closed',
839
- message: 'Remote session connection closed',
878
+ message: '远程会话连接已关闭',
840
879
  })
841
880
  controller.abort(reason)
842
881
  events.close()
843
882
  })
844
883
  unsubscribe = () => { unsubscribeThis(); unsubscribe = () => {} }
845
884
  unsubscribeClose = () => { unsubscribeCloseThis(); unsubscribeClose = () => {} }
846
- if (signal.aborted) throw signal.reason ?? new Error('Remote session cancelled')
885
+ if (signal.aborted) throw signal.reason ?? new Error('远程会话已取消')
847
886
 
848
887
  const initialized = await acp.request('initialize', {
849
888
  protocolVersion: 1,
@@ -883,7 +922,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
883
922
  type: 'session_missing',
884
923
  sessionId: selectedSession,
885
924
  code: 'session_not_found',
886
- message: 'This session no longer exists on the selected device (the runtime may have been reset). Create a new session instead.',
925
+ message: '该会话在所选设备上已不存在(runtime 可能已重置),请新建会话。',
887
926
  })
888
927
  connection.sessionMissing = true
889
928
  return
@@ -902,7 +941,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
902
941
  type: 'history_incomplete',
903
942
  sessionId: selectedSession,
904
943
  code: 'history_load_failed',
905
- message: 'History replay was repeatedly cut off by the remote runtime; showing only the messages received so far.',
944
+ message: '历史回放被远程 runtime 反复中断,仅显示已收到的消息。',
906
945
  })
907
946
  loadCompleted = true
908
947
  }
@@ -914,12 +953,12 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
914
953
  grantRef = await this.getGrant(accountId)
915
954
  this.requireScope(grantRef, 'device:connect')
916
955
  this.requireScope(grantRef, 'session:read')
917
- if (signal.aborted) throw signal.reason ?? new Error('Remote session cancelled')
956
+ if (signal.aborted) throw signal.reason ?? new Error('远程会话已取消')
918
957
 
919
958
  const token = await this.refreshAccessToken(accountId, signal)
920
959
  const ticket = await this.requestConnectionTicket(accountId, grantRef, token, target, { signal })
921
960
  ticketValue = ticket.ticket
922
- if (signal.aborted) throw signal.reason ?? new Error('Remote session cancelled')
961
+ if (signal.aborted) throw signal.reason ?? new Error('远程会话已取消')
923
962
 
924
963
  // Load the full history over the runtime's HTTP read channel before the
925
964
  // ACP connection starts. The ACP session/load replay only streams a
@@ -959,7 +998,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
959
998
  try {
960
999
  const currentGrant = await this.getGrant(accountId)
961
1000
  if (!currentGrant.token.scopes.includes('session:read')) {
962
- emit({ type: 'error', sessionId: selectedSession, code: 'read_permission_revoked', message: 'Session read permission is no longer available' })
1001
+ emit({ type: 'error', sessionId: selectedSession, code: 'read_permission_revoked', message: '会话读取权限已失效' })
963
1002
  controller.abort(new Error('Session read permission was revoked'))
964
1003
  currentAcp?.close(new Error('Session read permission was revoked'))
965
1004
  events.close()
@@ -973,18 +1012,20 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
973
1012
  continue
974
1013
  }
975
1014
 
976
- if (!currentAcp) throw new Error('ACP connection is re-establishing')
1015
+ if (!currentAcp) throw new Error('ACP 连接正在重建')
977
1016
 
978
1017
  if (command.type === 'switch_model') {
979
1018
  try {
980
- const result = await currentAcp.request('_monkeyai/session/model/set', {
1019
+ const model = await this.setSessionModel({
1020
+ acp: currentAcp,
981
1021
  sessionId: selectedSession,
982
1022
  model: command.model,
983
- ...(httpConfigurationRevision === undefined ? {} : { _meta: { expectedRevision: httpConfigurationRevision } }),
984
- }, { signal, timeoutMs: 120_000 })
985
- const nextRevision = result?._meta?.configurationRevision
986
- if (Number.isSafeInteger(nextRevision)) httpConfigurationRevision = nextRevision
987
- const model = typeof result?.model === 'string' ? result.model : command.model
1023
+ httpBase: ticket.httpBase,
1024
+ ticket: ticketValue,
1025
+ revision: httpConfigurationRevision,
1026
+ signal,
1027
+ onRevision: value => { httpConfigurationRevision = value },
1028
+ })
988
1029
  currentLoadedModel = model
989
1030
  emit({ type: 'model_switched', sessionId: selectedSession, data: { model } })
990
1031
  } catch (switchError) {
@@ -1017,7 +1058,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
1017
1058
  type: 'delivery',
1018
1059
  sessionId: selectedSession,
1019
1060
  data: { accepted: false, code: 'remote_rejected' },
1020
- message: promptError instanceof Error ? promptError.message : 'Remote runtime rejected the instruction',
1061
+ message: promptError instanceof Error ? promptError.message : '远程 runtime 拒绝了该指令',
1021
1062
  })
1022
1063
  }
1023
1064
  continue
@@ -1043,7 +1084,7 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
1043
1084
  continue
1044
1085
  }
1045
1086
 
1046
- emit({ type: 'error', sessionId: selectedSession, code: 'invalid_command', message: 'Remote command is unsupported' })
1087
+ emit({ type: 'error', sessionId: selectedSession, code: 'invalid_command', message: '不支持该远程命令' })
1047
1088
  } catch (error) {
1048
1089
  if (signal.aborted) return
1049
1090
  const requestId = command.type === 'respond' ? command.requestId : undefined
@@ -1061,9 +1102,9 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
1061
1102
  })()
1062
1103
  commandPump.catch(() => {
1063
1104
  if (signal.aborted) return
1064
- emit({ type: 'error', sessionId: selectedSession, code: 'command_channel_closed', message: 'Remote command channel closed' })
1065
- controller.abort(new Error('Remote command channel closed'))
1066
- currentAcp?.close(new Error('Remote command channel closed'))
1105
+ emit({ type: 'error', sessionId: selectedSession, code: 'command_channel_closed', message: '远程命令通道已关闭' })
1106
+ controller.abort(new Error('远程命令通道已关闭'))
1107
+ currentAcp?.close(new Error('远程命令通道已关闭'))
1067
1108
  events.close()
1068
1109
  })
1069
1110
 
@@ -1223,6 +1264,16 @@ async function readHistoryBodyText(response, maxBytes = HISTORY_PAGE_MAX_BYTES)
1223
1264
  return new TextDecoder().decode(joined)
1224
1265
  }
1225
1266
 
1267
+ // Reads the session's current configuration revision over the HTTP read
1268
+ // channel. Configuration writes must carry it, and the value changes whenever
1269
+ // any client edits the session.
1270
+ async function readSessionConfigurationRevision({ httpBase, ticket, sessionId, signal }) {
1271
+ const timeout = AbortSignal.any([AbortSignal.timeout(20_000), ...(signal ? [signal] : [])])
1272
+ const result = await readSessionHistoryPage({ httpBase, ticket, sessionId, limit: 1, signal: timeout })
1273
+ const revision = result.session?.configurationRevision
1274
+ return Number.isSafeInteger(revision) ? revision : undefined
1275
+ }
1276
+
1226
1277
  async function readSessionHistoryPage({ httpBase, ticket, sessionId, cursor, direction, limit = 50, signal }) {
1227
1278
  const params = { sessionId, limit, ...(cursor ? { cursor, direction } : {}) }
1228
1279
  const response = await fetch(`${httpBase.endsWith('/') ? httpBase : `${httpBase}/`}read/session`, {
@@ -1,7 +1,7 @@
1
1
  const MESSAGE_UPDATES = Object.freeze({
2
- user_message_chunk: { role: 'user', label: 'You', mode: 'append' },
2
+ user_message_chunk: { role: 'user', label: '我', mode: 'append' },
3
3
  agent_message_chunk: { role: 'assistant', label: 'MonkeyAI', mode: 'append' },
4
- user_message: { role: 'user', label: 'You', mode: 'replace' },
4
+ user_message: { role: 'user', label: '我', mode: 'replace' },
5
5
  agent_message: { role: 'assistant', label: 'MonkeyAI', mode: 'replace' },
6
6
  })
7
7
 
@@ -11,7 +11,7 @@ const THOUGHT_UPDATES = Object.freeze({
11
11
  })
12
12
 
13
13
  export const TOOL_STATUS_LABEL = Object.freeze({
14
- pending: 'queued', in_progress: 'running', completed: 'done', failed: 'failed', cancelled: 'cancelled',
14
+ pending: '排队中', in_progress: '执行中', completed: '完成', failed: '失败', cancelled: '已取消',
15
15
  })
16
16
 
17
17
  function contentText(content) {
@@ -124,20 +124,20 @@ function projectFrames(frames) {
124
124
  entries.push({ kind: 'raw', label: kind || 'session/update', data: update, key: `raw:${entries.length}` })
125
125
  continue
126
126
  }
127
- if (frame.type === 'ready') { if (!seenReady) { seenReady = true; entries.push({ kind: 'system', label: 'Connected to remote runtime', key: `sys-ready:${entries.length}` }) } continue }
128
- if (frame.type === 'history_loaded') { if (!seenHistoryLoaded) { seenHistoryLoaded = true; entries.push({ kind: 'system', label: 'History loaded', key: `sys-hist:${entries.length}` }) } continue }
129
- if (frame.type === 'history_incomplete') { entries.push({ kind: 'system', label: 'History incomplete - some earlier messages may be missing', key: `sys-inc:${entries.length}` }); continue }
130
- if (frame.type === 'reconnecting') { if (!seenReconnecting) { seenReconnecting = true; entries.push({ kind: 'system', label: frame.message || 'Reconnecting to remote runtime', key: `sys-reconn:${entries.length}` }) } continue }
127
+ if (frame.type === 'ready') { if (!seenReady) { seenReady = true; entries.push({ kind: 'system', label: '已连接到远程 runtime', key: `sys-ready:${entries.length}` }) } continue }
128
+ if (frame.type === 'history_loaded') { if (!seenHistoryLoaded) { seenHistoryLoaded = true; entries.push({ kind: 'system', label: '历史已加载', key: `sys-hist:${entries.length}` }) } continue }
129
+ if (frame.type === 'history_incomplete') { entries.push({ kind: 'system', label: '历史不完整,部分更早的消息可能缺失', key: `sys-inc:${entries.length}` }); continue }
130
+ if (frame.type === 'reconnecting') { if (!seenReconnecting) { seenReconnecting = true; entries.push({ kind: 'system', label: frame.message || '正在重连远程 runtime', key: `sys-reconn:${entries.length}` }) } continue }
131
131
  if (frame.type === 'reconnected') { continue }
132
132
  if (frame.type === 'delivery') {
133
133
  const accepted = Boolean(frame.data?.accepted)
134
134
  if (accepted) continue // turn completion is shown in the composer status line
135
- entries.push({ kind: 'system', label: 'MonkeyAI did not accept the instruction', key: `sys-del:${entries.length}` })
135
+ entries.push({ kind: 'system', label: 'MonkeyAI 未接受该指令', key: `sys-del:${entries.length}` })
136
136
  continue
137
137
  }
138
- if (frame.type === 'cancel_sent') { entries.push({ kind: 'system', label: 'Stop requested', key: `sys-cancel:${entries.length}` }); continue }
138
+ if (frame.type === 'cancel_sent') { entries.push({ kind: 'system', label: '已请求停止', key: `sys-cancel:${entries.length}` }); continue }
139
139
  if (frame.type === 'request') { continue } // rendered as an approval card from pendingRequests
140
- if (frame.type === 'model_switched') { entries.push({ kind: 'system', label: `Model switched to ${frame.data?.model ?? 'selected model'}`, key: `sys-model:${entries.length}` }); continue }
140
+ if (frame.type === 'model_switched') { entries.push({ kind: 'system', label: `已切换模型:${frame.data?.model ?? '所选模型'}`, key: `sys-model:${entries.length}` }); continue }
141
141
  if (frame.type === 'model_switch_failed') { continue } // surfaced via the error banner
142
142
  if (frame.type === 'notification' && typeof frame.method === 'string' && frame.method.startsWith('_monkeyai/')) { continue } // internal runtime state notifications
143
143
  if (frame.type === 'error') { entries.push({ kind: 'raw', label: frame.code || 'error', data: { message: frame.message || frame.code }, key: `sys-err:${entries.length}` }); continue }