@ganglion/xacpx 0.20.3 → 0.21.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/bridge/bridge-main.js +2 -0
- package/dist/channels/cli/provider.d.ts +17 -0
- package/dist/channels/plugin.d.ts +21 -0
- package/dist/channels/types.d.ts +16 -2
- package/dist/cli.js +340 -70
- package/dist/i18n/types.d.ts +1 -0
- package/dist/plugin-api.d.ts +4 -3
- package/dist/plugin-api.js +2 -0
- package/dist/sessions/session-resource-catalog.d.ts +96 -0
- package/dist/sessions/session-service.d.ts +30 -1
- package/dist/state/state-store.d.ts +23 -4
- package/dist/state/types.d.ts +8 -0
- package/package.json +2 -1
|
@@ -4889,6 +4889,7 @@ var init_channel_cli = __esm(() => {
|
|
|
4889
4889
|
channelAdded: (type) => `Channel ${type} added`,
|
|
4890
4890
|
cannotRemoveLastEnabled: "Cannot remove the last enabled channel.",
|
|
4891
4891
|
channelRemoved: (id) => `Channel ${id} removed`,
|
|
4892
|
+
channelRetirementFailed: (id, error) => `Channel ${id} retirement cleanup failed: ${error}`,
|
|
4892
4893
|
channelCredentialsCleared: (id) => `Removed stored credentials for channel ${id}`,
|
|
4893
4894
|
channelCredentialsClearFailed: (id, error) => `Channel ${id} removed, but clearing its stored credentials failed: ${error}`,
|
|
4894
4895
|
channelCredentialsKept: (id) => `Kept stored credentials for channel ${id} (--keep-credentials)`,
|
|
@@ -6015,6 +6016,7 @@ var init_channel_cli2 = __esm(() => {
|
|
|
6015
6016
|
channelAdded: (type) => `频道 ${type} 已添加`,
|
|
6016
6017
|
cannotRemoveLastEnabled: "不能删除最后一个启用的频道。",
|
|
6017
6018
|
channelRemoved: (id) => `频道 ${id} 已删除`,
|
|
6019
|
+
channelRetirementFailed: (id, error) => `频道 ${id} 退役清理失败:${error}`,
|
|
6018
6020
|
channelCredentialsCleared: (id) => `已移除频道 ${id} 的存储凭证`,
|
|
6019
6021
|
channelCredentialsClearFailed: (id, error) => `频道 ${id} 已删除,但清除其存储凭证失败:${error}`,
|
|
6020
6022
|
channelCredentialsKept: (id) => `已保留频道 ${id} 的存储凭证(--keep-credentials)`,
|
|
@@ -22,6 +22,18 @@ export interface ChannelCliIo {
|
|
|
22
22
|
promptText: (message: string) => Promise<string>;
|
|
23
23
|
promptSecret: (message: string) => Promise<string>;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Structured doctor finding from an optional channel/plugin diagnostic hook.
|
|
27
|
+
* Core renders these verbatim and must not interpret RMUX-specific codes.
|
|
28
|
+
*/
|
|
29
|
+
export type ChannelDoctorFindingLevel = "ok" | "warn" | "error" | "skip";
|
|
30
|
+
export interface ChannelDoctorFinding {
|
|
31
|
+
level: ChannelDoctorFindingLevel;
|
|
32
|
+
code: string;
|
|
33
|
+
message: string;
|
|
34
|
+
suggestion?: string;
|
|
35
|
+
details?: Record<string, string | number | boolean | null>;
|
|
36
|
+
}
|
|
25
37
|
export interface ChannelCliProvider {
|
|
26
38
|
type: string;
|
|
27
39
|
displayName: string;
|
|
@@ -31,6 +43,11 @@ export interface ChannelCliProvider {
|
|
|
31
43
|
validateConfig(config: ChannelRuntimeConfig): ChannelCliValidationIssue[];
|
|
32
44
|
renderSummary(config: ChannelRuntimeConfig): string[];
|
|
33
45
|
promptForMissingFields(input: ChannelCliInput, io: ChannelCliIo): Promise<ChannelCliInput>;
|
|
46
|
+
/**
|
|
47
|
+
* Optional read-only health probe for `xacpx doctor` / `xacpx plugin doctor`.
|
|
48
|
+
* Must not mutate registry, credentials, or kill resources.
|
|
49
|
+
*/
|
|
50
|
+
diagnose?(config: ChannelRuntimeConfig): Promise<ChannelDoctorFinding[]> | ChannelDoctorFinding[];
|
|
34
51
|
/**
|
|
35
52
|
* Optional: declares this plugin supports the `xacpx channel ... --account <id>`
|
|
36
53
|
* multi-bot CLI surface. Plugins that opt in must also implement
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import type { ChannelFactory } from "./create-channel.js";
|
|
2
2
|
import type { ChannelCliProvider } from "./cli/provider.js";
|
|
3
|
+
import type { ChannelRuntimeConfig } from "../config/types.js";
|
|
4
|
+
export type ChannelRetireDaemonState = "running" | "stopped" | "indeterminate";
|
|
5
|
+
/** Context passed to a plugin `retireChannel` hook from `channel disable|rm`. */
|
|
6
|
+
export interface ChannelRetireContext {
|
|
7
|
+
channel: ChannelRuntimeConfig;
|
|
8
|
+
reason: "disabled" | "removed";
|
|
9
|
+
print: (line: string) => void;
|
|
10
|
+
getDaemonStatus: () => Promise<{
|
|
11
|
+
state: ChannelRetireDaemonState;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export type ChannelRetireHook = (ctx: ChannelRetireContext) => Promise<void>;
|
|
3
15
|
export interface ChannelPluginDefinition {
|
|
4
16
|
/** Stable channel type used in channels[].type and chatKey prefix. */
|
|
5
17
|
type: string;
|
|
6
18
|
factory: ChannelFactory;
|
|
7
19
|
cliProvider?: ChannelCliProvider;
|
|
20
|
+
/**
|
|
21
|
+
* Optional CLI lifecycle hook for `xacpx channel disable|rm`.
|
|
22
|
+
* Invoked after plugins are loaded, via the per-type registry — core must
|
|
23
|
+
* not import the plugin npm package to retire resources.
|
|
24
|
+
*/
|
|
25
|
+
retireChannel?: ChannelRetireHook;
|
|
8
26
|
}
|
|
9
27
|
export declare function registerChannelPlugin(plugin: ChannelPluginDefinition): void;
|
|
28
|
+
export declare function getChannelRetireHook(type: string): ChannelRetireHook | undefined;
|
|
29
|
+
/** Dispatch `channel disable|rm` retirement to the loaded plugin for this type. */
|
|
30
|
+
export declare function invokeChannelRetireHook(ctx: ChannelRetireContext): Promise<void>;
|
package/dist/channels/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { AppLogger } from "../logging/app-logger.js";
|
|
|
5
5
|
import type { PendingFinalChunk } from "../weixin/messaging/quota-manager.js";
|
|
6
6
|
import type { PerfTracer } from "../perf/perf-tracer.js";
|
|
7
7
|
import type { SessionService } from "../sessions/session-service.js";
|
|
8
|
+
import type { SessionResourceCatalog } from "../sessions/session-resource-catalog.js";
|
|
8
9
|
import type { ActiveTurnRegistry } from "../sessions/active-turn-registry.js";
|
|
9
10
|
import type { Locale } from "../i18n/index.js";
|
|
10
11
|
import type { ControlService } from "../control/control-service.js";
|
|
@@ -79,6 +80,12 @@ export interface ChannelStartInput {
|
|
|
79
80
|
* channels ignore it.
|
|
80
81
|
*/
|
|
81
82
|
control?: ControlService;
|
|
83
|
+
/**
|
|
84
|
+
* Generic catalog of logical-session resources: immutable logical session
|
|
85
|
+
* IDs, internal/display aliases, authoritative workspace cwd, archived flag,
|
|
86
|
+
* and a lifecycle-event subscription. Optional: text-only channels ignore it.
|
|
87
|
+
*/
|
|
88
|
+
sessionResources?: SessionResourceCatalog;
|
|
82
89
|
}
|
|
83
90
|
export interface OrchestrationDeliveryCallbacks {
|
|
84
91
|
markTaskNoticeDelivered: (taskId: string, accountId: string) => Promise<void>;
|
|
@@ -114,6 +121,7 @@ export interface ConsumerLockOptions {
|
|
|
114
121
|
lockFilePath?: string;
|
|
115
122
|
onDiagnostic?: (event: string, context: Record<string, string | number | boolean | undefined>) => void | Promise<void>;
|
|
116
123
|
}
|
|
124
|
+
export type ChannelStopReason = "shutdown" | "disabled" | "removed" | "logout";
|
|
117
125
|
export interface MessageChannelRuntime {
|
|
118
126
|
id: string;
|
|
119
127
|
isLoggedIn(): boolean;
|
|
@@ -122,15 +130,21 @@ export interface MessageChannelRuntime {
|
|
|
122
130
|
* Destructive credential removal. Reached only via the explicit
|
|
123
131
|
* `xacpx logout` CLI path — never as part of a normal shutdown.
|
|
124
132
|
*/
|
|
125
|
-
logout(): void
|
|
133
|
+
logout(): void | Promise<void>;
|
|
126
134
|
start(input: ChannelStartInput): Promise<void>;
|
|
127
135
|
/**
|
|
128
136
|
* Non-destructive shutdown: release runtime resources without touching
|
|
129
137
|
* stored credentials. Optional for compatibility with already-published
|
|
130
138
|
* plugin channels; when absent, the registry falls back to `logout()`
|
|
131
139
|
* (which for those plugins is a benign client stop).
|
|
140
|
+
*
|
|
141
|
+
* @param reason - The reason for stopping:
|
|
142
|
+
* - "shutdown": normal signal/startup-error cleanup
|
|
143
|
+
* - "disabled": channel disabled via CLI
|
|
144
|
+
* - "removed": channel removed via CLI
|
|
145
|
+
* - "logout": explicit logout command
|
|
132
146
|
*/
|
|
133
|
-
stop?(): void | Promise<void>;
|
|
147
|
+
stop?(reason?: ChannelStopReason): void | Promise<void>;
|
|
134
148
|
createConsumerLock?(options?: ConsumerLockOptions): ConsumerLock;
|
|
135
149
|
configureOrchestration?(callbacks: OrchestrationDeliveryCallbacks): void;
|
|
136
150
|
notifyTaskCompletion(task: OrchestrationTaskRecord): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -927,6 +927,7 @@ var init_channel_cli = __esm(() => {
|
|
|
927
927
|
channelAdded: (type) => `Channel ${type} added`,
|
|
928
928
|
cannotRemoveLastEnabled: "Cannot remove the last enabled channel.",
|
|
929
929
|
channelRemoved: (id) => `Channel ${id} removed`,
|
|
930
|
+
channelRetirementFailed: (id, error) => `Channel ${id} retirement cleanup failed: ${error}`,
|
|
930
931
|
channelCredentialsCleared: (id) => `Removed stored credentials for channel ${id}`,
|
|
931
932
|
channelCredentialsClearFailed: (id, error) => `Channel ${id} removed, but clearing its stored credentials failed: ${error}`,
|
|
932
933
|
channelCredentialsKept: (id) => `Kept stored credentials for channel ${id} (--keep-credentials)`,
|
|
@@ -2053,6 +2054,7 @@ var init_channel_cli2 = __esm(() => {
|
|
|
2053
2054
|
channelAdded: (type) => `频道 ${type} 已添加`,
|
|
2054
2055
|
cannotRemoveLastEnabled: "不能删除最后一个启用的频道。",
|
|
2055
2056
|
channelRemoved: (id) => `频道 ${id} 已删除`,
|
|
2057
|
+
channelRetirementFailed: (id, error) => `频道 ${id} 退役清理失败:${error}`,
|
|
2056
2058
|
channelCredentialsCleared: (id) => `已移除频道 ${id} 的存储凭证`,
|
|
2057
2059
|
channelCredentialsClearFailed: (id, error) => `频道 ${id} 已删除,但清除其存储凭证失败:${error}`,
|
|
2058
2060
|
channelCredentialsKept: (id) => `已保留频道 ${id} 的存储凭证(--keep-credentials)`,
|
|
@@ -8203,7 +8205,7 @@ function createCoreRuntimeLock(options) {
|
|
|
8203
8205
|
try {
|
|
8204
8206
|
if (isBunRuntime() && !options.loadFsExt) {
|
|
8205
8207
|
await handle.close();
|
|
8206
|
-
posixHelper = await acquireFlockHelper(options.lockFilePath, options.helperAcquireTimeoutMs, options.helperHandshakeDelayMs);
|
|
8208
|
+
posixHelper = await acquireFlockHelper(options.lockFilePath, options.helperAcquireTimeoutMs, options.helperHandshakeDelayMs, onDiagnostic);
|
|
8207
8209
|
await emitDiagnostic(onDiagnostic, "lock_helper_started", {
|
|
8208
8210
|
lockFilePath: options.lockFilePath,
|
|
8209
8211
|
helperPid: posixHelper.pid
|
|
@@ -8301,11 +8303,17 @@ async function releasePosixLock(options, handle, helper) {
|
|
|
8301
8303
|
function isBunRuntime() {
|
|
8302
8304
|
return Boolean(globalThis.Bun);
|
|
8303
8305
|
}
|
|
8304
|
-
async function acquireFlockHelper(lockFilePath, timeoutMs = 1e4, handshakeDelayMs = 0) {
|
|
8306
|
+
async function acquireFlockHelper(lockFilePath, timeoutMs = 1e4, handshakeDelayMs = 0, onDiagnostic) {
|
|
8305
8307
|
const fsExtPath = require2.resolve("fs-ext");
|
|
8306
8308
|
const child = spawn7("node", ["-e", FLOCK_HELPER_SOURCE, fsExtPath, lockFilePath, String(handshakeDelayMs)], {
|
|
8307
8309
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8308
8310
|
});
|
|
8311
|
+
if (child.pid) {
|
|
8312
|
+
await emitDiagnostic(onDiagnostic, "lock_helper_spawned", {
|
|
8313
|
+
lockFilePath,
|
|
8314
|
+
helperPid: child.pid
|
|
8315
|
+
});
|
|
8316
|
+
}
|
|
8309
8317
|
const line = await new Promise((resolve5, reject) => {
|
|
8310
8318
|
let stdout = "";
|
|
8311
8319
|
let stderr = "";
|
|
@@ -29367,6 +29375,7 @@ function createEmptyState() {
|
|
|
29367
29375
|
var init_types4 = () => {};
|
|
29368
29376
|
|
|
29369
29377
|
// src/state/state-store.ts
|
|
29378
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
29370
29379
|
import { readFile as readFile10, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
|
|
29371
29380
|
function isRecord3(value) {
|
|
29372
29381
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -29619,6 +29628,9 @@ function parseOrchestrationState(raw, dropped) {
|
|
|
29619
29628
|
function isReplyMode2(value) {
|
|
29620
29629
|
return value === "stream" || value === "final" || value === "verbose";
|
|
29621
29630
|
}
|
|
29631
|
+
function isLogicalSessionId(value) {
|
|
29632
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
29633
|
+
}
|
|
29622
29634
|
function isSessionSource(value) {
|
|
29623
29635
|
return value === undefined || value === "weacpx" || value === "xacpx" || value === "agent-side";
|
|
29624
29636
|
}
|
|
@@ -29626,16 +29638,26 @@ function isSessionRecord(value) {
|
|
|
29626
29638
|
if (!isRecord3(value)) {
|
|
29627
29639
|
return false;
|
|
29628
29640
|
}
|
|
29629
|
-
return isString(value.alias) && isString(value.agent) && isString(value.workspace) && isString(value.transport_session) && isSessionSource(value.source) && isOptionalString(value.agent_session_id) && isOptionalString(value.agent_session_title) && isOptionalString(value.agent_session_updated_at) && isOptionalString(value.attached_at) && isOptionalString(value.transport_agent_command) && isOptionalString(value.transport_acpx_agent) && (value.transport_agent_argv === undefined || isStringArray(value.transport_agent_argv)) && isOptionalString(value.mode_id) && isOptionalString(value.effort) && (value.reply_mode === undefined || isReplyMode2(value.reply_mode)) && isString(value.created_at) && isString(value.last_used_at);
|
|
29641
|
+
return isString(value.alias) && isString(value.agent) && isString(value.workspace) && isString(value.transport_session) && (value.logical_session_id === undefined || isLogicalSessionId(value.logical_session_id)) && isSessionSource(value.source) && isOptionalString(value.agent_session_id) && isOptionalString(value.agent_session_title) && isOptionalString(value.agent_session_updated_at) && isOptionalString(value.attached_at) && isOptionalString(value.transport_agent_command) && isOptionalString(value.transport_acpx_agent) && (value.transport_agent_argv === undefined || isStringArray(value.transport_agent_argv)) && isOptionalString(value.mode_id) && isOptionalString(value.effort) && (value.reply_mode === undefined || isReplyMode2(value.reply_mode)) && isString(value.created_at) && isString(value.last_used_at);
|
|
29630
29642
|
}
|
|
29631
|
-
function parseSessions(raw, dropped) {
|
|
29643
|
+
function parseSessions(raw, dropped, migrated) {
|
|
29632
29644
|
const sessions = {};
|
|
29633
29645
|
for (const [alias, value] of Object.entries(raw)) {
|
|
29634
29646
|
if (!isSessionRecord(value)) {
|
|
29635
29647
|
dropped.push({ section: "sessions", key: alias, reason: "malformed session record" });
|
|
29636
29648
|
continue;
|
|
29637
29649
|
}
|
|
29638
|
-
|
|
29650
|
+
if (value.logical_session_id === undefined) {
|
|
29651
|
+
migrated.push({
|
|
29652
|
+
section: "sessions",
|
|
29653
|
+
key: alias,
|
|
29654
|
+
reason: "legacy record missing logical_session_id; assigned a new UUIDv4"
|
|
29655
|
+
});
|
|
29656
|
+
}
|
|
29657
|
+
sessions[alias] = {
|
|
29658
|
+
...value,
|
|
29659
|
+
logical_session_id: value.logical_session_id ?? randomUUID6()
|
|
29660
|
+
};
|
|
29639
29661
|
}
|
|
29640
29662
|
return sessions;
|
|
29641
29663
|
}
|
|
@@ -29700,11 +29722,11 @@ function parseScheduledTasks(raw, dropped) {
|
|
|
29700
29722
|
}
|
|
29701
29723
|
return tasks;
|
|
29702
29724
|
}
|
|
29703
|
-
function parseState(raw, path3, dropped = []) {
|
|
29725
|
+
function parseState(raw, path3, dropped = [], migrated = []) {
|
|
29704
29726
|
if (!isRecord3(raw)) {
|
|
29705
29727
|
throw new Error(`state file "${path3}" must contain a JSON object`);
|
|
29706
29728
|
}
|
|
29707
|
-
const parsedSessions = parseSessions(sectionRecord(raw.sessions, "sessions", dropped), dropped);
|
|
29729
|
+
const parsedSessions = parseSessions(sectionRecord(raw.sessions, "sessions", dropped), dropped, migrated);
|
|
29708
29730
|
const orchestration3 = parseOrchestrationState(raw.orchestration, dropped);
|
|
29709
29731
|
repairExternalCoordinatorIdentityCollisions(parsedSessions, orchestration3, dropped);
|
|
29710
29732
|
return {
|
|
@@ -29765,20 +29787,31 @@ class StateStore {
|
|
|
29765
29787
|
if (read.kind === "corrupt") {
|
|
29766
29788
|
return await this.recoverFromCorruptFile(read.reason);
|
|
29767
29789
|
}
|
|
29768
|
-
if (read.dropped.length === 0) {
|
|
29790
|
+
if (read.dropped.length === 0 && read.migrated.length === 0) {
|
|
29769
29791
|
return read.state;
|
|
29770
29792
|
}
|
|
29771
29793
|
const report = { dropped: read.dropped };
|
|
29772
|
-
|
|
29773
|
-
|
|
29774
|
-
|
|
29775
|
-
|
|
29776
|
-
|
|
29777
|
-
|
|
29794
|
+
if (read.migrated.length > 0) {
|
|
29795
|
+
report.migrated = read.migrated;
|
|
29796
|
+
}
|
|
29797
|
+
if (read.dropped.length > 0) {
|
|
29798
|
+
const quarantinePath = `${this.path}.quarantine-${this.fileTimestamp()}`;
|
|
29799
|
+
try {
|
|
29800
|
+
const written = await (this.options.writeBackup ?? defaultWriteBackup)(quarantinePath, read.content);
|
|
29801
|
+
report.quarantinePath = typeof written === "string" ? written : quarantinePath;
|
|
29802
|
+
} catch (error2) {
|
|
29803
|
+
report.backupError = error2 instanceof Error ? error2.message : String(error2);
|
|
29804
|
+
}
|
|
29805
|
+
}
|
|
29806
|
+
if (read.migrated.length > 0) {
|
|
29807
|
+
await this.persistMigration(read.state);
|
|
29778
29808
|
}
|
|
29779
29809
|
this.loadReport = report;
|
|
29780
29810
|
return read.state;
|
|
29781
29811
|
}
|
|
29812
|
+
async persistMigration(state) {
|
|
29813
|
+
await (this.options.writeMigration ?? ((next) => this.save(next)))(state);
|
|
29814
|
+
}
|
|
29782
29815
|
async inspect() {
|
|
29783
29816
|
const read = await this.readAndParse();
|
|
29784
29817
|
if (read.kind === "absent") {
|
|
@@ -29792,7 +29825,10 @@ class StateStore {
|
|
|
29792
29825
|
}
|
|
29793
29826
|
return {
|
|
29794
29827
|
state: read.state,
|
|
29795
|
-
report: read.dropped.length > 0
|
|
29828
|
+
report: read.dropped.length > 0 || read.migrated.length > 0 ? {
|
|
29829
|
+
dropped: read.dropped,
|
|
29830
|
+
...read.migrated.length > 0 ? { migrated: read.migrated } : {}
|
|
29831
|
+
} : null
|
|
29796
29832
|
};
|
|
29797
29833
|
}
|
|
29798
29834
|
async readAndParse() {
|
|
@@ -29821,7 +29857,8 @@ class StateStore {
|
|
|
29821
29857
|
return { kind: "corrupt", reason: "top-level value is not an object" };
|
|
29822
29858
|
}
|
|
29823
29859
|
const dropped = [];
|
|
29824
|
-
|
|
29860
|
+
const migrated = [];
|
|
29861
|
+
return { kind: "parsed", state: parseState(parsed, this.path, dropped, migrated), dropped, migrated, content };
|
|
29825
29862
|
}
|
|
29826
29863
|
async save(state) {
|
|
29827
29864
|
await writePrivateFileAtomic(this.path, JSON.stringify({ version: STATE_FILE_VERSION, ...state }, null, 2));
|
|
@@ -32272,7 +32309,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
32272
32309
|
});
|
|
32273
32310
|
|
|
32274
32311
|
// src/weixin/auth/login-qr.ts
|
|
32275
|
-
import { randomUUID as
|
|
32312
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
32276
32313
|
function isLoginFresh(login3) {
|
|
32277
32314
|
return Date.now() - login3.startedAt < ACTIVE_LOGIN_TTL_MS;
|
|
32278
32315
|
}
|
|
@@ -32375,7 +32412,7 @@ async function readVerifyCodeFromStdin(prompt) {
|
|
|
32375
32412
|
});
|
|
32376
32413
|
}
|
|
32377
32414
|
async function startWeixinLoginWithQr(opts) {
|
|
32378
|
-
const sessionKey = opts.accountId ||
|
|
32415
|
+
const sessionKey = opts.accountId || randomUUID7();
|
|
32379
32416
|
purgeExpiredLogins();
|
|
32380
32417
|
const existing = activeLogins.get(sessionKey);
|
|
32381
32418
|
if (!opts.force && existing && isLoginFresh(existing) && existing.qrcodeUrl) {
|
|
@@ -32398,7 +32435,7 @@ async function startWeixinLoginWithQr(opts) {
|
|
|
32398
32435
|
});
|
|
32399
32436
|
const login3 = {
|
|
32400
32437
|
sessionKey,
|
|
32401
|
-
id:
|
|
32438
|
+
id: randomUUID7(),
|
|
32402
32439
|
qrcode: qrResponse.qrcode,
|
|
32403
32440
|
qrcodeUrl: qrResponse.qrcode_img_content,
|
|
32404
32441
|
startedAt: Date.now()
|
|
@@ -36388,7 +36425,7 @@ var init_scheduled_turn = __esm(() => {
|
|
|
36388
36425
|
});
|
|
36389
36426
|
|
|
36390
36427
|
// src/weixin/monitor/consumer-lock.ts
|
|
36391
|
-
import { randomUUID as
|
|
36428
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
36392
36429
|
import { mkdir as mkdir10, open as open6, readFile as readFile12, rm as rm9, writeFile as writeFile7 } from "node:fs/promises";
|
|
36393
36430
|
import { dirname as dirname15, join as join17 } from "node:path";
|
|
36394
36431
|
import { homedir as homedir6 } from "node:os";
|
|
@@ -36412,7 +36449,7 @@ function createWeixinConsumerLock(options = {}) {
|
|
|
36412
36449
|
const existing = await loadLockMetadata(lockFilePath) ?? meta2;
|
|
36413
36450
|
throw new ActiveWeixinConsumerLockError(lockFilePath, existing);
|
|
36414
36451
|
}
|
|
36415
|
-
windowsLockId = meta2.lockId ??
|
|
36452
|
+
windowsLockId = meta2.lockId ?? randomUUID8();
|
|
36416
36453
|
const metadata = {
|
|
36417
36454
|
...meta2,
|
|
36418
36455
|
schemaVersion: 2,
|
|
@@ -36432,7 +36469,7 @@ function createWeixinConsumerLock(options = {}) {
|
|
|
36432
36469
|
return;
|
|
36433
36470
|
}
|
|
36434
36471
|
const requestedIdentity = await (options.probeProcessIdentity ?? probePosixProcessIdentity)(meta2.pid);
|
|
36435
|
-
posixLockId = meta2.lockId ??
|
|
36472
|
+
posixLockId = meta2.lockId ?? randomUUID8();
|
|
36436
36473
|
const requested = requestedIdentity.status === "found" ? { ...meta2, schemaVersion: 2, lockId: posixLockId, processStartedAtMs: requestedIdentity.identity.startedAtMs } : { ...meta2, schemaVersion: 2, lockId: posixLockId };
|
|
36437
36474
|
while (true) {
|
|
36438
36475
|
try {
|
|
@@ -36916,6 +36953,12 @@ var init_registry = __esm(() => {
|
|
|
36916
36953
|
});
|
|
36917
36954
|
|
|
36918
36955
|
// src/channels/plugin.ts
|
|
36956
|
+
var exports_plugin = {};
|
|
36957
|
+
__export(exports_plugin, {
|
|
36958
|
+
registerChannelPlugin: () => registerChannelPlugin,
|
|
36959
|
+
invokeChannelRetireHook: () => invokeChannelRetireHook,
|
|
36960
|
+
getChannelRetireHook: () => getChannelRetireHook
|
|
36961
|
+
});
|
|
36919
36962
|
function registerChannelPlugin(plugin) {
|
|
36920
36963
|
bootstrapBuiltinChannelFactories();
|
|
36921
36964
|
bootstrapBuiltinChannelCliProviders();
|
|
@@ -36930,10 +36973,23 @@ function registerChannelPlugin(plugin) {
|
|
|
36930
36973
|
registerChannelFactory(plugin.type, plugin.factory);
|
|
36931
36974
|
if (plugin.cliProvider)
|
|
36932
36975
|
registerChannelCliProvider(plugin.cliProvider);
|
|
36976
|
+
if (plugin.retireChannel)
|
|
36977
|
+
retireHooks.set(channelType, plugin.retireChannel);
|
|
36933
36978
|
}
|
|
36979
|
+
function getChannelRetireHook(type) {
|
|
36980
|
+
return retireHooks.get(type);
|
|
36981
|
+
}
|
|
36982
|
+
async function invokeChannelRetireHook(ctx) {
|
|
36983
|
+
const hook = retireHooks.get(ctx.channel.type);
|
|
36984
|
+
if (!hook)
|
|
36985
|
+
return;
|
|
36986
|
+
await hook(ctx);
|
|
36987
|
+
}
|
|
36988
|
+
var retireHooks;
|
|
36934
36989
|
var init_plugin = __esm(() => {
|
|
36935
36990
|
init_create_channel();
|
|
36936
36991
|
init_registry();
|
|
36992
|
+
retireHooks = new Map;
|
|
36937
36993
|
});
|
|
36938
36994
|
|
|
36939
36995
|
// src/plugins/compatibility.ts
|
|
@@ -38373,6 +38429,13 @@ async function removeChannel(type, rawArgs, deps) {
|
|
|
38373
38429
|
deps.print(t().channelCli.cannotRemoveLastEnabled);
|
|
38374
38430
|
return 1;
|
|
38375
38431
|
}
|
|
38432
|
+
if (deps.retireChannel) {
|
|
38433
|
+
try {
|
|
38434
|
+
await deps.retireChannel(channel, "removed");
|
|
38435
|
+
} catch (error2) {
|
|
38436
|
+
deps.print(t().channelCli.channelRetirementFailed(channel.id, error2 instanceof Error ? error2.message : String(error2)));
|
|
38437
|
+
}
|
|
38438
|
+
}
|
|
38376
38439
|
config4.channels = config4.channels.filter((entry) => entry.id !== channel.id);
|
|
38377
38440
|
await deps.saveChannels(config4.channels);
|
|
38378
38441
|
deps.print(t().channelCli.channelRemoved(channel.id));
|
|
@@ -38405,6 +38468,13 @@ async function setChannelEnabled(type, enabled, rawArgs, deps) {
|
|
|
38405
38468
|
deps.print(t().channelCli.cannotDisableLastEnabled);
|
|
38406
38469
|
return 1;
|
|
38407
38470
|
}
|
|
38471
|
+
if (!enabled && channel.enabled && deps.retireChannel) {
|
|
38472
|
+
try {
|
|
38473
|
+
await deps.retireChannel(channel, "disabled");
|
|
38474
|
+
} catch (error2) {
|
|
38475
|
+
deps.print(t().channelCli.channelRetirementFailed(channel.id, error2 instanceof Error ? error2.message : String(error2)));
|
|
38476
|
+
}
|
|
38477
|
+
}
|
|
38408
38478
|
channel.enabled = enabled;
|
|
38409
38479
|
await deps.saveChannels(config4.channels);
|
|
38410
38480
|
deps.print(t().channelCli.channelEnabledToggled(channel.id, enabled));
|
|
@@ -38770,6 +38840,16 @@ async function inspectPlugins(input) {
|
|
|
38770
38840
|
message: configPlugin.enabled ? `plugin is installed and valid; channels: ${channelTypes.length > 0 ? channelTypes.join(", ") : "none"}` : `plugin is installed and valid but disabled; run xacpx plugin enable ${configPlugin.name}`,
|
|
38771
38841
|
...configPlugin.enabled ? {} : { suggestion: `xacpx plugin enable ${configPlugin.name}` }
|
|
38772
38842
|
});
|
|
38843
|
+
if (configPlugin.enabled) {
|
|
38844
|
+
for (const finding of await collectChannelDiagnoseFindings(plugin.channels ?? [], input.config)) {
|
|
38845
|
+
pushIfRelevant({
|
|
38846
|
+
level: finding.level === "skip" ? "ok" : finding.level,
|
|
38847
|
+
plugin: configPlugin.name,
|
|
38848
|
+
message: finding.level === "skip" ? `${finding.code}: ${finding.message}` : `${finding.code}: ${finding.message}`,
|
|
38849
|
+
...finding.suggestion ? { suggestion: finding.suggestion } : {}
|
|
38850
|
+
});
|
|
38851
|
+
}
|
|
38852
|
+
}
|
|
38773
38853
|
} catch (error2) {
|
|
38774
38854
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
38775
38855
|
pushIfRelevant({ level: "error", plugin: configPlugin.name, message });
|
|
@@ -38804,6 +38884,28 @@ async function inspectPlugins(input) {
|
|
|
38804
38884
|
}
|
|
38805
38885
|
return issues;
|
|
38806
38886
|
}
|
|
38887
|
+
async function collectChannelDiagnoseFindings(channelDefs, config4) {
|
|
38888
|
+
const findings = [];
|
|
38889
|
+
for (const def of channelDefs) {
|
|
38890
|
+
const diagnose = def.cliProvider?.diagnose;
|
|
38891
|
+
if (!diagnose)
|
|
38892
|
+
continue;
|
|
38893
|
+
const runtimeChannels = (config4.channels ?? []).filter((channel) => channel.type === def.type && channel.enabled !== false);
|
|
38894
|
+
for (const channel of runtimeChannels) {
|
|
38895
|
+
try {
|
|
38896
|
+
const result = await diagnose(channel);
|
|
38897
|
+
findings.push(...result);
|
|
38898
|
+
} catch (error2) {
|
|
38899
|
+
findings.push({
|
|
38900
|
+
level: "error",
|
|
38901
|
+
code: "channel-diagnose-failed",
|
|
38902
|
+
message: `channel ${channel.type} diagnose hook failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
38903
|
+
});
|
|
38904
|
+
}
|
|
38905
|
+
}
|
|
38906
|
+
}
|
|
38907
|
+
return findings;
|
|
38908
|
+
}
|
|
38807
38909
|
var init_plugin_doctor = __esm(() => {
|
|
38808
38910
|
init_channel_scope();
|
|
38809
38911
|
init_plugin_loader();
|
|
@@ -49610,7 +49712,7 @@ function replaceRuntimeState(target, source) {
|
|
|
49610
49712
|
}
|
|
49611
49713
|
|
|
49612
49714
|
// src/sessions/session-service.ts
|
|
49613
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
49715
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID9 } from "node:crypto";
|
|
49614
49716
|
import { dirname as dirname19 } from "node:path";
|
|
49615
49717
|
|
|
49616
49718
|
class SessionService {
|
|
@@ -49622,6 +49724,7 @@ class SessionService {
|
|
|
49622
49724
|
platform;
|
|
49623
49725
|
runtimeRoot;
|
|
49624
49726
|
pendingSessionAliasOperations = new Set;
|
|
49727
|
+
lifecyclePublisher;
|
|
49625
49728
|
constructor(config4, stateStore, state, options = {}) {
|
|
49626
49729
|
this.config = config4;
|
|
49627
49730
|
this.stateStore = stateStore;
|
|
@@ -49667,6 +49770,7 @@ class SessionService {
|
|
|
49667
49770
|
agent: agent3,
|
|
49668
49771
|
workspace: workspace3,
|
|
49669
49772
|
transport_session: transportSession,
|
|
49773
|
+
logical_session_id: existing?.logical_session_id ?? randomUUID9(),
|
|
49670
49774
|
transport_agent_command: sameAgentExisting?.transport_agent_command,
|
|
49671
49775
|
transport_acpx_agent: sameAgentExisting?.transport_acpx_agent,
|
|
49672
49776
|
transport_agent_argv: sameAgentExisting?.transport_agent_argv,
|
|
@@ -49740,30 +49844,42 @@ class SessionService {
|
|
|
49740
49844
|
if (!session3) {
|
|
49741
49845
|
throw new Error(`session "${alias}" does not exist`);
|
|
49742
49846
|
}
|
|
49743
|
-
const prevCtx = this.state.chat_contexts[chatKey];
|
|
49744
|
-
const previousCurrent = prevCtx?.current_session;
|
|
49745
|
-
const carriedPrevious = previousCurrent && previousCurrent !== internalAlias ? previousCurrent : prevCtx?.previous_session;
|
|
49746
|
-
session3.last_used_at = new Date().toISOString();
|
|
49747
49847
|
if (session3.archived) {
|
|
49748
|
-
|
|
49749
|
-
|
|
49750
|
-
|
|
49751
|
-
|
|
49752
|
-
|
|
49753
|
-
nextCtx.previous_session = carriedPrevious;
|
|
49754
|
-
} else {
|
|
49755
|
-
delete nextCtx.previous_session;
|
|
49848
|
+
const nextState = structuredClone(this.state);
|
|
49849
|
+
const result2 = this.applyUseSession(nextState, chatKey, internalAlias);
|
|
49850
|
+
await this.commitLifecycleTransition(nextState);
|
|
49851
|
+
this.publishLifecycleEvent("restored", nextState.sessions[internalAlias]);
|
|
49852
|
+
return result2;
|
|
49756
49853
|
}
|
|
49757
|
-
this.state
|
|
49854
|
+
const result = this.applyUseSession(this.state, chatKey, internalAlias);
|
|
49758
49855
|
await this.persist();
|
|
49759
|
-
return
|
|
49760
|
-
alias: toDisplaySessionAlias(session3.alias),
|
|
49761
|
-
agent: session3.agent,
|
|
49762
|
-
workspace: session3.workspace,
|
|
49763
|
-
previousAlias: carriedPrevious ? toDisplaySessionAlias(carriedPrevious) : undefined
|
|
49764
|
-
};
|
|
49856
|
+
return result;
|
|
49765
49857
|
});
|
|
49766
49858
|
}
|
|
49859
|
+
applyUseSession(state, chatKey, internalAlias) {
|
|
49860
|
+
const session3 = state.sessions[internalAlias];
|
|
49861
|
+
const prevCtx = state.chat_contexts[chatKey];
|
|
49862
|
+
const previousCurrent = prevCtx?.current_session;
|
|
49863
|
+
const carriedPrevious = previousCurrent && previousCurrent !== internalAlias ? previousCurrent : prevCtx?.previous_session;
|
|
49864
|
+
session3.last_used_at = new Date().toISOString();
|
|
49865
|
+
if (session3.archived) {
|
|
49866
|
+
delete session3.archived;
|
|
49867
|
+
delete session3.archived_at;
|
|
49868
|
+
}
|
|
49869
|
+
const nextCtx = { ...prevCtx, current_session: internalAlias };
|
|
49870
|
+
if (carriedPrevious) {
|
|
49871
|
+
nextCtx.previous_session = carriedPrevious;
|
|
49872
|
+
} else {
|
|
49873
|
+
delete nextCtx.previous_session;
|
|
49874
|
+
}
|
|
49875
|
+
state.chat_contexts[chatKey] = nextCtx;
|
|
49876
|
+
return {
|
|
49877
|
+
alias: toDisplaySessionAlias(session3.alias),
|
|
49878
|
+
agent: session3.agent,
|
|
49879
|
+
workspace: session3.workspace,
|
|
49880
|
+
previousAlias: carriedPrevious ? toDisplaySessionAlias(carriedPrevious) : undefined
|
|
49881
|
+
};
|
|
49882
|
+
}
|
|
49767
49883
|
async usePreviousSession(chatKey) {
|
|
49768
49884
|
return await this.mutate(async () => {
|
|
49769
49885
|
const ctx = this.state.chat_contexts[chatKey];
|
|
@@ -49927,6 +50043,12 @@ class SessionService {
|
|
|
49927
50043
|
listInternalAliases() {
|
|
49928
50044
|
return Object.keys(this.state.sessions);
|
|
49929
50045
|
}
|
|
50046
|
+
getLogicalSessionRecord(alias) {
|
|
50047
|
+
return this.state.sessions[alias] ?? null;
|
|
50048
|
+
}
|
|
50049
|
+
listLogicalSessionRecords() {
|
|
50050
|
+
return Object.values(this.state.sessions);
|
|
50051
|
+
}
|
|
49930
50052
|
async setCurrentSessionMode(chatKey, modeId) {
|
|
49931
50053
|
await this.mutate(async () => {
|
|
49932
50054
|
const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
|
|
@@ -50005,20 +50127,37 @@ class SessionService {
|
|
|
50005
50127
|
}
|
|
50006
50128
|
return count;
|
|
50007
50129
|
}
|
|
50130
|
+
setSessionResourceLifecyclePublisher(publish) {
|
|
50131
|
+
this.lifecyclePublisher = publish;
|
|
50132
|
+
}
|
|
50133
|
+
publishLifecycleEvent(type, record3) {
|
|
50134
|
+
this.lifecyclePublisher?.({ type, record: record3 });
|
|
50135
|
+
}
|
|
50136
|
+
async commitLifecycleTransition(nextState) {
|
|
50137
|
+
await this.stateStore.saveNow(nextState);
|
|
50138
|
+
replaceRuntimeState(this.state, nextState);
|
|
50139
|
+
}
|
|
50008
50140
|
async setArchived(alias, archived) {
|
|
50009
50141
|
await this.mutate(async () => {
|
|
50010
50142
|
const session3 = this.state.sessions[alias];
|
|
50011
50143
|
if (!session3) {
|
|
50012
50144
|
throw new Error(`session "${alias}" does not exist`);
|
|
50013
50145
|
}
|
|
50146
|
+
const wasArchived = session3.archived === true;
|
|
50147
|
+
if (wasArchived === archived) {
|
|
50148
|
+
return;
|
|
50149
|
+
}
|
|
50150
|
+
const nextState = structuredClone(this.state);
|
|
50151
|
+
const nextSession = nextState.sessions[alias];
|
|
50014
50152
|
if (archived) {
|
|
50015
|
-
|
|
50016
|
-
|
|
50153
|
+
nextSession.archived = true;
|
|
50154
|
+
nextSession.archived_at = new Date(this.now()).toISOString();
|
|
50017
50155
|
} else {
|
|
50018
|
-
delete
|
|
50019
|
-
delete
|
|
50156
|
+
delete nextSession.archived;
|
|
50157
|
+
delete nextSession.archived_at;
|
|
50020
50158
|
}
|
|
50021
|
-
await this.
|
|
50159
|
+
await this.commitLifecycleTransition(nextState);
|
|
50160
|
+
this.publishLifecycleEvent(archived ? "archived" : "restored", nextSession);
|
|
50022
50161
|
});
|
|
50023
50162
|
}
|
|
50024
50163
|
async removeSession(alias) {
|
|
@@ -50027,9 +50166,11 @@ class SessionService {
|
|
|
50027
50166
|
if (!session3) {
|
|
50028
50167
|
throw new Error(`session "${alias}" does not exist`);
|
|
50029
50168
|
}
|
|
50030
|
-
const
|
|
50031
|
-
|
|
50032
|
-
|
|
50169
|
+
const snapshot = structuredClone(session3);
|
|
50170
|
+
const nextState = structuredClone(this.state);
|
|
50171
|
+
const wasActive = Object.values(nextState.chat_contexts).some((ctx) => ctx.current_session === alias);
|
|
50172
|
+
delete nextState.sessions[alias];
|
|
50173
|
+
for (const [chatKey, ctx] of Object.entries(nextState.chat_contexts)) {
|
|
50033
50174
|
if (ctx.previous_session === alias) {
|
|
50034
50175
|
delete ctx.previous_session;
|
|
50035
50176
|
}
|
|
@@ -50048,10 +50189,11 @@ class SessionService {
|
|
|
50048
50189
|
}
|
|
50049
50190
|
}
|
|
50050
50191
|
if (!ctx.current_session && !ctx.previous_session && !ctx.background_results) {
|
|
50051
|
-
delete
|
|
50192
|
+
delete nextState.chat_contexts[chatKey];
|
|
50052
50193
|
}
|
|
50053
50194
|
}
|
|
50054
|
-
await this.
|
|
50195
|
+
await this.commitLifecycleTransition(nextState);
|
|
50196
|
+
this.publishLifecycleEvent("removed", snapshot);
|
|
50055
50197
|
return { wasActive };
|
|
50056
50198
|
});
|
|
50057
50199
|
}
|
|
@@ -50309,6 +50451,7 @@ class SessionService {
|
|
|
50309
50451
|
agent: agent3,
|
|
50310
50452
|
workspace: workspace3,
|
|
50311
50453
|
transport_session: transportSession,
|
|
50454
|
+
logical_session_id: randomUUID9(),
|
|
50312
50455
|
source: native?.source,
|
|
50313
50456
|
agent_session_id: native?.agentSessionId,
|
|
50314
50457
|
agent_session_title: native?.title ?? undefined,
|
|
@@ -50358,6 +50501,89 @@ var init_session_service = __esm(() => {
|
|
|
50358
50501
|
init_channel_scope();
|
|
50359
50502
|
});
|
|
50360
50503
|
|
|
50504
|
+
// src/sessions/session-resource-catalog.ts
|
|
50505
|
+
class CoreSessionResourceCatalog {
|
|
50506
|
+
deps;
|
|
50507
|
+
listeners = new Set;
|
|
50508
|
+
constructor(deps) {
|
|
50509
|
+
this.deps = deps;
|
|
50510
|
+
}
|
|
50511
|
+
async resolve(chatKey, alias) {
|
|
50512
|
+
let internalAlias;
|
|
50513
|
+
try {
|
|
50514
|
+
internalAlias = await this.deps.sessions.resolveAliasForChat(chatKey, alias);
|
|
50515
|
+
} catch {
|
|
50516
|
+
return null;
|
|
50517
|
+
}
|
|
50518
|
+
const record3 = this.deps.sessions.getLogicalSessionRecord(internalAlias);
|
|
50519
|
+
if (!record3) {
|
|
50520
|
+
return null;
|
|
50521
|
+
}
|
|
50522
|
+
const channelId = getChannelIdFromChatKey(chatKey);
|
|
50523
|
+
if (!isSessionAliasVisibleInChannel(record3.alias, channelId)) {
|
|
50524
|
+
return null;
|
|
50525
|
+
}
|
|
50526
|
+
return this.toDescriptor(record3);
|
|
50527
|
+
}
|
|
50528
|
+
async list(channelId) {
|
|
50529
|
+
const descriptors = [];
|
|
50530
|
+
for (const record3 of this.deps.sessions.listLogicalSessionRecords()) {
|
|
50531
|
+
if (!isSessionAliasVisibleInChannel(record3.alias, channelId)) {
|
|
50532
|
+
continue;
|
|
50533
|
+
}
|
|
50534
|
+
const descriptor = this.toDescriptor(record3);
|
|
50535
|
+
if (descriptor) {
|
|
50536
|
+
descriptors.push(descriptor);
|
|
50537
|
+
}
|
|
50538
|
+
}
|
|
50539
|
+
return descriptors;
|
|
50540
|
+
}
|
|
50541
|
+
subscribe(listener) {
|
|
50542
|
+
this.listeners.add(listener);
|
|
50543
|
+
return () => {
|
|
50544
|
+
this.listeners.delete(listener);
|
|
50545
|
+
};
|
|
50546
|
+
}
|
|
50547
|
+
publishLifecycleEvent(input) {
|
|
50548
|
+
const descriptor = this.toDescriptor(input.record);
|
|
50549
|
+
if (!descriptor) {
|
|
50550
|
+
return;
|
|
50551
|
+
}
|
|
50552
|
+
this.emit({ type: input.type, session: descriptor });
|
|
50553
|
+
}
|
|
50554
|
+
emit(event) {
|
|
50555
|
+
for (const listener of [...this.listeners]) {
|
|
50556
|
+
try {
|
|
50557
|
+
listener(event);
|
|
50558
|
+
} catch (error2) {
|
|
50559
|
+
this.deps.logger.error("sessions.resource_catalog.listener_failed", "session resource listener threw; delivery continues", {
|
|
50560
|
+
message: error2 instanceof Error ? error2.message : String(error2),
|
|
50561
|
+
eventType: event.type,
|
|
50562
|
+
internalAlias: event.session.internalAlias
|
|
50563
|
+
}).catch(() => {});
|
|
50564
|
+
}
|
|
50565
|
+
}
|
|
50566
|
+
}
|
|
50567
|
+
toDescriptor(record3) {
|
|
50568
|
+
const workspace3 = this.deps.config.workspaces[record3.workspace];
|
|
50569
|
+
if (!workspace3) {
|
|
50570
|
+
return null;
|
|
50571
|
+
}
|
|
50572
|
+
return {
|
|
50573
|
+
logicalSessionId: record3.logical_session_id,
|
|
50574
|
+
channelId: getChannelIdFromChatKey(record3.alias),
|
|
50575
|
+
internalAlias: record3.alias,
|
|
50576
|
+
displayAlias: toDisplaySessionAlias(record3.alias),
|
|
50577
|
+
workspace: record3.workspace,
|
|
50578
|
+
cwd: workspace3.cwd,
|
|
50579
|
+
archived: record3.archived === true
|
|
50580
|
+
};
|
|
50581
|
+
}
|
|
50582
|
+
}
|
|
50583
|
+
var init_session_resource_catalog = __esm(() => {
|
|
50584
|
+
init_channel_scope();
|
|
50585
|
+
});
|
|
50586
|
+
|
|
50361
50587
|
// src/sessions/active-turn-registry.ts
|
|
50362
50588
|
function createActiveTurnRegistry() {
|
|
50363
50589
|
const byChat = new Map;
|
|
@@ -50619,7 +50845,7 @@ async function deleteAcpxSessionFiles(options) {
|
|
|
50619
50845
|
var init_acpx_session_files = () => {};
|
|
50620
50846
|
|
|
50621
50847
|
// src/transport/acpx-queue-owner-launcher.ts
|
|
50622
|
-
import { createHash as createHash9, randomUUID as
|
|
50848
|
+
import { createHash as createHash9, randomUUID as randomUUID10 } from "node:crypto";
|
|
50623
50849
|
import { spawn as spawn13 } from "node:child_process";
|
|
50624
50850
|
import { access as access6, readFile as readFile18, unlink as unlink2 } from "node:fs/promises";
|
|
50625
50851
|
import { join as join27 } from "node:path";
|
|
@@ -50694,7 +50920,7 @@ class AcpxQueueOwnerLauncher {
|
|
|
50694
50920
|
this.probeSpawnedProcess = options.probeSpawnedProcess ?? defaultProbeSpawnedProcess;
|
|
50695
50921
|
this.handshakeTimeoutMs = options.handshakeTimeoutMs ?? 1e4;
|
|
50696
50922
|
this.handshakePollMs = options.handshakePollMs ?? 50;
|
|
50697
|
-
this.uuid = options.uuid ??
|
|
50923
|
+
this.uuid = options.uuid ?? randomUUID10;
|
|
50698
50924
|
this.sleep = options.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
50699
50925
|
}
|
|
50700
50926
|
async launch(input) {
|
|
@@ -56556,7 +56782,7 @@ var init_session_turn_runner = __esm(() => {
|
|
|
56556
56782
|
});
|
|
56557
56783
|
|
|
56558
56784
|
// src/control/turn-queue.ts
|
|
56559
|
-
import { randomUUID as
|
|
56785
|
+
import { randomUUID as randomUUID11 } from "node:crypto";
|
|
56560
56786
|
|
|
56561
56787
|
class TurnQueue {
|
|
56562
56788
|
deps;
|
|
@@ -56600,7 +56826,7 @@ class TurnQueue {
|
|
|
56600
56826
|
if (q.length >= QUEUE_MAX_DEPTH) {
|
|
56601
56827
|
return { ok: false, errorMessage: "queue-full" };
|
|
56602
56828
|
}
|
|
56603
|
-
const id =
|
|
56829
|
+
const id = randomUUID11();
|
|
56604
56830
|
const item = {
|
|
56605
56831
|
id,
|
|
56606
56832
|
text: params.text,
|
|
@@ -57403,7 +57629,7 @@ function warmthKey(session3) {
|
|
|
57403
57629
|
}
|
|
57404
57630
|
|
|
57405
57631
|
// src/control/terminal-service.ts
|
|
57406
|
-
import { randomUUID as
|
|
57632
|
+
import { randomUUID as randomUUID12 } from "node:crypto";
|
|
57407
57633
|
import { createRequire as createRequire8 } from "node:module";
|
|
57408
57634
|
import { statSync as statSync4 } from "node:fs";
|
|
57409
57635
|
import { spawn as spawnPty2 } from "node-pty";
|
|
@@ -57514,7 +57740,7 @@ function createTerminalService(deps) {
|
|
|
57514
57740
|
return {
|
|
57515
57741
|
create({ cwd, cols, rows }) {
|
|
57516
57742
|
const shell = resolveShell({ platform, env: process.env, shellOverride: deps.shell?.() });
|
|
57517
|
-
const terminalId =
|
|
57743
|
+
const terminalId = randomUUID12();
|
|
57518
57744
|
const handle = spawn18(shell, [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
|
|
57519
57745
|
const session3 = { handle, seq: 0, idleTimer: null, buffer: "", bufBytes: 0, pending: "", pendingBytes: 0, flushTimer: null };
|
|
57520
57746
|
sessions.set(terminalId, session3);
|
|
@@ -58260,7 +58486,7 @@ __export(exports_main, {
|
|
|
58260
58486
|
main: () => main,
|
|
58261
58487
|
buildApp: () => buildApp
|
|
58262
58488
|
});
|
|
58263
|
-
import { randomUUID as
|
|
58489
|
+
import { randomUUID as randomUUID13 } from "node:crypto";
|
|
58264
58490
|
import { homedir as homedir17 } from "node:os";
|
|
58265
58491
|
import { dirname as dirname24, join as join35 } from "node:path";
|
|
58266
58492
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
@@ -58376,6 +58602,14 @@ async function buildApp(paths, deps = {}) {
|
|
|
58376
58602
|
message: stateLoadReport.backupError
|
|
58377
58603
|
});
|
|
58378
58604
|
}
|
|
58605
|
+
for (const record3 of stateLoadReport.migrated ?? []) {
|
|
58606
|
+
await logger.info("state.session_id_migrated", "assigned a new logical_session_id to a legacy session record", {
|
|
58607
|
+
statePath: paths.statePath,
|
|
58608
|
+
section: record3.section,
|
|
58609
|
+
key: record3.key,
|
|
58610
|
+
reason: record3.reason
|
|
58611
|
+
});
|
|
58612
|
+
}
|
|
58379
58613
|
}
|
|
58380
58614
|
const stateMutex = new AsyncMutex;
|
|
58381
58615
|
const debouncedStateStore = new DebouncedStateStore({
|
|
@@ -58389,11 +58623,13 @@ async function buildApp(paths, deps = {}) {
|
|
|
58389
58623
|
});
|
|
58390
58624
|
const runtimeRoot = dirname24(paths.configPath);
|
|
58391
58625
|
const sessions = new SessionService(config4, debouncedStateStore, state, { stateMutex, runtimeRoot });
|
|
58626
|
+
const sessionResources = new CoreSessionResourceCatalog({ sessions, config: config4, logger });
|
|
58627
|
+
sessions.setSessionResourceLifecyclePublisher((transition) => sessionResources.publishLifecycleEvent(transition));
|
|
58392
58628
|
const launchIntentCoordinator = new LaunchIntentCoordinator({
|
|
58393
58629
|
platform: process.platform,
|
|
58394
58630
|
runtimeRoot,
|
|
58395
58631
|
configRoot: runtimeRoot,
|
|
58396
|
-
generationId: deps.daemonIdentity?.generationId ??
|
|
58632
|
+
generationId: deps.daemonIdentity?.generationId ?? randomUUID13(),
|
|
58397
58633
|
...deps.orphanRegistry ? { registry: deps.orphanRegistry } : {},
|
|
58398
58634
|
classifyAdapter: (command) => classifyPreinstalledAdapterCommandShape(command),
|
|
58399
58635
|
resolveAdapter: async (command) => (await validateAndReResolveAdapterCommand(runtimeRoot, command)).agentCommand,
|
|
@@ -58735,7 +58971,7 @@ async function buildApp(paths, deps = {}) {
|
|
|
58735
58971
|
};
|
|
58736
58972
|
orchestration3 = new OrchestrationService({
|
|
58737
58973
|
now: deps.loggerNow ?? (() => new Date),
|
|
58738
|
-
createId: () =>
|
|
58974
|
+
createId: () => randomUUID13(),
|
|
58739
58975
|
config: config4,
|
|
58740
58976
|
loadState: async () => JSON.parse(JSON.stringify(state)),
|
|
58741
58977
|
saveState: async (nextState) => {
|
|
@@ -59008,6 +59244,7 @@ async function buildApp(paths, deps = {}) {
|
|
|
59008
59244
|
agent: agent3,
|
|
59009
59245
|
router: router3,
|
|
59010
59246
|
sessions,
|
|
59247
|
+
sessionResources,
|
|
59011
59248
|
activeTurns,
|
|
59012
59249
|
stateStore,
|
|
59013
59250
|
configStore,
|
|
@@ -59144,6 +59381,7 @@ var init_main = __esm(() => {
|
|
|
59144
59381
|
init_scheduled_route_create();
|
|
59145
59382
|
init_scheduled_route_manage();
|
|
59146
59383
|
init_session_service();
|
|
59384
|
+
init_session_resource_catalog();
|
|
59147
59385
|
init_state_store();
|
|
59148
59386
|
init_acpx_bridge_client();
|
|
59149
59387
|
init_acpx_bridge_transport();
|
|
@@ -59344,6 +59582,7 @@ async function runConsole(paths, deps) {
|
|
|
59344
59582
|
abortSignal: shutdownController.signal,
|
|
59345
59583
|
quota: runtime.quota,
|
|
59346
59584
|
sessions: runtime.sessions,
|
|
59585
|
+
sessionResources: runtime.sessionResources,
|
|
59347
59586
|
activeTurns: runtime.activeTurns,
|
|
59348
59587
|
logger: runtime.logger,
|
|
59349
59588
|
perfTracer: runtime.perfTracer,
|
|
@@ -59509,14 +59748,14 @@ class MessageChannelRegistry {
|
|
|
59509
59748
|
throw new Error("all channels failed to start");
|
|
59510
59749
|
}
|
|
59511
59750
|
}
|
|
59512
|
-
async stopAll() {
|
|
59751
|
+
async stopAll(reason = "shutdown") {
|
|
59513
59752
|
let firstError;
|
|
59514
59753
|
for (const channel of this.channels.values()) {
|
|
59515
59754
|
try {
|
|
59516
59755
|
if (channel.stop) {
|
|
59517
|
-
await channel.stop();
|
|
59756
|
+
await channel.stop(reason);
|
|
59518
59757
|
} else {
|
|
59519
|
-
channel.logout();
|
|
59758
|
+
await channel.logout();
|
|
59520
59759
|
}
|
|
59521
59760
|
} catch (error2) {
|
|
59522
59761
|
firstError ??= error2;
|
|
@@ -61232,11 +61471,20 @@ function applyStateInspectionReport(result, report, statePath, daemonRunning, is
|
|
|
61232
61471
|
if (!report) {
|
|
61233
61472
|
return result;
|
|
61234
61473
|
}
|
|
61474
|
+
const migrated = report.migrated ?? [];
|
|
61475
|
+
const migrationDetails = migrated.map((record3) => `legacy session record ${record3.section}["${record3.key}"]: ${record3.reason} (applied automatically at next daemon startup)`);
|
|
61476
|
+
if (report.dropped.length === 0) {
|
|
61477
|
+
return {
|
|
61478
|
+
...result,
|
|
61479
|
+
details: [...result.details ?? [], `state path: ${statePath}`, ...migrationDetails]
|
|
61480
|
+
};
|
|
61481
|
+
}
|
|
61235
61482
|
const fileCorrupt = report.dropped.some((record3) => record3.section === "file");
|
|
61236
61483
|
const details = [
|
|
61237
61484
|
...result.details ?? [],
|
|
61238
61485
|
`state path: ${statePath}`,
|
|
61239
|
-
...report.dropped.map((record3) => record3.section === "file" ? `state.json is unreadable: ${record3.reason}` : `invalid state record ${record3.section}["${record3.key}"]: ${record3.reason}`)
|
|
61486
|
+
...report.dropped.map((record3) => record3.section === "file" ? `state.json is unreadable: ${record3.reason}` : `invalid state record ${record3.section}["${record3.key}"]: ${record3.reason}`),
|
|
61487
|
+
...migrationDetails
|
|
61240
61488
|
];
|
|
61241
61489
|
return {
|
|
61242
61490
|
...result,
|
|
@@ -61272,9 +61520,11 @@ function createStateQuarantineFix(statePath, daemonRunning, isDaemonRunning) {
|
|
|
61272
61520
|
return { ok: true, message: `state.json was unreadable; renamed to ${report.corruptPath} and reset` };
|
|
61273
61521
|
}
|
|
61274
61522
|
const backup = report.quarantinePath ? ` (original backed up to ${report.quarantinePath})` : "";
|
|
61523
|
+
const migratedCount = report.migrated?.length ?? 0;
|
|
61524
|
+
const migration = migratedCount > 0 ? `; assigned logical_session_id to ${migratedCount} legacy session record(s)` : "";
|
|
61275
61525
|
return {
|
|
61276
61526
|
ok: true,
|
|
61277
|
-
message: `quarantined ${report.dropped.length} invalid state.json record(s)${backup}`
|
|
61527
|
+
message: `quarantined ${report.dropped.length} invalid state.json record(s)${backup}${migration}`
|
|
61278
61528
|
};
|
|
61279
61529
|
}
|
|
61280
61530
|
};
|
|
@@ -61354,7 +61604,7 @@ __export(exports_cli, {
|
|
|
61354
61604
|
getUsageText: () => getUsageText,
|
|
61355
61605
|
createMcpStdioIdentityResolver: () => createMcpStdioIdentityResolver
|
|
61356
61606
|
});
|
|
61357
|
-
import { randomUUID as
|
|
61607
|
+
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
61358
61608
|
import { homedir as homedir23 } from "node:os";
|
|
61359
61609
|
import { dirname as dirname26, join as join39, sep as sep6 } from "node:path";
|
|
61360
61610
|
import { fileURLToPath as fileURLToPath8 } from "node:url";
|
|
@@ -61400,7 +61650,7 @@ async function prepareMcpCoordinatorStartup(input) {
|
|
|
61400
61650
|
return { kind: "external-coordinator" };
|
|
61401
61651
|
}
|
|
61402
61652
|
function createMcpStdioIdentityResolver(input) {
|
|
61403
|
-
const instanceId =
|
|
61653
|
+
const instanceId = randomUUID14().slice(0, 8);
|
|
61404
61654
|
return async (context) => {
|
|
61405
61655
|
const parsedCoordinatorSession = input.parsedCoordinatorSession?.trim() || null;
|
|
61406
61656
|
const workspace3 = input.workspace?.trim() || null;
|
|
@@ -61745,6 +61995,10 @@ function warnStateLoadReport(store, writeStderr = (text) => process.stderr.write
|
|
|
61745
61995
|
}
|
|
61746
61996
|
if (report.backupError) {
|
|
61747
61997
|
writeStderr(`[xacpx] state.quarantine_backup_failed ${report.backupError}
|
|
61998
|
+
`);
|
|
61999
|
+
}
|
|
62000
|
+
for (const record3 of report.migrated ?? []) {
|
|
62001
|
+
writeStderr(`[xacpx] state.session_id_migrated section=${record3.section}${record3.key ? ` key=${record3.key}` : ""} reason=${record3.reason}
|
|
61748
62002
|
`);
|
|
61749
62003
|
}
|
|
61750
62004
|
}
|
|
@@ -62064,7 +62318,7 @@ async function defaultLogin() {
|
|
|
62064
62318
|
}
|
|
62065
62319
|
async function defaultLogout() {
|
|
62066
62320
|
const channel = await resolveLoginChannelForCli();
|
|
62067
|
-
channel.logout();
|
|
62321
|
+
await channel.logout();
|
|
62068
62322
|
}
|
|
62069
62323
|
async function defaultLoadConfiguredPluginsForChannelCli() {
|
|
62070
62324
|
const store = await createCliConfigStore();
|
|
@@ -62294,7 +62548,23 @@ async function createChannelCliDeps(input) {
|
|
|
62294
62548
|
restartDaemon: async () => await restartDaemonCli(controller, input.print),
|
|
62295
62549
|
clearChannelCredentials: async (channel) => {
|
|
62296
62550
|
const { createMessageChannel: createMessageChannel2 } = await Promise.resolve().then(() => (init_create_channel(), exports_create_channel));
|
|
62297
|
-
createMessageChannel2(channel.type, channel).logout();
|
|
62551
|
+
await createMessageChannel2(channel.type, channel).logout();
|
|
62552
|
+
},
|
|
62553
|
+
retireChannel: async (channel, reason) => {
|
|
62554
|
+
const { invokeChannelRetireHook: invokeChannelRetireHook2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
|
|
62555
|
+
await invokeChannelRetireHook2({
|
|
62556
|
+
channel,
|
|
62557
|
+
reason,
|
|
62558
|
+
print: input.print,
|
|
62559
|
+
getDaemonStatus: async () => {
|
|
62560
|
+
const status = await controller.getStatus();
|
|
62561
|
+
if (status.state === "running")
|
|
62562
|
+
return { state: "running" };
|
|
62563
|
+
if (status.state === "indeterminate")
|
|
62564
|
+
return { state: "indeterminate" };
|
|
62565
|
+
return { state: "stopped" };
|
|
62566
|
+
}
|
|
62567
|
+
});
|
|
62298
62568
|
}
|
|
62299
62569
|
};
|
|
62300
62570
|
return { ...base, ...input.overrides };
|
package/dist/i18n/types.d.ts
CHANGED
|
@@ -700,6 +700,7 @@ export interface ChannelCliMessages {
|
|
|
700
700
|
channelAdded: (type: string) => string;
|
|
701
701
|
cannotRemoveLastEnabled: string;
|
|
702
702
|
channelRemoved: (id: string) => string;
|
|
703
|
+
channelRetirementFailed: (id: string, error: string) => string;
|
|
703
704
|
channelCredentialsCleared: (id: string) => string;
|
|
704
705
|
channelCredentialsClearFailed: (id: string, error: string) => string;
|
|
705
706
|
channelCredentialsKept: (id: string) => string;
|
package/dist/plugin-api.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export type { ChannelPluginDefinition } from "./channels/plugin.js";
|
|
1
|
+
export type { ChannelPluginDefinition, ChannelRetireContext, ChannelRetireDaemonState, ChannelRetireHook, } from "./channels/plugin.js";
|
|
2
2
|
export type { ChannelFactory, CreateChannelDeps } from "./channels/create-channel.js";
|
|
3
|
-
export type { ChannelStartInput, ConsumerLock, ConsumerLockMetadata, ConsumerLockOptions, CoordinatorMessageInput, MessageChannelRuntime, ScheduledChannelMessageInput, OrchestrationDeliveryCallbacks, OutboundQuota, PlanEntry, PlanEntryStatus, ToolUseEvent, ToolUseKind, ToolUseStatus, } from "./channels/types.js";
|
|
3
|
+
export type { ChannelStartInput, ConsumerLock, ConsumerLockMetadata, ConsumerLockOptions, ChannelStopReason, CoordinatorMessageInput, MessageChannelRuntime, ScheduledChannelMessageInput, OrchestrationDeliveryCallbacks, OutboundQuota, PlanEntry, PlanEntryStatus, ToolUseEvent, ToolUseKind, ToolUseStatus, } from "./channels/types.js";
|
|
4
4
|
export { formatSubagentNotice, SubagentNoticeTracker } from "./channels/subagent-notice.js";
|
|
5
5
|
export type { AgentCommand, PromptUsage, UsageBreakdown, UsageCost, } from "./transport/types.js";
|
|
6
|
-
export type { ChannelCliInput, ChannelCliIo, ChannelCliParseResult, ChannelCliProvider, ChannelCliValidationIssue, } from "./channels/cli/provider.js";
|
|
6
|
+
export type { ChannelCliInput, ChannelCliIo, ChannelCliParseResult, ChannelCliProvider, ChannelCliValidationIssue, ChannelDoctorFinding, ChannelDoctorFindingLevel, } from "./channels/cli/provider.js";
|
|
7
7
|
export type { ChannelRuntimeConfig } from "./config/types.js";
|
|
8
8
|
export type { CommandHint } from "./commands/command-hints.js";
|
|
9
9
|
export type { AppLogger } from "./logging/app-logger.js";
|
|
@@ -22,4 +22,5 @@ export { getLocale } from "./i18n/index.js";
|
|
|
22
22
|
export type { Locale } from "./i18n/index.js";
|
|
23
23
|
export type { ControlExecuteCommandInput, ControlPromptInput, ControlPromptResult, ControlService, ControlSessionInfo, } from "./control/control-service.js";
|
|
24
24
|
export type { ControlEvent, ControlEventBus, ControlEventListener } from "./control/control-event-bus.js";
|
|
25
|
+
export type { SessionResourceCatalog, SessionResourceDescriptor, SessionResourceLifecycleEvent, } from "./sessions/session-resource-catalog.js";
|
|
25
26
|
export { coreHomeDir } from "./runtime/core-home.js";
|
package/dist/plugin-api.js
CHANGED
|
@@ -927,6 +927,7 @@ var init_channel_cli = __esm(() => {
|
|
|
927
927
|
channelAdded: (type) => `Channel ${type} added`,
|
|
928
928
|
cannotRemoveLastEnabled: "Cannot remove the last enabled channel.",
|
|
929
929
|
channelRemoved: (id) => `Channel ${id} removed`,
|
|
930
|
+
channelRetirementFailed: (id, error) => `Channel ${id} retirement cleanup failed: ${error}`,
|
|
930
931
|
channelCredentialsCleared: (id) => `Removed stored credentials for channel ${id}`,
|
|
931
932
|
channelCredentialsClearFailed: (id, error) => `Channel ${id} removed, but clearing its stored credentials failed: ${error}`,
|
|
932
933
|
channelCredentialsKept: (id) => `Kept stored credentials for channel ${id} (--keep-credentials)`,
|
|
@@ -2053,6 +2054,7 @@ var init_channel_cli2 = __esm(() => {
|
|
|
2053
2054
|
channelAdded: (type) => `频道 ${type} 已添加`,
|
|
2054
2055
|
cannotRemoveLastEnabled: "不能删除最后一个启用的频道。",
|
|
2055
2056
|
channelRemoved: (id) => `频道 ${id} 已删除`,
|
|
2057
|
+
channelRetirementFailed: (id, error) => `频道 ${id} 退役清理失败:${error}`,
|
|
2056
2058
|
channelCredentialsCleared: (id) => `已移除频道 ${id} 的存储凭证`,
|
|
2057
2059
|
channelCredentialsClearFailed: (id, error) => `频道 ${id} 已删除,但清除其存储凭证失败:${error}`,
|
|
2058
2060
|
channelCredentialsKept: (id) => `已保留频道 ${id} 的存储凭证(--keep-credentials)`,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AppConfig } from "../config/types";
|
|
2
|
+
import type { AppLogger } from "../logging/app-logger";
|
|
3
|
+
import type { LogicalSession } from "../state/types";
|
|
4
|
+
import type { SessionService } from "./session-service";
|
|
5
|
+
/**
|
|
6
|
+
* A point-in-time snapshot of one logical session's resource identity. The
|
|
7
|
+
* immutable `logicalSessionId` is the ONLY stable key: aliases, display names
|
|
8
|
+
* and transport bindings can all change under the same id. `cwd` is resolved
|
|
9
|
+
* authoritatively from the core workspace config — callers (e.g. browsers)
|
|
10
|
+
* never contribute a cwd. Descriptors are snapshots: a previously returned
|
|
11
|
+
* descriptor never mutates when the underlying session changes.
|
|
12
|
+
*/
|
|
13
|
+
export interface SessionResourceDescriptor {
|
|
14
|
+
logicalSessionId: string;
|
|
15
|
+
channelId: string;
|
|
16
|
+
internalAlias: string;
|
|
17
|
+
displayAlias: string;
|
|
18
|
+
workspace: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
archived: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type SessionResourceLifecycleEvent = {
|
|
23
|
+
type: "archived";
|
|
24
|
+
session: SessionResourceDescriptor;
|
|
25
|
+
} | {
|
|
26
|
+
type: "restored";
|
|
27
|
+
session: SessionResourceDescriptor;
|
|
28
|
+
} | {
|
|
29
|
+
type: "removed";
|
|
30
|
+
session: SessionResourceDescriptor;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Write-side seam handed to SessionService: after a lifecycle transition has
|
|
34
|
+
* been durably persisted (never before), the service reports the transition
|
|
35
|
+
* type plus the persisted record; the catalog maps the record to a descriptor
|
|
36
|
+
* and publishes the event to subscribers. For `removed` the record is the
|
|
37
|
+
* pre-delete snapshot, so the event carries the session's final identity.
|
|
38
|
+
*/
|
|
39
|
+
export interface SessionResourceLifecyclePublishInput {
|
|
40
|
+
type: SessionResourceLifecycleEvent["type"];
|
|
41
|
+
record: LogicalSession;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Generic, channel-agnostic catalog over logical session resources. Contains
|
|
45
|
+
* no channel- or consumer-specific vocabulary; structured channels (and their
|
|
46
|
+
* plugins) use it to enumerate and resolve the sessions they own.
|
|
47
|
+
*/
|
|
48
|
+
export interface SessionResourceCatalog {
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a chat-scoped alias (display or internal form) to a descriptor.
|
|
51
|
+
* Uses the same chat-scope resolution as ControlService: a caller can never
|
|
52
|
+
* reach another channel's sessions. Returns null for unknown aliases and
|
|
53
|
+
* for sessions whose workspace is no longer registered (their cwd cannot be
|
|
54
|
+
* authoritatively resolved).
|
|
55
|
+
*/
|
|
56
|
+
resolve(chatKey: string, alias: string): Promise<SessionResourceDescriptor | null>;
|
|
57
|
+
/** All sessions of one channel, active AND archived, for reconciliation. */
|
|
58
|
+
list(channelId: string): Promise<SessionResourceDescriptor[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Register a lifecycle listener; returns an unsubscribe function. Listener
|
|
61
|
+
* exceptions are caught and logged to the app log: they never roll back the
|
|
62
|
+
* underlying session operation and never block other listeners.
|
|
63
|
+
*/
|
|
64
|
+
subscribe(listener: (event: SessionResourceLifecycleEvent) => void): () => void;
|
|
65
|
+
}
|
|
66
|
+
interface SessionResourceCatalogDeps {
|
|
67
|
+
sessions: SessionService;
|
|
68
|
+
config: AppConfig;
|
|
69
|
+
logger: AppLogger;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Production catalog assembled by the core runtime over the live SessionService
|
|
73
|
+
* and workspace config. Plugin/channel tests should use their own in-memory
|
|
74
|
+
* implementation of the interface instead of this adapter.
|
|
75
|
+
*/
|
|
76
|
+
export declare class CoreSessionResourceCatalog implements SessionResourceCatalog {
|
|
77
|
+
private readonly deps;
|
|
78
|
+
private readonly listeners;
|
|
79
|
+
constructor(deps: SessionResourceCatalogDeps);
|
|
80
|
+
resolve(chatKey: string, alias: string): Promise<SessionResourceDescriptor | null>;
|
|
81
|
+
list(channelId: string): Promise<SessionResourceDescriptor[]>;
|
|
82
|
+
subscribe(listener: (event: SessionResourceLifecycleEvent) => void): () => void;
|
|
83
|
+
/**
|
|
84
|
+
* Publish one durably-persisted lifecycle transition. SessionService calls
|
|
85
|
+
* this (via the publisher it was handed at runtime wiring) strictly AFTER
|
|
86
|
+
* saveNow succeeded and the runtime state was published; the catalog itself
|
|
87
|
+
* never persists anything. Records whose workspace is no longer registered
|
|
88
|
+
* produce no event: such sessions are invisible in resolve()/list(), so
|
|
89
|
+
* consumers never tracked a resource for them.
|
|
90
|
+
*/
|
|
91
|
+
publishLifecycleEvent(input: SessionResourceLifecyclePublishInput): void;
|
|
92
|
+
/** Fan an event out to all subscribers; listener throws are isolated. */
|
|
93
|
+
private emit;
|
|
94
|
+
private toDescriptor;
|
|
95
|
+
}
|
|
96
|
+
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AppConfig } from "../config/types";
|
|
2
2
|
import { AsyncMutex } from "../orchestration/async-mutex";
|
|
3
3
|
import type { StateStore } from "../state/state-store";
|
|
4
|
-
import type { AppState, BackgroundResult } from "../state/types";
|
|
4
|
+
import type { AppState, BackgroundResult, LogicalSession } from "../state/types";
|
|
5
|
+
import type { SessionResourceLifecyclePublishInput } from "./session-resource-catalog";
|
|
5
6
|
import type { AgentSession, ResolvedSession } from "../transport/types";
|
|
6
7
|
interface SessionListItem {
|
|
7
8
|
alias: string;
|
|
@@ -79,6 +80,7 @@ export declare class SessionService {
|
|
|
79
80
|
private readonly platform;
|
|
80
81
|
private readonly runtimeRoot;
|
|
81
82
|
private readonly pendingSessionAliasOperations;
|
|
83
|
+
private lifecyclePublisher;
|
|
82
84
|
constructor(config: AppConfig, stateStore: SessionStateWriter, state: AppState, options?: SessionServiceOptions);
|
|
83
85
|
createSession(alias: string, agent: string, workspace: string): Promise<ResolvedSession>;
|
|
84
86
|
/**
|
|
@@ -109,6 +111,8 @@ export declare class SessionService {
|
|
|
109
111
|
getPreferredSessionForTransport(transportSession: string): Promise<ResolvedSession | null>;
|
|
110
112
|
findAttachedNativeSession(chatKey: string, agent: string, agentSessionId: string): Promise<ResolvedSession | null>;
|
|
111
113
|
useSession(chatKey: string, alias: string): Promise<SessionSwitchResult>;
|
|
114
|
+
/** Apply the use-session mutation (switch, plus restore when archived). */
|
|
115
|
+
private applyUseSession;
|
|
112
116
|
usePreviousSession(chatKey: string): Promise<SessionSwitchResult | null>;
|
|
113
117
|
setBackgroundResult(chatKey: string, alias: string, result: BackgroundResult): Promise<void>;
|
|
114
118
|
takeBackgroundResult(chatKey: string, alias: string): Promise<BackgroundResult | null>;
|
|
@@ -155,11 +159,36 @@ export declare class SessionService {
|
|
|
155
159
|
*/
|
|
156
160
|
buildFreshTransportSession(stableTransportSession: string): string;
|
|
157
161
|
listInternalAliases(): string[];
|
|
162
|
+
/**
|
|
163
|
+
* Read-only access to the persisted logical session record by internal alias.
|
|
164
|
+
* Returns the LIVE record — callers must treat it as immutable. Exists for
|
|
165
|
+
* consumers (the session resource catalog) that need fields ResolvedSession
|
|
166
|
+
* does not carry (immutable logical_session_id, archived flag).
|
|
167
|
+
*/
|
|
168
|
+
getLogicalSessionRecord(alias: string): LogicalSession | null;
|
|
169
|
+
/** Read-only view of every persisted logical session record, across all channels. */
|
|
170
|
+
listLogicalSessionRecords(): LogicalSession[];
|
|
158
171
|
setCurrentSessionMode(chatKey: string, modeId: string | undefined): Promise<void>;
|
|
159
172
|
setCurrentSessionReplyMode(chatKey: string, replyMode: "stream" | "final" | "verbose" | undefined): Promise<void>;
|
|
160
173
|
getCurrentSession(chatKey: string): Promise<ResolvedSession | null>;
|
|
161
174
|
listSessions(chatKey: string): Promise<SessionListItem[]>;
|
|
162
175
|
countAliasesSharingTransport(transportSession: string, excludeAlias?: string): number;
|
|
176
|
+
/**
|
|
177
|
+
* Wire the sink for resource lifecycle events (production: the core
|
|
178
|
+
* SessionResourceCatalog; the runtime calls this once after constructing
|
|
179
|
+
* the catalog). Archive/restore/remove transitions report through it only
|
|
180
|
+
* AFTER their state has been durably persisted — never before.
|
|
181
|
+
*/
|
|
182
|
+
setSessionResourceLifecyclePublisher(publish: (input: SessionResourceLifecyclePublishInput) => void): void;
|
|
183
|
+
private publishLifecycleEvent;
|
|
184
|
+
/**
|
|
185
|
+
* Durability gate for lifecycle transitions: persist the copy-on-write
|
|
186
|
+
* snapshot first (saveNow rejects on write failure, so the live state, chat
|
|
187
|
+
* contexts and event stream all stay untouched), then publish the persisted
|
|
188
|
+
* snapshot to the runtime state. Only after this resolves may the lifecycle
|
|
189
|
+
* event be emitted.
|
|
190
|
+
*/
|
|
191
|
+
private commitLifecycleTransition;
|
|
163
192
|
setArchived(alias: string, archived: boolean): Promise<void>;
|
|
164
193
|
removeSession(alias: string): Promise<{
|
|
165
194
|
wasActive: boolean;
|
|
@@ -12,6 +12,12 @@ export interface StateLoadDroppedRecord {
|
|
|
12
12
|
}
|
|
13
13
|
export interface StateLoadReport {
|
|
14
14
|
dropped: StateLoadDroppedRecord[];
|
|
15
|
+
/**
|
|
16
|
+
* Legacy session records that were assigned a fresh logical_session_id
|
|
17
|
+
* during load. Kept separate from {@link dropped}: a migrated record is
|
|
18
|
+
* healthy and survives, a dropped record is corrupt and was removed.
|
|
19
|
+
*/
|
|
20
|
+
migrated?: StateLoadDroppedRecord[];
|
|
15
21
|
/** Backup copy of the original file, written because records were dropped. */
|
|
16
22
|
quarantinePath?: string;
|
|
17
23
|
/** Unreadable original renamed aside (whole-file JSON corruption). */
|
|
@@ -19,6 +25,12 @@ export interface StateLoadReport {
|
|
|
19
25
|
/** Best-effort backup/rename failure; load still returned the cleaned state. */
|
|
20
26
|
backupError?: string;
|
|
21
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* A logical_session_id is always a UUIDv4 (randomUUID). Present-but-invalid
|
|
30
|
+
* values are treated as corruption and quarantined with the record; only a
|
|
31
|
+
* genuinely MISSING field (legacy record) enters the load-time migration.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isLogicalSessionId(value: unknown): value is string;
|
|
22
34
|
/**
|
|
23
35
|
* Lenient state parser: a malformed record (or wrong-typed section) is skipped
|
|
24
36
|
* and collected in `dropped` instead of throwing, so one bad record can never
|
|
@@ -27,7 +39,7 @@ export interface StateLoadReport {
|
|
|
27
39
|
* non-object top level still throws (StateStore.load treats that as a corrupt
|
|
28
40
|
* file and renames it aside).
|
|
29
41
|
*/
|
|
30
|
-
export declare function parseState(raw: unknown, path: string, dropped?: StateLoadDroppedRecord[]): AppState;
|
|
42
|
+
export declare function parseState(raw: unknown, path: string, dropped?: StateLoadDroppedRecord[], migrated?: StateLoadDroppedRecord[]): AppState;
|
|
31
43
|
export interface StateStoreOptions {
|
|
32
44
|
/** Injectable clock used for quarantine/corrupt backup file names. */
|
|
33
45
|
now?: () => Date;
|
|
@@ -37,6 +49,11 @@ export interface StateStoreOptions {
|
|
|
37
49
|
* writer suffix-retries instead of overwriting an existing backup).
|
|
38
50
|
*/
|
|
39
51
|
writeBackup?: (targetPath: string, content: string) => Promise<string | void>;
|
|
52
|
+
/**
|
|
53
|
+
* Injectable durable writer for the legacy logical_session_id migration
|
|
54
|
+
* (tests simulate write failures). Defaults to the regular atomic save.
|
|
55
|
+
*/
|
|
56
|
+
writeMigration?: (state: AppState) => Promise<void>;
|
|
40
57
|
}
|
|
41
58
|
/** Result of a side-effect-free {@link StateStore.inspect}. */
|
|
42
59
|
export interface StateLoadInspection {
|
|
@@ -56,11 +73,13 @@ export declare class StateStore {
|
|
|
56
73
|
*/
|
|
57
74
|
get lastLoadReport(): StateLoadReport | null;
|
|
58
75
|
load(): Promise<AppState>;
|
|
76
|
+
/** Synchronous (awaited) atomic write of a migrated state, via the private-file writer. */
|
|
77
|
+
private persistMigration;
|
|
59
78
|
/**
|
|
60
79
|
* Side-effect-free variant of load() for diagnostic callers (doctor): parses
|
|
61
|
-
* and reports exactly what load() would drop/repair, but never writes
|
|
62
|
-
* quarantine backup, never renames a corrupt file,
|
|
63
|
-
* {@link lastLoadReport}.
|
|
80
|
+
* and reports exactly what load() would drop/repair/migrate, but never writes
|
|
81
|
+
* a quarantine backup, never renames a corrupt file, never persists pending
|
|
82
|
+
* logical_session_id migrations, and does not touch {@link lastLoadReport}.
|
|
64
83
|
*/
|
|
65
84
|
inspect(): Promise<StateLoadInspection>;
|
|
66
85
|
private readAndParse;
|
package/dist/state/types.d.ts
CHANGED
|
@@ -20,6 +20,14 @@ export interface LogicalSession {
|
|
|
20
20
|
agent: string;
|
|
21
21
|
workspace: string;
|
|
22
22
|
transport_session: string;
|
|
23
|
+
/**
|
|
24
|
+
* Immutable identity of this logical session (UUIDv4), assigned once at
|
|
25
|
+
* create/attach time. Never changes across rename, display-name, agent,
|
|
26
|
+
* workspace, or transport-binding updates; never reused after the alias is
|
|
27
|
+
* deleted. Legacy records missing the field are migrated once at load time
|
|
28
|
+
* and persisted before startup proceeds.
|
|
29
|
+
*/
|
|
30
|
+
logical_session_id: string;
|
|
23
31
|
source?: LogicalSessionSource;
|
|
24
32
|
agent_session_id?: string;
|
|
25
33
|
agent_session_title?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ganglion/xacpx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"acpx",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"build:relay": "bun run build:relay-web && bun run clean:relay && bun build ./packages/relay/src/cli.ts --outdir ./packages/relay/dist --target node --external ws --external hono --external @hono/node-server --external @ganglion/xacpx-relay-protocol && tsc -p packages/relay/tsconfig.json && bun run bundle:relay-web",
|
|
55
55
|
"clean:channel-relay": "node -e \"require('node:fs').rmSync('packages/channel-relay/dist', { recursive: true, force: true })\"",
|
|
56
56
|
"build:channel-relay": "bun run build:plugin-api && bun run build:relay-protocol && bun run clean:channel-relay && bun build ./packages/channel-relay/src/index.ts --outdir ./packages/channel-relay/dist --target node --external xacpx --external ws --external @ganglion/xacpx-relay-protocol && tsc -p packages/channel-relay/tsconfig.json",
|
|
57
|
+
"pack:rmux-bridge": "node ./scripts/pack-rmux-bridge-platform.mjs",
|
|
57
58
|
"clean:relay-web": "node -e \"require('node:fs').rmSync('packages/relay-web/dist',{recursive:true,force:true})\"",
|
|
58
59
|
"build:relay-web": "bun run build:relay-protocol && bun run clean:relay-web && bun run --cwd packages/relay-web build",
|
|
59
60
|
"test:web": "bun run --cwd packages/relay-web test",
|