@ouro.bot/cli 0.1.0-alpha.12 → 0.1.0-alpha.121
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/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +147 -205
- package/assets/ouroboros.png +0 -0
- package/changelog.json +737 -0
- package/dist/heart/active-work.js +622 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/commitments.js +105 -0
- package/dist/heart/config.js +68 -23
- package/dist/heart/core.js +528 -100
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/auth-flow.js +430 -0
- package/dist/heart/daemon/daemon-cli.js +1601 -207
- package/dist/heart/daemon/daemon-entry.js +43 -2
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon.js +226 -1
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +7 -82
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/ouro-bot-entry.js +0 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-entry.js +0 -0
- package/dist/heart/daemon/ouro-path-installer.js +59 -15
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/process-manager.js +13 -0
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +43 -2
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +37 -94
- package/dist/heart/daemon/specialist-prompt.js +50 -12
- package/dist/heart/daemon/specialist-tools.js +211 -60
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +64 -21
- package/dist/heart/kicks.js +1 -19
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +197 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-failover.js +88 -0
- package/dist/heart/provider-ping.js +151 -0
- package/dist/heart/providers/anthropic.js +107 -20
- package/dist/heart/providers/azure.js +115 -9
- package/dist/heart/providers/github-copilot.js +157 -0
- package/dist/heart/providers/minimax.js +33 -3
- package/dist/heart/providers/openai-codex.js +49 -14
- package/dist/heart/safe-workspace.js +381 -0
- package/dist/heart/session-activity.js +169 -0
- package/dist/heart/session-recall.js +216 -0
- package/dist/heart/streaming.js +108 -24
- package/dist/heart/target-resolution.js +123 -0
- package/dist/heart/tool-loop.js +194 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +14 -2
- package/dist/mind/bundle-manifest.js +70 -0
- package/dist/mind/context.js +60 -14
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +35 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/memory.js +27 -26
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +459 -77
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/index.js +12 -0
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/context-pack.js +254 -0
- package/dist/repertoire/coding/feedback.js +301 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +210 -4
- package/dist/repertoire/coding/spawner.js +39 -9
- package/dist/repertoire/coding/tools.js +171 -4
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +290 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +195 -0
- package/dist/repertoire/skills.js +3 -26
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/transitions.js +1 -2
- package/dist/repertoire/tools-base.js +925 -250
- package/dist/repertoire/tools-bluebubbles.js +93 -0
- package/dist/repertoire/tools-teams.js +58 -25
- package/dist/repertoire/tools.js +106 -53
- package/dist/senses/bluebubbles-client.js +210 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +912 -45
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +477 -170
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +154 -0
- package/dist/senses/inner-dialog-worker.js +47 -18
- package/dist/senses/inner-dialog.js +388 -83
- package/dist/senses/pipeline.js +444 -0
- package/dist/senses/teams.js +607 -129
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +14 -3
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/specialist-session.js +0 -177
- package/dist/heart/daemon/subagent-installer.js +0 -134
- package/dist/inner-worker-entry.js +0 -4
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
|
@@ -3,12 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.classifyOpenAICodexError = classifyOpenAICodexError;
|
|
6
7
|
exports.createOpenAICodexProviderRuntime = createOpenAICodexProviderRuntime;
|
|
7
8
|
const openai_1 = __importDefault(require("openai"));
|
|
8
9
|
const config_1 = require("../config");
|
|
9
10
|
const identity_1 = require("../identity");
|
|
10
11
|
const runtime_1 = require("../../nerves/runtime");
|
|
11
12
|
const streaming_1 = require("../streaming");
|
|
13
|
+
const model_capabilities_1 = require("../model-capabilities");
|
|
12
14
|
const OPENAI_CODEX_AUTH_FAILURE_MARKERS = [
|
|
13
15
|
"authentication failed",
|
|
14
16
|
"unauthorized",
|
|
@@ -27,11 +29,11 @@ function getOpenAICodexOAuthInstructions() {
|
|
|
27
29
|
const agentName = getOpenAICodexAgentNameForGuidance();
|
|
28
30
|
return [
|
|
29
31
|
"Fix:",
|
|
30
|
-
` 1. Run \`
|
|
31
|
-
" (or run `codex login` and set the OAuth token manually)",
|
|
32
|
+
` 1. Run \`ouro auth --agent ${agentName}\``,
|
|
32
33
|
` 2. Open ${getOpenAICodexSecretsPathForGuidance()}`,
|
|
33
34
|
" 3. Confirm providers.openai-codex.oauthAccessToken is set",
|
|
34
35
|
" 4. This provider uses chatgpt.com/backend-api/codex/responses (not api.openai.com/responses).",
|
|
36
|
+
" 5. After reauth, retry the failed ouro command or reconnect this session.",
|
|
35
37
|
].join("\n");
|
|
36
38
|
}
|
|
37
39
|
function getOpenAICodexReauthGuidance(reason) {
|
|
@@ -41,6 +43,33 @@ function getOpenAICodexReauthGuidance(reason) {
|
|
|
41
43
|
getOpenAICodexOAuthInstructions(),
|
|
42
44
|
].join("\n");
|
|
43
45
|
}
|
|
46
|
+
/* v8 ignore start -- shared network error utility, tested via classification tests @preserve */
|
|
47
|
+
function isNetworkError(error) {
|
|
48
|
+
const code = error.code || "";
|
|
49
|
+
if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
|
|
50
|
+
"EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
|
|
51
|
+
return true;
|
|
52
|
+
const msg = error.message || "";
|
|
53
|
+
return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
|
|
54
|
+
}
|
|
55
|
+
/* v8 ignore stop */
|
|
56
|
+
function classifyOpenAICodexError(error) {
|
|
57
|
+
const status = error.status;
|
|
58
|
+
if (status === 401 || status === 403 || isOpenAICodexAuthFailure(error))
|
|
59
|
+
return "auth-failure";
|
|
60
|
+
if (status === 429) {
|
|
61
|
+
const lower = error.message.toLowerCase();
|
|
62
|
+
if (lower.includes("usage") || lower.includes("quota") || lower.includes("exceeded your"))
|
|
63
|
+
return "usage-limit";
|
|
64
|
+
return "rate-limit";
|
|
65
|
+
}
|
|
66
|
+
if (status && status >= 500)
|
|
67
|
+
return "server-error";
|
|
68
|
+
if (isNetworkError(error))
|
|
69
|
+
return "network-error";
|
|
70
|
+
return "unknown";
|
|
71
|
+
}
|
|
72
|
+
/* v8 ignore start -- auth detection: only called from classifyOpenAICodexError which always passes Error @preserve */
|
|
44
73
|
function isOpenAICodexAuthFailure(error) {
|
|
45
74
|
if (!(error instanceof Error))
|
|
46
75
|
return false;
|
|
@@ -50,13 +79,7 @@ function isOpenAICodexAuthFailure(error) {
|
|
|
50
79
|
const lower = error.message.toLowerCase();
|
|
51
80
|
return OPENAI_CODEX_AUTH_FAILURE_MARKERS.some((marker) => lower.includes(marker));
|
|
52
81
|
}
|
|
53
|
-
|
|
54
|
-
const base = error instanceof Error ? error.message : String(error);
|
|
55
|
-
if (isOpenAICodexAuthFailure(error)) {
|
|
56
|
-
return new Error(getOpenAICodexReauthGuidance(`OpenAI Codex authentication failed (${base}).`));
|
|
57
|
-
}
|
|
58
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
59
|
-
}
|
|
82
|
+
/* v8 ignore stop */
|
|
60
83
|
function decodeJwtPayload(token) {
|
|
61
84
|
const parts = token.split(".");
|
|
62
85
|
if (parts.length < 2)
|
|
@@ -87,14 +110,14 @@ function getChatGPTAccountIdFromToken(token) {
|
|
|
87
110
|
return "";
|
|
88
111
|
return accountId.trim();
|
|
89
112
|
}
|
|
90
|
-
function createOpenAICodexProviderRuntime() {
|
|
113
|
+
function createOpenAICodexProviderRuntime(config) {
|
|
91
114
|
(0, runtime_1.emitNervesEvent)({
|
|
92
115
|
component: "engine",
|
|
93
116
|
event: "engine.provider_init",
|
|
94
117
|
message: "openai-codex provider init",
|
|
95
118
|
meta: { provider: "openai-codex" },
|
|
96
119
|
});
|
|
97
|
-
const codexConfig = (0, config_1.getOpenAICodexConfig)();
|
|
120
|
+
const codexConfig = config ?? (0, config_1.getOpenAICodexConfig)();
|
|
98
121
|
if (!(codexConfig.model && codexConfig.oauthAccessToken)) {
|
|
99
122
|
throw new Error(getOpenAICodexReauthGuidance("provider 'openai-codex' is selected in agent.json but providers.openai-codex.model/oauthAccessToken is incomplete in secrets.json."));
|
|
100
123
|
}
|
|
@@ -106,6 +129,12 @@ function createOpenAICodexProviderRuntime() {
|
|
|
106
129
|
if (!chatgptAccountId) {
|
|
107
130
|
throw new Error(getOpenAICodexReauthGuidance("OpenAI Codex OAuth access token is missing a chatgpt_account_id claim required for chatgpt.com/backend-api/codex."));
|
|
108
131
|
}
|
|
132
|
+
const modelCaps = (0, model_capabilities_1.getModelCapabilities)(codexConfig.model);
|
|
133
|
+
const capabilities = new Set();
|
|
134
|
+
if (modelCaps.reasoningEffort)
|
|
135
|
+
capabilities.add("reasoning-effort");
|
|
136
|
+
if (modelCaps.phase)
|
|
137
|
+
capabilities.add("phase-annotation");
|
|
109
138
|
const client = new openai_1.default({
|
|
110
139
|
apiKey: token,
|
|
111
140
|
baseURL: OPENAI_CODEX_BACKEND_BASE_URL,
|
|
@@ -123,6 +152,8 @@ function createOpenAICodexProviderRuntime() {
|
|
|
123
152
|
id: "openai-codex",
|
|
124
153
|
model: codexConfig.model,
|
|
125
154
|
client,
|
|
155
|
+
capabilities,
|
|
156
|
+
supportedReasoningEfforts: modelCaps.reasoningEffort,
|
|
126
157
|
resetTurnState(messages) {
|
|
127
158
|
const { instructions, input } = (0, streaming_1.toResponsesInput)(messages);
|
|
128
159
|
nativeInput = input;
|
|
@@ -141,7 +172,7 @@ function createOpenAICodexProviderRuntime() {
|
|
|
141
172
|
input: nativeInput,
|
|
142
173
|
instructions: nativeInstructions,
|
|
143
174
|
tools: (0, streaming_1.toResponsesTools)(request.activeTools),
|
|
144
|
-
reasoning: { effort: "medium", summary: "detailed" },
|
|
175
|
+
reasoning: { effort: request.reasoningEffort ?? "medium", summary: "detailed" },
|
|
145
176
|
stream: true,
|
|
146
177
|
store: false,
|
|
147
178
|
include: ["reasoning.encrypted_content"],
|
|
@@ -149,14 +180,18 @@ function createOpenAICodexProviderRuntime() {
|
|
|
149
180
|
if (request.toolChoiceRequired)
|
|
150
181
|
params.tool_choice = "required";
|
|
151
182
|
try {
|
|
152
|
-
const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal);
|
|
183
|
+
const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal, request.eagerFinalAnswerStreaming);
|
|
153
184
|
for (const item of result.outputItems)
|
|
154
185
|
nativeInput.push(item);
|
|
155
186
|
return result;
|
|
156
187
|
}
|
|
157
188
|
catch (error) {
|
|
158
|
-
throw
|
|
189
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
159
190
|
}
|
|
160
191
|
},
|
|
192
|
+
/* v8 ignore next 3 -- delegation: classification logic tested via classifyOpenAICodexError @preserve */
|
|
193
|
+
classifyError(error) {
|
|
194
|
+
return classifyOpenAICodexError(error);
|
|
195
|
+
},
|
|
161
196
|
};
|
|
162
197
|
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resetSafeWorkspaceSelection = resetSafeWorkspaceSelection;
|
|
37
|
+
exports.getActiveSafeWorkspaceSelection = getActiveSafeWorkspaceSelection;
|
|
38
|
+
exports.ensureSafeRepoWorkspace = ensureSafeRepoWorkspace;
|
|
39
|
+
exports.resolveSafeRepoPath = resolveSafeRepoPath;
|
|
40
|
+
exports.resolveSafeShellExecution = resolveSafeShellExecution;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const identity_1 = require("./identity");
|
|
45
|
+
const runtime_1 = require("../nerves/runtime");
|
|
46
|
+
let activeSelection = null;
|
|
47
|
+
let cleanupHookRegistered = false;
|
|
48
|
+
function workspaceSelectionStateFile(workspaceBase) {
|
|
49
|
+
return path.join(workspaceBase, ".active-safe-workspace.json");
|
|
50
|
+
}
|
|
51
|
+
function getOptionalFsFn(name) {
|
|
52
|
+
try {
|
|
53
|
+
return fs[name];
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function shouldPersistSelection(options) {
|
|
60
|
+
return options.persistSelection ?? options.workspaceRoot === undefined;
|
|
61
|
+
}
|
|
62
|
+
function isPersistedSelectionShape(value) {
|
|
63
|
+
if (!value || typeof value !== "object")
|
|
64
|
+
return false;
|
|
65
|
+
const candidate = value;
|
|
66
|
+
return (typeof candidate.runtimeKind === "string"
|
|
67
|
+
&& typeof candidate.repoRoot === "string"
|
|
68
|
+
&& typeof candidate.workspaceRoot === "string"
|
|
69
|
+
&& typeof candidate.workspaceBranch === "string"
|
|
70
|
+
&& (candidate.sourceBranch === null || typeof candidate.sourceBranch === "string")
|
|
71
|
+
&& typeof candidate.sourceCloneUrl === "string"
|
|
72
|
+
&& typeof candidate.cleanupAfterMerge === "boolean"
|
|
73
|
+
&& typeof candidate.created === "boolean"
|
|
74
|
+
&& typeof candidate.note === "string");
|
|
75
|
+
}
|
|
76
|
+
function loadPersistedSelection(workspaceBase, options) {
|
|
77
|
+
const existsSync = options.existsSync ?? fs.existsSync;
|
|
78
|
+
const readFileSync = options.readFileSync ?? getOptionalFsFn("readFileSync");
|
|
79
|
+
const unlinkSync = options.unlinkSync ?? getOptionalFsFn("unlinkSync");
|
|
80
|
+
const stateFile = workspaceSelectionStateFile(workspaceBase);
|
|
81
|
+
if (!existsSync(stateFile))
|
|
82
|
+
return null;
|
|
83
|
+
if (!readFileSync)
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
const raw = readFileSync(stateFile, "utf-8");
|
|
87
|
+
const parsed = JSON.parse(raw);
|
|
88
|
+
if (!isPersistedSelectionShape(parsed) || !existsSync(parsed.workspaceRoot)) {
|
|
89
|
+
try {
|
|
90
|
+
unlinkSync?.(stateFile);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// best effort
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return parsed;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
try {
|
|
101
|
+
unlinkSync?.(stateFile);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// best effort
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function persistSelectionState(workspaceBase, selection, options) {
|
|
110
|
+
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
|
|
111
|
+
const writeFileSync = options.writeFileSync ?? getOptionalFsFn("writeFileSync");
|
|
112
|
+
if (!writeFileSync)
|
|
113
|
+
return;
|
|
114
|
+
mkdirSync(workspaceBase, { recursive: true });
|
|
115
|
+
writeFileSync(workspaceSelectionStateFile(workspaceBase), JSON.stringify(selection, null, 2), "utf-8");
|
|
116
|
+
}
|
|
117
|
+
function defaultNow() {
|
|
118
|
+
return Date.now();
|
|
119
|
+
}
|
|
120
|
+
function resolveAgentName(explicit) {
|
|
121
|
+
if (explicit && explicit.trim().length > 0)
|
|
122
|
+
return explicit.trim();
|
|
123
|
+
try {
|
|
124
|
+
return (0, identity_1.getAgentName)();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return "slugger";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function runGit(cwd, args, spawnSync) {
|
|
131
|
+
return spawnSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
132
|
+
}
|
|
133
|
+
function readStdout(result) {
|
|
134
|
+
return (result.stdout ?? Buffer.from("")).toString("utf-8").trim();
|
|
135
|
+
}
|
|
136
|
+
function readStderr(result) {
|
|
137
|
+
return (result.stderr ?? Buffer.from("")).toString("utf-8").trim();
|
|
138
|
+
}
|
|
139
|
+
function assertGitOk(result, action) {
|
|
140
|
+
if (result.error) {
|
|
141
|
+
throw result.error;
|
|
142
|
+
}
|
|
143
|
+
if (result.status !== 0) {
|
|
144
|
+
const detail = readStderr(result) || readStdout(result) || `exit ${result.status ?? "unknown"}`;
|
|
145
|
+
throw new Error(`${action} failed: ${detail}`);
|
|
146
|
+
}
|
|
147
|
+
return readStdout(result);
|
|
148
|
+
}
|
|
149
|
+
function isGitClone(repoRoot, spawnSync) {
|
|
150
|
+
const result = runGit(repoRoot, ["rev-parse", "--is-inside-work-tree"], spawnSync);
|
|
151
|
+
return result.status === 0 && readStdout(result) === "true";
|
|
152
|
+
}
|
|
153
|
+
function readCurrentBranch(repoRoot, spawnSync) {
|
|
154
|
+
return assertGitOk(runGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"], spawnSync), "git branch read");
|
|
155
|
+
}
|
|
156
|
+
function ensureFetchedOrigin(repoRoot, spawnSync) {
|
|
157
|
+
assertGitOk(runGit(repoRoot, ["fetch", "origin"], spawnSync), "git fetch origin");
|
|
158
|
+
}
|
|
159
|
+
function ensureMainFastForward(repoRoot, spawnSync) {
|
|
160
|
+
assertGitOk(runGit(repoRoot, ["pull", "--ff-only", "origin", "main"], spawnSync), "git pull --ff-only origin main");
|
|
161
|
+
}
|
|
162
|
+
function createDedicatedWorktree(repoRoot, workspaceRoot, branchSuffix, existsSync, mkdirSync, rmSync, spawnSync) {
|
|
163
|
+
mkdirSync(path.dirname(workspaceRoot), { recursive: true });
|
|
164
|
+
const branchName = `slugger/${branchSuffix}`;
|
|
165
|
+
if (existsSync(workspaceRoot)) {
|
|
166
|
+
rmSync(workspaceRoot, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
assertGitOk(runGit(repoRoot, ["worktree", "add", "-B", branchName, workspaceRoot, "origin/main"], spawnSync), "git worktree add");
|
|
169
|
+
return { workspaceRoot, created: true, branchName };
|
|
170
|
+
}
|
|
171
|
+
function createScratchClone(workspaceRoot, cloneUrl, existsSync, mkdirSync, rmSync, spawnSync) {
|
|
172
|
+
mkdirSync(path.dirname(workspaceRoot), { recursive: true });
|
|
173
|
+
if (existsSync(workspaceRoot)) {
|
|
174
|
+
rmSync(workspaceRoot, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
const result = spawnSync("git", ["clone", "--depth", "1", "--branch", "main", cloneUrl, workspaceRoot], {
|
|
177
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
178
|
+
});
|
|
179
|
+
assertGitOk(result, "git clone");
|
|
180
|
+
return { workspaceRoot, created: true, branchName: "main" };
|
|
181
|
+
}
|
|
182
|
+
const REPO_LOCAL_SHELL_COMMAND = /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(git|npm|npx|node|pnpm|yarn|bun|rg|sed|cat|ls|find|grep|vitest|tsc|eslint)\b/;
|
|
183
|
+
function looksRepoLocalShellCommand(command) {
|
|
184
|
+
return REPO_LOCAL_SHELL_COMMAND.test(command.trim());
|
|
185
|
+
}
|
|
186
|
+
function registerCleanupHook(options) {
|
|
187
|
+
if (cleanupHookRegistered)
|
|
188
|
+
return;
|
|
189
|
+
cleanupHookRegistered = true;
|
|
190
|
+
process.on("exit", () => {
|
|
191
|
+
if (!activeSelection?.cleanupAfterMerge)
|
|
192
|
+
return;
|
|
193
|
+
try {
|
|
194
|
+
options.rmSync(activeSelection.workspaceRoot, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// best effort
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function resetSafeWorkspaceSelection(options = {}) {
|
|
202
|
+
activeSelection = null;
|
|
203
|
+
if (!options.keepCleanupHookRegistered) {
|
|
204
|
+
cleanupHookRegistered = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function getActiveSafeWorkspaceSelection() {
|
|
208
|
+
return activeSelection;
|
|
209
|
+
}
|
|
210
|
+
function refreshSelectionWorkspaceBranch(selection, spawnSync) {
|
|
211
|
+
try {
|
|
212
|
+
if (!isGitClone(selection.workspaceRoot, spawnSync)) {
|
|
213
|
+
return selection;
|
|
214
|
+
}
|
|
215
|
+
const liveBranch = readCurrentBranch(selection.workspaceRoot, spawnSync);
|
|
216
|
+
if (liveBranch === selection.workspaceBranch) {
|
|
217
|
+
return selection;
|
|
218
|
+
}
|
|
219
|
+
return { ...selection, workspaceBranch: liveBranch };
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return selection;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function ensureSafeRepoWorkspace(options = {}) {
|
|
226
|
+
const agentName = resolveAgentName(options.agentName);
|
|
227
|
+
const workspaceBase = options.workspaceRoot ?? (0, identity_1.getAgentRepoWorkspacesRoot)(agentName);
|
|
228
|
+
const persistSelection = shouldPersistSelection(options);
|
|
229
|
+
const spawnSync = options.spawnSync ?? child_process_1.spawnSync;
|
|
230
|
+
const existsSync = options.existsSync ?? fs.existsSync;
|
|
231
|
+
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
|
|
232
|
+
const rmSync = options.rmSync ?? fs.rmSync;
|
|
233
|
+
if (activeSelection) {
|
|
234
|
+
const refreshed = refreshSelectionWorkspaceBranch(activeSelection, spawnSync);
|
|
235
|
+
activeSelection = refreshed;
|
|
236
|
+
return refreshed;
|
|
237
|
+
}
|
|
238
|
+
const repoRoot = options.repoRoot ?? (0, identity_1.getRepoRoot)();
|
|
239
|
+
const canonicalRepoUrl = options.canonicalRepoUrl ?? identity_1.HARNESS_CANONICAL_REPO_URL;
|
|
240
|
+
const now = options.now ?? defaultNow;
|
|
241
|
+
const stamp = String(now());
|
|
242
|
+
registerCleanupHook({ rmSync });
|
|
243
|
+
if (persistSelection) {
|
|
244
|
+
const restored = loadPersistedSelection(workspaceBase, options);
|
|
245
|
+
if (restored) {
|
|
246
|
+
const refreshed = refreshSelectionWorkspaceBranch(restored, spawnSync);
|
|
247
|
+
activeSelection = refreshed;
|
|
248
|
+
persistSelectionState(workspaceBase, refreshed, options);
|
|
249
|
+
(0, runtime_1.emitNervesEvent)({
|
|
250
|
+
component: "workspace",
|
|
251
|
+
event: "workspace.safe_repo_restored",
|
|
252
|
+
message: "restored safe repo workspace after runtime restart",
|
|
253
|
+
meta: {
|
|
254
|
+
runtimeKind: refreshed.runtimeKind,
|
|
255
|
+
repoRoot: refreshed.repoRoot,
|
|
256
|
+
workspaceRoot: refreshed.workspaceRoot,
|
|
257
|
+
workspaceBranch: refreshed.workspaceBranch,
|
|
258
|
+
sourceBranch: refreshed.sourceBranch,
|
|
259
|
+
cleanupAfterMerge: refreshed.cleanupAfterMerge,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
return refreshed;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
let selection;
|
|
266
|
+
if (isGitClone(repoRoot, spawnSync)) {
|
|
267
|
+
const branch = readCurrentBranch(repoRoot, spawnSync);
|
|
268
|
+
ensureFetchedOrigin(repoRoot, spawnSync);
|
|
269
|
+
if (branch === "main") {
|
|
270
|
+
ensureMainFastForward(repoRoot, spawnSync);
|
|
271
|
+
const worktreeRoot = path.join(workspaceBase, `ouroboros-main-${stamp}`);
|
|
272
|
+
const created = createDedicatedWorktree(repoRoot, worktreeRoot, `safe-workspace-${stamp}`, existsSync, mkdirSync, rmSync, spawnSync);
|
|
273
|
+
selection = {
|
|
274
|
+
runtimeKind: "clone-main",
|
|
275
|
+
repoRoot,
|
|
276
|
+
workspaceRoot: created.workspaceRoot,
|
|
277
|
+
workspaceBranch: created.branchName,
|
|
278
|
+
sourceBranch: branch,
|
|
279
|
+
sourceCloneUrl: canonicalRepoUrl,
|
|
280
|
+
cleanupAfterMerge: false,
|
|
281
|
+
created: created.created,
|
|
282
|
+
note: `running from clone on main; fast-forwarded and created dedicated worktree ${created.workspaceRoot}`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const worktreeRoot = path.join(workspaceBase, `ouroboros-origin-main-${stamp}`);
|
|
287
|
+
const created = createDedicatedWorktree(repoRoot, worktreeRoot, `safe-workspace-${stamp}`, existsSync, mkdirSync, rmSync, spawnSync);
|
|
288
|
+
selection = {
|
|
289
|
+
runtimeKind: "clone-non-main",
|
|
290
|
+
repoRoot,
|
|
291
|
+
workspaceRoot: created.workspaceRoot,
|
|
292
|
+
workspaceBranch: created.branchName,
|
|
293
|
+
sourceBranch: branch,
|
|
294
|
+
sourceCloneUrl: canonicalRepoUrl,
|
|
295
|
+
cleanupAfterMerge: false,
|
|
296
|
+
created: created.created,
|
|
297
|
+
note: `running from branch ${branch}; defaulted new work from origin/main in dedicated worktree ${created.workspaceRoot}`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const scratchRoot = path.join(workspaceBase, `ouroboros-scratch-${stamp}`);
|
|
303
|
+
const created = createScratchClone(scratchRoot, canonicalRepoUrl, existsSync, mkdirSync, rmSync, spawnSync);
|
|
304
|
+
selection = {
|
|
305
|
+
runtimeKind: "installed-runtime",
|
|
306
|
+
repoRoot,
|
|
307
|
+
workspaceRoot: created.workspaceRoot,
|
|
308
|
+
workspaceBranch: created.branchName,
|
|
309
|
+
sourceBranch: null,
|
|
310
|
+
sourceCloneUrl: canonicalRepoUrl,
|
|
311
|
+
cleanupAfterMerge: true,
|
|
312
|
+
created: created.created,
|
|
313
|
+
note: `running from installed runtime/wrapper; created scratch clone ${created.workspaceRoot} from ${canonicalRepoUrl}`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
activeSelection = selection;
|
|
317
|
+
if (persistSelection) {
|
|
318
|
+
persistSelectionState(workspaceBase, selection, options);
|
|
319
|
+
}
|
|
320
|
+
(0, runtime_1.emitNervesEvent)({
|
|
321
|
+
component: "workspace",
|
|
322
|
+
event: "workspace.safe_repo_acquired",
|
|
323
|
+
message: "acquired safe repo workspace before local edits",
|
|
324
|
+
meta: {
|
|
325
|
+
runtimeKind: selection.runtimeKind,
|
|
326
|
+
repoRoot: selection.repoRoot,
|
|
327
|
+
workspaceRoot: selection.workspaceRoot,
|
|
328
|
+
workspaceBranch: selection.workspaceBranch,
|
|
329
|
+
sourceBranch: selection.sourceBranch,
|
|
330
|
+
sourceCloneUrl: selection.sourceCloneUrl,
|
|
331
|
+
cleanupAfterMerge: selection.cleanupAfterMerge,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
return selection;
|
|
335
|
+
}
|
|
336
|
+
function resolveSafeRepoPath(options) {
|
|
337
|
+
const rawRequestedPath = options.requestedPath;
|
|
338
|
+
const repoRoot = path.resolve(options.repoRoot ?? (0, identity_1.getRepoRoot)());
|
|
339
|
+
if (!path.isAbsolute(rawRequestedPath) && !rawRequestedPath.startsWith("~")) {
|
|
340
|
+
const selection = activeSelection ?? ensureSafeRepoWorkspace(options);
|
|
341
|
+
return {
|
|
342
|
+
selection,
|
|
343
|
+
resolvedPath: path.resolve(selection.workspaceRoot, rawRequestedPath),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const requestedPath = path.resolve(rawRequestedPath);
|
|
347
|
+
if (activeSelection && requestedPath.startsWith(activeSelection.workspaceRoot + path.sep)) {
|
|
348
|
+
return { selection: activeSelection, resolvedPath: requestedPath };
|
|
349
|
+
}
|
|
350
|
+
if (requestedPath !== repoRoot && !requestedPath.startsWith(repoRoot + path.sep)) {
|
|
351
|
+
return { selection: activeSelection, resolvedPath: requestedPath };
|
|
352
|
+
}
|
|
353
|
+
const selection = ensureSafeRepoWorkspace(options);
|
|
354
|
+
const relativePath = requestedPath === repoRoot ? "" : path.relative(repoRoot, requestedPath);
|
|
355
|
+
const resolvedPath = relativePath ? path.join(selection.workspaceRoot, relativePath) : selection.workspaceRoot;
|
|
356
|
+
return { selection, resolvedPath };
|
|
357
|
+
}
|
|
358
|
+
function resolveSafeShellExecution(command, options = {}) {
|
|
359
|
+
const trimmed = command.trim();
|
|
360
|
+
if (!trimmed) {
|
|
361
|
+
return { selection: activeSelection, command };
|
|
362
|
+
}
|
|
363
|
+
if (activeSelection && command.includes(activeSelection.workspaceRoot)) {
|
|
364
|
+
return { selection: activeSelection, command, cwd: activeSelection.workspaceRoot };
|
|
365
|
+
}
|
|
366
|
+
const repoRoot = path.resolve(options.repoRoot ?? (0, identity_1.getRepoRoot)());
|
|
367
|
+
const mentionsRepoRoot = command.includes(repoRoot);
|
|
368
|
+
const shouldRoute = mentionsRepoRoot || looksRepoLocalShellCommand(trimmed);
|
|
369
|
+
if (!shouldRoute) {
|
|
370
|
+
return { selection: activeSelection, command };
|
|
371
|
+
}
|
|
372
|
+
const selection = ensureSafeRepoWorkspace(options);
|
|
373
|
+
const rewrittenCommand = mentionsRepoRoot
|
|
374
|
+
? command.split(repoRoot).join(selection.workspaceRoot)
|
|
375
|
+
: command;
|
|
376
|
+
return {
|
|
377
|
+
selection,
|
|
378
|
+
command: rewrittenCommand,
|
|
379
|
+
cwd: selection.workspaceRoot,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.listSessionActivity = listSessionActivity;
|
|
37
|
+
exports.findFreshestFriendSession = findFreshestFriendSession;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const runtime_1 = require("../nerves/runtime");
|
|
41
|
+
const DEFAULT_ACTIVE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
function activityPriority(source) {
|
|
43
|
+
return source === "friend-facing" ? 0 : 1;
|
|
44
|
+
}
|
|
45
|
+
function resolveFriendName(friendId, friendsDir, agentName) {
|
|
46
|
+
if (friendId === "self")
|
|
47
|
+
return agentName;
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs.readFileSync(path.join(friendsDir, `${friendId}.json`), "utf-8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
return parsed.name ?? friendId;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return friendId;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function parseFriendActivity(sessionPath) {
|
|
58
|
+
let mtimeMs;
|
|
59
|
+
try {
|
|
60
|
+
mtimeMs = fs.statSync(sessionPath).mtimeMs;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const raw = fs.readFileSync(sessionPath, "utf-8");
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
const explicit = parsed?.state?.lastFriendActivityAt;
|
|
69
|
+
if (typeof explicit === "string") {
|
|
70
|
+
const parsedMs = Date.parse(explicit);
|
|
71
|
+
if (Number.isFinite(parsedMs)) {
|
|
72
|
+
return {
|
|
73
|
+
lastActivityMs: parsedMs,
|
|
74
|
+
lastActivityAt: new Date(parsedMs).toISOString(),
|
|
75
|
+
activitySource: "friend-facing",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// fall back to file mtime below
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
lastActivityMs: mtimeMs,
|
|
85
|
+
lastActivityAt: new Date(mtimeMs).toISOString(),
|
|
86
|
+
activitySource: "mtime-fallback",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function listSessionActivity(query) {
|
|
90
|
+
const { sessionsDir, friendsDir, agentName, activeThresholdMs = DEFAULT_ACTIVE_THRESHOLD_MS, currentSession = null, } = query;
|
|
91
|
+
(0, runtime_1.emitNervesEvent)({
|
|
92
|
+
component: "daemon",
|
|
93
|
+
event: "daemon.session_activity_scan",
|
|
94
|
+
message: "scanning session activity",
|
|
95
|
+
meta: {
|
|
96
|
+
sessionsDir,
|
|
97
|
+
currentSession: currentSession ? `${currentSession.friendId}/${currentSession.channel}/${currentSession.key}` : null,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
if (!fs.existsSync(sessionsDir))
|
|
101
|
+
return [];
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const results = [];
|
|
104
|
+
let friendDirs;
|
|
105
|
+
try {
|
|
106
|
+
friendDirs = fs.readdirSync(sessionsDir);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
for (const friendId of friendDirs) {
|
|
112
|
+
const friendPath = path.join(sessionsDir, friendId);
|
|
113
|
+
let channels;
|
|
114
|
+
try {
|
|
115
|
+
channels = fs.readdirSync(friendPath);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
for (const channel of channels) {
|
|
121
|
+
const channelPath = path.join(friendPath, channel);
|
|
122
|
+
let keys;
|
|
123
|
+
try {
|
|
124
|
+
keys = fs.readdirSync(channelPath);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const keyFile of keys) {
|
|
130
|
+
if (!keyFile.endsWith(".json"))
|
|
131
|
+
continue;
|
|
132
|
+
const key = keyFile.replace(/\.json$/, "");
|
|
133
|
+
if (currentSession && friendId === currentSession.friendId && channel === currentSession.channel && key === currentSession.key) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const sessionPath = path.join(channelPath, keyFile);
|
|
137
|
+
const activity = parseFriendActivity(sessionPath);
|
|
138
|
+
if (!activity)
|
|
139
|
+
continue;
|
|
140
|
+
if (now - activity.lastActivityMs > activeThresholdMs)
|
|
141
|
+
continue;
|
|
142
|
+
results.push({
|
|
143
|
+
friendId,
|
|
144
|
+
friendName: resolveFriendName(friendId, friendsDir, agentName),
|
|
145
|
+
channel,
|
|
146
|
+
key,
|
|
147
|
+
sessionPath,
|
|
148
|
+
lastActivityAt: activity.lastActivityAt,
|
|
149
|
+
lastActivityMs: activity.lastActivityMs,
|
|
150
|
+
activitySource: activity.activitySource,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return results.sort((a, b) => {
|
|
156
|
+
const sourceDiff = activityPriority(a.activitySource) - activityPriority(b.activitySource);
|
|
157
|
+
if (sourceDiff !== 0)
|
|
158
|
+
return sourceDiff;
|
|
159
|
+
return b.lastActivityMs - a.lastActivityMs;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function findFreshestFriendSession(query) {
|
|
163
|
+
const { activeOnly = false, activeThresholdMs = DEFAULT_ACTIVE_THRESHOLD_MS, ...rest } = query;
|
|
164
|
+
const currentSession = rest.currentSession ?? null;
|
|
165
|
+
const all = activeOnly
|
|
166
|
+
? listSessionActivity({ ...rest, activeThresholdMs, currentSession })
|
|
167
|
+
: listSessionActivity({ ...rest, activeThresholdMs: Number.MAX_SAFE_INTEGER, currentSession });
|
|
168
|
+
return all.find((entry) => entry.friendId === query.friendId) ?? null;
|
|
169
|
+
}
|