@khalilgharbaoui/opencode-claude-code-plugin 0.62.1 → 0.63.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/dist/index.js CHANGED
@@ -2045,6 +2045,10 @@ function clearAccountFailoverQuestions(sessionKey2) {
2045
2045
  if (key.startsWith(prefix)) pendingQuestions2.delete(key);
2046
2046
  }
2047
2047
  }
2048
+ function hasAccountFailoverQuestions(sessionKey2) {
2049
+ const prefix = `${sessionKey2}${KEY_SEPARATOR2}`;
2050
+ return [...pendingQuestions2.keys()].some((key) => key.startsWith(prefix));
2051
+ }
2048
2052
  function describeReset(resetsAt) {
2049
2053
  return formatResetsAt(resetsAt);
2050
2054
  }
@@ -3164,6 +3168,10 @@ function clearSubagentDispatchQuestions(sessionKey2) {
3164
3168
  if (key.startsWith(prefix)) pending.delete(key);
3165
3169
  }
3166
3170
  }
3171
+ function hasPendingSubagentDispatch(sessionKey2) {
3172
+ const prefix = `${sessionKey2}${KEY_SEPARATOR3}`;
3173
+ return [...pending.keys()].some((key) => key.startsWith(prefix));
3174
+ }
3167
3175
  function isSubagentDispatchActive(input) {
3168
3176
  if (input.compactionMode) return false;
3169
3177
  if (input.childSession) return false;
@@ -5833,6 +5841,9 @@ function isPendingProxyCallChannelClosed(call) {
5833
5841
  function findPendingProxyCall(toolCallId) {
5834
5842
  return pendingByCallId.get(toolCallId);
5835
5843
  }
5844
+ function hasAnyPendingProxyCall() {
5845
+ return pendingByCallId.size > 0;
5846
+ }
5836
5847
  function getPendingProxyCalls(sessionKey2) {
5837
5848
  const s = callIdsBySession.get(sessionKey2);
5838
5849
  if (!s || s.size === 0) return [];
@@ -6074,6 +6085,10 @@ function noteExplicitTaskModels(parentSessionId, tasks, now = Date.now()) {
6074
6085
  }
6075
6086
  return noted;
6076
6087
  }
6088
+ function hasUnclaimedExplicitTaskModels(now = Date.now()) {
6089
+ pruneClaims2(now);
6090
+ return claims2.length > 0;
6091
+ }
6077
6092
  function explicitTaskModelFor(sessionId) {
6078
6093
  return sessionModels.get(sessionId);
6079
6094
  }
@@ -7895,6 +7910,12 @@ function noteRelayMemberServing() {
7895
7910
  ownServed = true;
7896
7911
  join10();
7897
7912
  }
7913
+ function relayMemberHasServed() {
7914
+ return ownServed;
7915
+ }
7916
+ function leaveSessionEventRelay() {
7917
+ leave();
7918
+ }
7898
7919
  function openRelayFollower() {
7899
7920
  const state = relayState();
7900
7921
  join10();
@@ -8327,6 +8348,10 @@ function setClaudeSessionId(key, sessionId) {
8327
8348
  capClaudeSessions();
8328
8349
  }
8329
8350
  function deleteClaudeSessionId(key) {
8351
+ forgetClaudeSessionInMemory(key);
8352
+ forgetResumePoint(key);
8353
+ }
8354
+ function forgetClaudeSessionInMemory(key) {
8330
8355
  clearExitPlanModeQuestions(key);
8331
8356
  clearAccountFailoverQuestions(key);
8332
8357
  clearSubagentDispatchQuestions(key);
@@ -8335,7 +8360,52 @@ function deleteClaudeSessionId(key) {
8335
8360
  claudeSessions.delete(key);
8336
8361
  forgetForkFingerprint(key);
8337
8362
  forgetServedConversation(key);
8338
- forgetResumePoint(key);
8363
+ }
8364
+ function keyBelongsToSession(key, sessionID, ap) {
8365
+ return ap?.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
8366
+ }
8367
+ function keyHeldHere(key) {
8368
+ return claudeSessionIsWriting(key) || hasAccountFailoverQuestions(key) || hasPendingSubagentDispatch(key);
8369
+ }
8370
+ function sessionHeldHere(sessionID) {
8371
+ if (!sessionID || sessionID === "default") return false;
8372
+ for (const [key, ap] of activeProcesses) {
8373
+ if (keyBelongsToSession(key, sessionID, ap) && keyHeldHere(key)) return true;
8374
+ }
8375
+ for (const key of claudeSessions.keys()) {
8376
+ if (keyBelongsToSession(key, sessionID) && keyHeldHere(key)) return true;
8377
+ }
8378
+ return false;
8379
+ }
8380
+ function noWorkInFlightHere() {
8381
+ for (const ap of activeProcesses.values()) {
8382
+ if (isTurnInFlight(ap)) return false;
8383
+ }
8384
+ return !hasAnyPendingProxyCall() && !hasUnclaimedExplicitTaskModels();
8385
+ }
8386
+ function holdsNoClaudeState() {
8387
+ return activeProcesses.size === 0 && claudeSessions.size === 0 && !hasAnyPendingProxyCall();
8388
+ }
8389
+ async function releaseIdleForHandover(sessionID) {
8390
+ if (sessionID === "default") return [];
8391
+ const belongs = (key, ap) => sessionID === void 0 || keyBelongsToSession(key, sessionID, ap);
8392
+ const released = [];
8393
+ const exits = [];
8394
+ for (const [key, ap] of [...activeProcesses]) {
8395
+ if (!belongs(key, ap) || keyHeldHere(key)) continue;
8396
+ exits.push(deleteActiveProcessAndWait(key));
8397
+ released.push(key);
8398
+ }
8399
+ await Promise.all(exits);
8400
+ for (const key of [...claudeSessions.keys()]) {
8401
+ if (!belongs(key) || keyHeldHere(key)) continue;
8402
+ forgetClaudeSessionInMemory(key);
8403
+ if (!released.includes(key)) released.push(key);
8404
+ }
8405
+ if (released.length > 0) {
8406
+ log.info("handed claude sessions to another copy of the plugin", { sessionID, released });
8407
+ }
8408
+ return released;
8339
8409
  }
8340
8410
  function transferClaudeSession(fromKey, toKey) {
8341
8411
  if (fromKey === toKey) return void 0;
@@ -9166,6 +9236,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
9166
9236
  "agent maxTurns refused: not a positive whole number",
9167
9237
  "agent model override refused: unknown model",
9168
9238
  "agent prompt cache ttl refused: unknown value",
9239
+ "an older plugin build failed to release its claude sessions",
9169
9240
  "an opencode read deny rule could not be mapped onto Claude's own Read; that path stays readable by the CLI's own file tools",
9170
9241
  "auto-continuation stopped",
9171
9242
  "auto-continuing incomplete claude result",
@@ -9312,6 +9383,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
9312
9383
  "task_batch resolving with child results missing",
9313
9384
  "the interactive transport has not been measured on this Claude Code version; it reads the TUI's screen and transcript, which a new release can change. If a turn hangs or a dialog is not handled, run /claude-code-doctor bundle and report it",
9314
9385
  "the interactive turn did not take a forwarded user message",
9386
+ "the plugin build chosen for this turn could not take it; running it on this one",
9315
9387
  "this opencode process is running an older plugin build than the one on disk",
9316
9388
  "timed out pending proxy call",
9317
9389
  "turn abort listener threw",
@@ -10017,7 +10089,7 @@ function emptyMapping(enabled) {
10017
10089
  }
10018
10090
  function mapReadRules(rules, options = {}) {
10019
10091
  const deny = [];
10020
- const ask = [];
10092
+ const ask2 = [];
10021
10093
  const unmapped = [];
10022
10094
  const shadowedAllows = [];
10023
10095
  const seen = /* @__PURE__ */ new Set();
@@ -10025,7 +10097,7 @@ function mapReadRules(rules, options = {}) {
10025
10097
  rules.forEach((rule, index) => {
10026
10098
  if (rule.effect === "ask") {
10027
10099
  if (supersededBy(index, rule.pattern, () => true)) return;
10028
- if (!ask.includes(rule.pattern)) ask.push(rule.pattern);
10100
+ if (!ask2.includes(rule.pattern)) ask2.push(rule.pattern);
10029
10101
  return;
10030
10102
  }
10031
10103
  if (rule.effect !== "deny") return;
@@ -10046,7 +10118,7 @@ function mapReadRules(rules, options = {}) {
10046
10118
  if (!shadowedAllows.includes(later.pattern)) shadowedAllows.push(later.pattern);
10047
10119
  }
10048
10120
  });
10049
- return { deny, ask, unmapped, shadowedAllows };
10121
+ return { deny, ask: ask2, unmapped, shadowedAllows };
10050
10122
  }
10051
10123
  function resolveReadRuleMapping(input) {
10052
10124
  if (input.enabled === false) return emptyMapping(false);
@@ -10801,6 +10873,159 @@ import * as fs7 from "fs";
10801
10873
  import * as path10 from "path";
10802
10874
  import { fileURLToPath as fileURLToPath2 } from "url";
10803
10875
 
10876
+ // src/plugin-copies.ts
10877
+ var ROUTED_TURN_LIMIT_MS = 2 * 60 * 6e4;
10878
+ var COPIES_KEY = /* @__PURE__ */ Symbol.for("@khalilgharbaoui/opencode-claude-code-plugin/copies/v1");
10879
+ function copiesState() {
10880
+ const holder = globalThis;
10881
+ let state = holder[COPIES_KEY];
10882
+ if (!state) {
10883
+ state = { copies: [], active: void 0, switching: void 0, turns: /* @__PURE__ */ new Map() };
10884
+ holder[COPIES_KEY] = state;
10885
+ }
10886
+ return state;
10887
+ }
10888
+ function routedTurns(state, now) {
10889
+ for (const [token, turn] of state.turns) {
10890
+ if (now - turn.startedAt > ROUTED_TURN_LIMIT_MS) state.turns.delete(token);
10891
+ }
10892
+ return [...state.turns.values()];
10893
+ }
10894
+ function ask(question, fallback) {
10895
+ try {
10896
+ return question();
10897
+ } catch {
10898
+ return fallback;
10899
+ }
10900
+ }
10901
+ async function releaseQuietly(copy, sessionID) {
10902
+ try {
10903
+ await copy.release(sessionID);
10904
+ } catch (err) {
10905
+ log.warn("an older plugin build failed to release its claude sessions", {
10906
+ version: copy.version,
10907
+ error: err instanceof Error ? err.message : String(err)
10908
+ });
10909
+ }
10910
+ }
10911
+ function registerPluginCopy(copy) {
10912
+ const state = copiesState();
10913
+ if (state.copies.includes(copy)) return;
10914
+ state.copies.push(copy);
10915
+ state.active ??= copy;
10916
+ }
10917
+ function unregisterPluginCopy(copy) {
10918
+ const state = copiesState();
10919
+ const index = state.copies.indexOf(copy);
10920
+ if (index !== -1) state.copies.splice(index, 1);
10921
+ if (state.active === copy) state.active = void 0;
10922
+ }
10923
+ function isActivePluginCopy(copy) {
10924
+ return copiesState().active === copy;
10925
+ }
10926
+ function processLoadedBuild(build) {
10927
+ return copiesState().copies.some(
10928
+ (copy) => !copy.disposed && copy.version === build.version && copy.build.entryPath === build.entryPath && copy.build.mtimeMs === build.mtimeMs && copy.build.size === build.size
10929
+ );
10930
+ }
10931
+ function switchWhenQuiet(state, now) {
10932
+ if (state.switching) return;
10933
+ const newest = [...state.copies].reverse().find((copy) => !copy.disposed);
10934
+ if (!newest || newest === state.active) return;
10935
+ const others = state.copies.filter((copy) => copy !== newest);
10936
+ const busy = new Set(routedTurns(state, now).map((turn) => turn.copy));
10937
+ if (!others.every((copy) => !busy.has(copy) && ask(() => copy.quiet(), false))) return;
10938
+ const previous = state.active;
10939
+ state.active = newest;
10940
+ log.info("switched to the newest plugin build", {
10941
+ from: previous?.version,
10942
+ to: newest.version
10943
+ });
10944
+ state.switching = Promise.all(others.map((copy) => releaseQuietly(copy))).then(() => {
10945
+ state.switching = void 0;
10946
+ });
10947
+ }
10948
+ function servingCopy(state, sessionID, own) {
10949
+ if (sessionID) {
10950
+ const holder = state.copies.find((copy) => ask(() => copy.holds(sessionID), false));
10951
+ if (holder) return holder;
10952
+ }
10953
+ return state.active ?? own;
10954
+ }
10955
+ function followStream(source, end) {
10956
+ const reader = source.getReader();
10957
+ return new ReadableStream({
10958
+ async pull(controller) {
10959
+ try {
10960
+ const { done, value } = await reader.read();
10961
+ if (done) {
10962
+ end();
10963
+ controller.close();
10964
+ return;
10965
+ }
10966
+ controller.enqueue(value);
10967
+ } catch (err) {
10968
+ end();
10969
+ controller.error(err);
10970
+ }
10971
+ },
10972
+ async cancel(reason) {
10973
+ end();
10974
+ await reader.cancel(reason);
10975
+ }
10976
+ });
10977
+ }
10978
+ function untilTurnEnds(result, end) {
10979
+ const stream = result?.stream;
10980
+ if (!(stream instanceof ReadableStream)) {
10981
+ end();
10982
+ return result;
10983
+ }
10984
+ return { ...result, stream: followStream(stream, end) };
10985
+ }
10986
+ async function routeTurn(input) {
10987
+ const state = copiesState();
10988
+ if (state.copies.length === 0) return input.runHere();
10989
+ const now = input.now ?? Date.now;
10990
+ if (state.switching) await state.switching;
10991
+ switchWhenQuiet(state, now());
10992
+ if (state.switching) await state.switching;
10993
+ const sessionID = input.sessionID && input.sessionID !== "default" ? input.sessionID : void 0;
10994
+ const target = servingCopy(state, sessionID, input.own);
10995
+ const token = {};
10996
+ state.turns.set(token, { copy: target, sessionID, startedAt: now() });
10997
+ let ended = false;
10998
+ const end = () => {
10999
+ if (ended) return;
11000
+ ended = true;
11001
+ state.turns.delete(token);
11002
+ };
11003
+ try {
11004
+ if (sessionID) {
11005
+ await Promise.all(
11006
+ state.copies.filter((copy) => copy !== target).map((copy) => releaseQuietly(copy, sessionID))
11007
+ );
11008
+ }
11009
+ if (target === input.own) return untilTurnEnds(await input.runHere(), end);
11010
+ let model;
11011
+ try {
11012
+ model = await target.modelFor(input.turn);
11013
+ } catch (err) {
11014
+ log.warn("the plugin build chosen for this turn could not take it; running it on this one", {
11015
+ version: target.version,
11016
+ error: err instanceof Error ? err.message : String(err)
11017
+ });
11018
+ const turn = state.turns.get(token);
11019
+ if (turn) turn.copy = input.own;
11020
+ return untilTurnEnds(await input.runHere(), end);
11021
+ }
11022
+ return untilTurnEnds(await input.runOn(model), end);
11023
+ } catch (err) {
11024
+ end();
11025
+ throw err;
11026
+ }
11027
+ }
11028
+
10804
11029
  // src/startup-diagnostics.ts
10805
11030
  import { execFile as execFile3 } from "child_process";
10806
11031
  import * as fs6 from "fs";
@@ -11065,6 +11290,8 @@ function createStaleBuildWatch(io) {
11065
11290
  const warned = /* @__PURE__ */ new Set();
11066
11291
  let checkedAt;
11067
11292
  let cached2 = null;
11293
+ let cachedOnDisk;
11294
+ const loadedHere = (onDisk) => onDisk !== void 0 && io.loadedElsewhere?.(onDisk) === true;
11068
11295
  const read = () => {
11069
11296
  let onDisk;
11070
11297
  try {
@@ -11082,18 +11309,21 @@ function createStaleBuildWatch(io) {
11082
11309
  },
11083
11310
  check() {
11084
11311
  const now = io.now();
11085
- if (checkedAt !== void 0 && now - checkedAt < throttleMs) return cached2;
11312
+ if (checkedAt !== void 0 && now - checkedAt < throttleMs) {
11313
+ return cached2 && !loadedHere(cachedOnDisk) ? cached2 : null;
11314
+ }
11086
11315
  checkedAt = now;
11087
11316
  const status = read();
11088
- cached2 = status.stale;
11089
- if (status.stale && status.onDisk) {
11090
- const identity = `${status.stale.kind}:${status.onDisk.version}:${status.onDisk.mtimeMs}:${status.onDisk.size}`;
11317
+ cachedOnDisk = status.onDisk;
11318
+ cached2 = status.stale && loadedHere(status.onDisk) ? null : status.stale;
11319
+ if (cached2 && status.onDisk) {
11320
+ const identity = `${cached2.kind}:${status.onDisk.version}:${status.onDisk.mtimeMs}:${status.onDisk.size}`;
11091
11321
  if (!warned.has(identity)) {
11092
11322
  warned.add(identity);
11093
11323
  warn({
11094
- kind: status.stale.kind,
11095
- loadedVersion: status.stale.loadedVersion,
11096
- onDiskVersion: status.stale.onDiskVersion,
11324
+ kind: cached2.kind,
11325
+ loadedVersion: cached2.loadedVersion,
11326
+ onDiskVersion: cached2.onDiskVersion,
11097
11327
  path: io.loaded.entryPath
11098
11328
  });
11099
11329
  }
@@ -11152,7 +11382,8 @@ function staleBuildWatch() {
11152
11382
  watch = createStaleBuildWatch({
11153
11383
  loaded,
11154
11384
  readOnDisk: () => readOnDiskBuild(loaded.entryPath),
11155
- now: () => Date.now()
11385
+ now: () => Date.now(),
11386
+ loadedElsewhere: (onDisk) => processLoadedBuild({ entryPath: loaded.entryPath, ...onDisk })
11156
11387
  });
11157
11388
  }
11158
11389
  return watch;
@@ -20900,9 +21131,97 @@ function handleSessionEventHere(event) {
20900
21131
  const stopped = stoppedSessionIdV2(event);
20901
21132
  if (stopped) releaseParkedProxyCallsForSession(stopped);
20902
21133
  }
21134
+ var ownCopy;
21135
+ var liveSetups = 0;
21136
+ var plannedByDirectory = /* @__PURE__ */ new Map();
21137
+ var forwardedProviders = /* @__PURE__ */ new Map();
21138
+ var MAX_FORWARDED_PROVIDERS = 32;
21139
+ function plannedSettings(directory, providerID) {
21140
+ const here = plannedByDirectory.get(directory)?.get(providerID);
21141
+ if (here) return here;
21142
+ for (const planned of plannedByDirectory.values()) {
21143
+ const found = planned.get(providerID);
21144
+ if (found) return found;
21145
+ }
21146
+ return void 0;
21147
+ }
21148
+ function pluginCopyFor(deps) {
21149
+ if (ownCopy) return ownCopy;
21150
+ const loaded = staleBuildWatch().loaded;
21151
+ ownCopy = {
21152
+ id: randomUUID8(),
21153
+ version: loaded.version,
21154
+ build: { entryPath: loaded.entryPath, mtimeMs: loaded.mtimeMs, size: loaded.size },
21155
+ disposed: false,
21156
+ holds: (sessionID) => sessionHeldHere(sessionID),
21157
+ quiet: () => noWorkInFlightHere(),
21158
+ release: (sessionID) => releaseIdleForHandover(sessionID),
21159
+ async modelFor(turn) {
21160
+ const planned = plannedSettings(turn.directory, turn.providerID);
21161
+ if (!planned) throw new Error(`no provider ${turn.providerID} is planned in this build`);
21162
+ const cacheKey = JSON.stringify([turn.directory ?? null, turn.providerID, turn.sdkOptions]);
21163
+ let provider = forwardedProviders.get(cacheKey);
21164
+ if (!provider) {
21165
+ const settings = await resolveSdkSettings(turn.providerID, planned, turn.sdkOptions);
21166
+ provider = deps.createProvider(settings);
21167
+ forwardedProviders.set(cacheKey, provider);
21168
+ while (forwardedProviders.size > MAX_FORWARDED_PROVIDERS) {
21169
+ const oldest = forwardedProviders.keys().next().value;
21170
+ if (oldest === void 0) break;
21171
+ forwardedProviders.delete(oldest);
21172
+ }
21173
+ }
21174
+ return provider.languageModel(turn.modelId);
21175
+ }
21176
+ };
21177
+ return ownCopy;
21178
+ }
21179
+ function retireWhenSpent() {
21180
+ const copy = ownCopy;
21181
+ if (!copy?.disposed || isActivePluginCopy(copy) || !holdsNoClaudeState()) return;
21182
+ unregisterPluginCopy(copy);
21183
+ leaveSessionEventRelay();
21184
+ log.info("a disposed plugin build left the process", { version: copy.version });
21185
+ }
21186
+ function routedModel(model, copy, turn) {
21187
+ const sessionOf = (options) => resolveSessionAffinity(
21188
+ options.headers,
21189
+ options.providerOptions,
21190
+ turn.providerID
21191
+ );
21192
+ return {
21193
+ specificationVersion: model.specificationVersion,
21194
+ provider: model.provider,
21195
+ modelId: model.modelId,
21196
+ supportedUrls: model.supportedUrls,
21197
+ doStream: (options) => routeTurn({
21198
+ own: copy,
21199
+ turn,
21200
+ sessionID: sessionOf(options),
21201
+ runHere: () => model.doStream(options),
21202
+ runOn: (other) => other.doStream(options)
21203
+ }),
21204
+ doGenerate: (options) => routeTurn({
21205
+ own: copy,
21206
+ turn,
21207
+ sessionID: sessionOf(options),
21208
+ runHere: () => model.doGenerate(options),
21209
+ runOn: (other) => other.doGenerate(options)
21210
+ })
21211
+ };
21212
+ }
21213
+ function routedProvider(provider, copy, base) {
21214
+ const languageModel = (modelId) => routedModel(provider.languageModel(modelId), copy, { ...base, modelId });
21215
+ return Object.assign((modelId) => languageModel(modelId), provider, {
21216
+ languageModel
21217
+ });
21218
+ }
20903
21219
  var closeOwnFeed;
20904
21220
  defineOwnRelayMember({
20905
- handle: (event) => handleSessionEventHere(event),
21221
+ handle: (event) => {
21222
+ handleSessionEventHere(event);
21223
+ retireWhenSpent();
21224
+ },
20906
21225
  feedOpened: () => {
20907
21226
  closeOwnFeed ??= openSessionRunEventFeed();
20908
21227
  },
@@ -21015,6 +21334,11 @@ function createV2Setup(deps) {
21015
21334
  }
21016
21335
  }
21017
21336
  const planned = /* @__PURE__ */ new Map();
21337
+ const copy = pluginCopyFor(deps);
21338
+ copy.disposed = false;
21339
+ registerPluginCopy(copy);
21340
+ liveSetups += 1;
21341
+ plannedByDirectory.set(directory, planned);
21018
21342
  registrations.push(
21019
21343
  await ctx.provider.transform((editor) => {
21020
21344
  const seed = editor.get(BASE_PROVIDER_ID);
@@ -21058,7 +21382,11 @@ function createV2Setup(deps) {
21058
21382
  const providerID = event.model.providerID;
21059
21383
  if (!isClaudeCodeProviderId(providerID)) return;
21060
21384
  const settings = await resolveSdkSettings(providerID, planned.get(providerID), event.options);
21061
- event.sdk = deps.createProvider(settings);
21385
+ event.sdk = routedProvider(deps.createProvider(settings), copy, {
21386
+ directory,
21387
+ providerID,
21388
+ sdkOptions: event.options
21389
+ });
21062
21390
  log.debug("v2 sdk created", { providerID, cliPath: settings.cliPath });
21063
21391
  })
21064
21392
  );
@@ -21125,6 +21453,11 @@ function createV2Setup(deps) {
21125
21453
  });
21126
21454
  return async () => {
21127
21455
  events.abort();
21456
+ liveSetups = Math.max(0, liveSetups - 1);
21457
+ if (liveSetups === 0) {
21458
+ copy.disposed = true;
21459
+ if (!relayMemberHasServed()) unregisterPluginCopy(copy);
21460
+ }
21128
21461
  for (const registration of registrations.splice(0)) {
21129
21462
  await registration.dispose().catch(() => void 0);
21130
21463
  }