crewly 1.20.171 → 1.20.173

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.
Files changed (83) hide show
  1. package/dist/backend/backend/src/constants.d.ts +43 -9
  2. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  3. package/dist/backend/backend/src/constants.js +48 -11
  4. package/dist/backend/backend/src/constants.js.map +1 -1
  5. package/dist/backend/backend/src/index.d.ts.map +1 -1
  6. package/dist/backend/backend/src/index.js +34 -2
  7. package/dist/backend/backend/src/index.js.map +1 -1
  8. package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +1 -1
  9. package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -1
  10. package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.js +22 -6
  11. package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.js.map +1 -1
  12. package/dist/backend/backend/src/services/harness/harness-exec.utils.d.ts.map +1 -1
  13. package/dist/backend/backend/src/services/harness/harness-exec.utils.js +1 -0
  14. package/dist/backend/backend/src/services/harness/harness-exec.utils.js.map +1 -1
  15. package/dist/backend/backend/src/services/harness/harness-login-probe.d.ts +65 -0
  16. package/dist/backend/backend/src/services/harness/harness-login-probe.d.ts.map +1 -0
  17. package/dist/backend/backend/src/services/harness/harness-login-probe.js +99 -0
  18. package/dist/backend/backend/src/services/harness/harness-login-probe.js.map +1 -0
  19. package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts +349 -113
  20. package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -1
  21. package/dist/backend/backend/src/services/harness/harness-relogin.service.js +792 -278
  22. package/dist/backend/backend/src/services/harness/harness-relogin.service.js.map +1 -1
  23. package/dist/backend/backend/src/services/harness/harness.types.d.ts +2 -0
  24. package/dist/backend/backend/src/services/harness/harness.types.d.ts.map +1 -1
  25. package/dist/backend/backend/src/services/harness/relogin-state.store.d.ts +61 -0
  26. package/dist/backend/backend/src/services/harness/relogin-state.store.d.ts.map +1 -0
  27. package/dist/backend/backend/src/services/harness/relogin-state.store.js +92 -0
  28. package/dist/backend/backend/src/services/harness/relogin-state.store.js.map +1 -0
  29. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts +27 -2
  30. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
  31. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js +78 -3
  32. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
  33. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.d.ts +1 -1
  34. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.js +1 -1
  35. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts +42 -19
  36. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts.map +1 -1
  37. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js +69 -31
  38. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js.map +1 -1
  39. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt-data.js +3 -3
  40. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt-data.js.map +1 -1
  41. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.js +1 -1
  42. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.js.map +1 -1
  43. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.renderer.d.ts +12 -11
  44. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.renderer.d.ts.map +1 -1
  45. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.renderer.js +21 -20
  46. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.renderer.js.map +1 -1
  47. package/dist/backend/backend/src/services/v3/request-status-update.subscriber.d.ts +1 -1
  48. package/dist/backend/backend/src/services/v3/request-status-update.subscriber.js +15 -15
  49. package/dist/backend/backend/src/services/v3/request-status-update.subscriber.js.map +1 -1
  50. package/dist/backend/build-info.json +2 -2
  51. package/dist/cli/backend/src/constants.d.ts +43 -9
  52. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  53. package/dist/cli/backend/src/constants.js +48 -11
  54. package/dist/cli/backend/src/constants.js.map +1 -1
  55. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +1 -1
  56. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -1
  57. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.js +22 -6
  58. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.js.map +1 -1
  59. package/dist/cli/backend/src/services/harness/harness-exec.utils.d.ts.map +1 -1
  60. package/dist/cli/backend/src/services/harness/harness-exec.utils.js +1 -0
  61. package/dist/cli/backend/src/services/harness/harness-exec.utils.js.map +1 -1
  62. package/dist/cli/backend/src/services/harness/harness-login-probe.d.ts +65 -0
  63. package/dist/cli/backend/src/services/harness/harness-login-probe.d.ts.map +1 -0
  64. package/dist/cli/backend/src/services/harness/harness-login-probe.js +99 -0
  65. package/dist/cli/backend/src/services/harness/harness-login-probe.js.map +1 -0
  66. package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts +349 -113
  67. package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -1
  68. package/dist/cli/backend/src/services/harness/harness-relogin.service.js +792 -278
  69. package/dist/cli/backend/src/services/harness/harness-relogin.service.js.map +1 -1
  70. package/dist/cli/backend/src/services/harness/harness.types.d.ts +2 -0
  71. package/dist/cli/backend/src/services/harness/harness.types.d.ts.map +1 -1
  72. package/dist/cli/backend/src/services/harness/relogin-state.store.d.ts +61 -0
  73. package/dist/cli/backend/src/services/harness/relogin-state.store.d.ts.map +1 -0
  74. package/dist/cli/backend/src/services/harness/relogin-state.store.js +92 -0
  75. package/dist/cli/backend/src/services/harness/relogin-state.store.js.map +1 -0
  76. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts +27 -2
  77. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
  78. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js +78 -3
  79. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
  80. package/frontend/dist/assets/{index-769ba7ac.css → index-15501404.css} +1 -1
  81. package/frontend/dist/assets/{index-212a6639.js → index-36e85ac8.js} +280 -280
  82. package/frontend/dist/index.html +2 -2
  83. package/package.json +1 -1
@@ -1,47 +1,51 @@
1
1
  /**
2
- * Harness re-login coordinator — Phase 2 of onboarding: re-login over Slack.
2
+ * Harness re-login coordinator — re-login from the owner's phone, with no
3
+ * agent involved.
3
4
  *
4
- * When a harness login expires, Crewly notices by itself, starts ONE login
5
- * broker session for that harness, DMs the owner what to do on their phone,
6
- * and resumes the agents that were stuck once the login succeeds. Nobody
7
- * has to touch the machine.
5
+ * The instance backend (never an agent: every agent may be stuck on the very
6
+ * login that expired) notices that a harness's login is gone, tells the owner
7
+ * once, runs the harness's login itself when the owner replies, relays the
8
+ * link and the code, and brings the agents back with the messages that
9
+ * waited for them.
8
10
  *
9
- * Inputs:
10
- * - {@link HarnessReloginService.reportExpiry} — called by the OAuth
11
- * re-login monitor when an agent's terminal output matches an expiry rule
12
- * (login-expiry-rules.ts), and by the periodic status check of the
13
- * orchestrator's harness ({@link HarnessReloginService.checkOrcHarness}).
14
- * - {@link HarnessReloginService.handleOwnerReply} — an owner's Slack DM
15
- * reply. A reply is consumed only when it belongs to a flow: Claude's
16
- * authorization code (while Claude's login is waiting for it and the reply
17
- * looks like a code), the retry keyword, or a reply to an unrecognised
18
- * login screen. Everything else goes through the normal chat path.
19
- * - The broker's `update` / `finished` events.
20
- * - {@link HarnessReloginService.startOwnerLogin} — the owner asked for a
21
- * login: 「重新登录 claude」 in the orc DM (parsed by
22
- * {@link HarnessReloginService.handleOwnerReply}, no LLM involved) or the
23
- * orchestrator's `harness-login` skill. Such a flow is *forced*: it runs
24
- * even when the harness is still logged in (switching accounts), skips the
25
- * silent API-key path, and answers in the conversation it was asked in.
11
+ * Flow per harness (at most one at a time):
26
12
  *
27
- * Rules:
28
- * - One flow per harness. Repeated detections only add stuck sessions. A
29
- * flow that failed is followed up by a new detection at most once per
30
- * {@link HARNESS_CONSTANTS.RELOGIN.REMIND_INTERVAL_MS} (the re-reminder),
31
- * or restarted at once when the owner replies `relogin` / `重新登录`.
32
- * When the last link simply expired unused (`timed_out`), the reminder is
33
- * a text DM asking the owner to reply when they are ready — it does NOT
34
- * start a new login: a device code lives 15 minutes, so a link sent while
35
- * the owner is away is wasted, and `codex login` revokes whatever
36
- * credentials exist when it starts (steamfun-ops 2026-09-30: five links at
37
- * night, all expired unused).
38
- * - A login is started only when the harness's own status says
39
- * `logged_out`; `unknown` is not enough, for the same revocation reason.
40
- * - If Crewly holds an API key for the harness it is used silently (no DM).
41
- * - Claude → `subscription` (`claude setup-token`), Codex → `device`.
42
- * - Secrets: Claude's code is passed to the broker and never stored, logged
43
- * or echoed; DMs are built from {@link LoginSession} snapshots, which never
44
- * contain a token, and the screen text is redacted again.
13
+ * 1. **Detect.** {@link HarnessReloginService.reportExpiry} — an agent's
14
+ * output or screen matched an expiry rule (login-expiry-rules.ts, via the
15
+ * OAuth re-login monitor), or the periodic check
16
+ * ({@link HarnessReloginService.checkHarnesses}) found a harness in use
17
+ * signed out. The periodic check covers every harness the orchestrator or
18
+ * a team member is configured on, so it works with zero agents running.
19
+ * 2. **Confirm.** The harness's status command, then — because Claude Code's
20
+ * stored credential survives an expiry — a live probe
21
+ * (harness-login-probe.ts). Only a confirmed `logged_out` goes on;
22
+ * `unknown` never does (`codex login` revokes a working login).
23
+ * 3. **Tell the owner once.** A stored API key is used silently if Crewly has
24
+ * one. Otherwise one DM — "Claude Code on <machine> is signed out, so N
25
+ * agents can't work. Reply `login` …" — then re-reminders with backoff
26
+ * (3 h, doubling, 24 h cap), persisted so restarts do not re-send it. No
27
+ * login is started yet: a link sent while the owner is away expires unused.
28
+ * 4. **The owner replies `login`** (or 「重新登录」, `relogin claude`, …) in
29
+ * the DM. The Slack bridge offers owner DMs to
30
+ * {@link HarnessReloginService.handleOwnerReply} before anything else, so
31
+ * this works with no agent awake. The login broker runs the harness's
32
+ * login in its own PTY (`claude setup-token` / `codex login --device-auth`)
33
+ * and the link (and Codex's one-time code) is sent back.
34
+ * 5. **The code.** For Claude the owner replies with the code from the page;
35
+ * it is typed into the waiting login. A rejected code is reported once; an
36
+ * expired link puts the flow back to "signed out" ("reply `login` for a
37
+ * new link").
38
+ * 6. **Success** is confirmed with the probe, then the stuck agents are
39
+ * restarted (conversation resumed) and the owner messages that waited are
40
+ * re-delivered ({@link HarnessReloginDeps.onLoginRestored}). One DM says so.
41
+ *
42
+ * Owner-requested logins ({@link HarnessReloginService.startOwnerLogin}:
43
+ * 「重新登录 claude」 / "relogin codex" / the orchestrator's `harness-login`
44
+ * skill) start the broker at once, even while logged in (account switch).
45
+ *
46
+ * Secrets: Claude's code is passed to the broker and never stored, logged
47
+ * or echoed; DMs are built from {@link LoginSession} snapshots, which never
48
+ * contain a token, and the screen text is redacted again.
45
49
  *
46
50
  * @module services/harness/harness-relogin.service
47
51
  */
@@ -49,6 +53,7 @@ import type { HarnessApiKeyService } from './harness-api-key.service.js';
49
53
  import { type HarnessCredentialsStore } from './harness-credentials.store.js';
50
54
  import { type HarnessId, type HarnessLogger, type LoginSession, type LoginState, type ReloginPending } from './harness.types.js';
51
55
  import { type LoginBrokerService } from './login-broker.service.js';
56
+ import { type ReloginStateStore } from './relogin-state.store.js';
52
57
  /** Where an expiry report came from. */
53
58
  export type ExpirySource = 'output' | 'screen' | 'status';
54
59
  /** An expired-login report. */
@@ -58,31 +63,35 @@ export interface ExpiryReport {
58
63
  sessionName: string | null;
59
64
  /**
60
65
  * `output`: a live PTY chunk; `screen`: the periodic screen sweep (may show
61
- * old scrollback); `status`: the harness's own status command
66
+ * old scrollback); `status`: the harness's own status command or probe
62
67
  */
63
68
  source: ExpirySource;
64
69
  }
65
70
  /**
66
- * Where an owner-requested login answers: the Slack conversation (and thread)
67
- * the owner asked in. `agentSession` is set when that conversation is a DM
68
- * with an agent's own bot (the orc's "Crewly Orc" app), whose token must be
69
- * used to post there.
71
+ * A Slack conversation (and thread) to answer in. `agentSession` is set when
72
+ * that conversation is a DM with an agent's own bot (the orc's "Crewly Orc"
73
+ * app), whose token must be used to post there.
70
74
  */
71
75
  export interface ReloginReplyTarget {
72
76
  channelId: string;
73
77
  threadTs?: string;
74
78
  agentSession?: string;
75
79
  }
80
+ /**
81
+ * Result of a DM: false when nothing was delivered; true, or the
82
+ * conversation it landed in (so a code reply can be matched to it).
83
+ */
84
+ export type ReloginDelivery = boolean | ReloginReplyTarget;
76
85
  /** Sends a Slack DM to the owner. */
77
86
  export interface ReloginOwnerNotifier {
78
87
  /**
79
88
  * Send a DM to the owner.
80
89
  *
81
90
  * @param text - Slack mrkdwn text (never contains a secret)
82
- * @param target - Conversation to answer in; absent = the owner's DM with the master bot
83
- * @returns True when delivered
91
+ * @param target - Conversation to answer in; absent = this machine's own DM with the owner
92
+ * @returns Where it was delivered (or true), false when it was not
84
93
  */
85
- sendToOwner(text: string, target?: ReloginReplyTarget | null): Promise<boolean>;
94
+ sendToOwner(text: string, target?: ReloginReplyTarget | null): Promise<ReloginDelivery>;
86
95
  /**
87
96
  * Whether a DM can be sent right now (Slack connected).
88
97
  *
@@ -92,11 +101,13 @@ export interface ReloginOwnerNotifier {
92
101
  }
93
102
  /** What started a flow. */
94
103
  export type ReloginTrigger = 'expiry' | 'owner';
104
+ /** Where an owner reply was written: the orc's / master DM, or another agent's bot DM. */
105
+ export type OwnerReplyScope = 'orc' | 'agent';
95
106
  /** Options of an owner-requested login. */
96
107
  export interface OwnerLoginOptions {
97
108
  /** The owner wants a different account (only changes the wording) */
98
109
  switchAccount?: boolean;
99
- /** Conversation to answer in (null = the owner's master-bot DM) */
110
+ /** Conversation to answer in (null = this machine's DM with the owner) */
100
111
  replyTarget?: ReloginReplyTarget | null;
101
112
  /** Who relayed the request, for logs */
102
113
  requestedBy: 'owner_dm' | 'orchestrator';
@@ -114,7 +125,7 @@ export interface OwnerLoginUnsupported {
114
125
  /** The harness has no link login (Antigravity: API key; Gemini: enterprise only) */
115
126
  status: 'no_broker_login';
116
127
  harnessId: HarnessId;
117
- /** What to tell the owner, in Chinese */
128
+ /** What to tell the owner */
118
129
  message: string;
119
130
  }
120
131
  /** Result of {@link HarnessReloginService.startOwnerLogin}. */
@@ -139,6 +150,14 @@ export interface ReloginAgentResumer {
139
150
  failed: string[];
140
151
  }>;
141
152
  }
153
+ /** An agent configured on this machine (running or not). */
154
+ export interface ConfiguredAgent {
155
+ sessionName: string;
156
+ /** Runtime type (= harness id for the harnesses Crewly can log in) */
157
+ harnessId: string;
158
+ /** Name the owner knows ("Ella") */
159
+ displayName?: string;
160
+ }
142
161
  /** The slice of the login broker the coordinator uses. */
143
162
  export type ReloginBroker = Pick<LoginBrokerService, 'start' | 'get' | 'input' | 'cancel' | 'on'>;
144
163
  /** Injectable dependencies. */
@@ -146,15 +165,40 @@ export interface HarnessReloginDeps {
146
165
  broker: ReloginBroker;
147
166
  credentials: Pick<HarnessCredentialsStore, 'read' | 'getClaudeCredentialKind'>;
148
167
  apiKeys: Pick<HarnessApiKeyService, 'submit'>;
149
- /** The harness's login state from its own status command */
168
+ /** The harness's login state from its own status command (cheap; a stored Claude credential reads as logged in) */
150
169
  checkLoginState: (harnessId: HarnessId) => Promise<LoginState>;
170
+ /**
171
+ * Live sign-in probe (harness-login-probe.ts): `logged_out` / `logged_in`
172
+ * when it could tell, `unknown` otherwise. Absent = status only.
173
+ */
174
+ verifyLogin?: (harnessId: HarnessId) => Promise<LoginState>;
151
175
  /** The orchestrator's harness (null before setup recorded one) */
152
176
  getOrcHarness: () => Promise<string | null>;
177
+ /** Every agent configured on this machine, running or not (orc included) */
178
+ listAgents?: () => Promise<ConfiguredAgent[]>;
179
+ /** Whether a live agent session is sitting at a sign-in screen (OAuth monitor flag) */
180
+ sessionNeedsLogin?: (sessionName: string) => boolean;
181
+ /**
182
+ * After a login came back and the agents were restarted: re-deliver what
183
+ * waited for them (owner-message watchdog). Returns how many messages.
184
+ */
185
+ onLoginRestored?: (harnessId: HarnessId, resumed: readonly string[]) => Promise<number> | number;
186
+ /** This machine's name, for the owner ("iriss-air.lan") */
187
+ machineName?: () => string;
188
+ /** Persisted notice / backoff state */
189
+ state?: ReloginStateStore;
153
190
  notifier?: ReloginOwnerNotifier | null;
154
191
  resumer?: ReloginAgentResumer | null;
155
192
  now?: () => number;
156
193
  logger?: HarnessLogger;
157
194
  }
195
+ /**
196
+ * The word the owner types after `relogin` for a harness.
197
+ *
198
+ * @param harnessId - Harness id
199
+ * @returns e.g. "claude"
200
+ */
201
+ export declare function harnessCommandWord(harnessId: HarnessId): string;
158
202
  /**
159
203
  * Whether a reply asks to start the login over.
160
204
  *
@@ -162,6 +206,19 @@ export interface HarnessReloginDeps {
162
206
  * @returns True for `relogin`, `re-login` or `重新登录`
163
207
  */
164
208
  export declare function isRetryKeyword(text: string): boolean;
209
+ /**
210
+ * Whether a bare reply asks to sign in (`login`, `sign in`, 「登录」, the retry keywords).
211
+ *
212
+ * @param text - Owner reply
213
+ * @returns True when the whole reply is a login keyword
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * isLoginKeyword('Login!'); // true
218
+ * isLoginKeyword('login to gmail'); // false
219
+ * ```
220
+ */
221
+ export declare function isLoginKeyword(text: string): boolean;
165
222
  /**
166
223
  * Strip formatting Slack clients add around a pasted value (backticks).
167
224
  *
@@ -178,10 +235,10 @@ export declare function unwrapReply(text: string): string;
178
235
  */
179
236
  export declare function looksLikeAuthCode(text: string): boolean;
180
237
  /**
181
- * "2 agents are waiting: a, b" — capped list of waiting agents.
238
+ * "Ella, Rex and 3 more" — capped list of agent names.
182
239
  *
183
- * @param agents - Session names
184
- * @returns Sentence fragment
240
+ * @param agents - Agent names
241
+ * @returns List text ('' when empty)
185
242
  */
186
243
  export declare function describeWaitingAgents(agents: readonly string[]): string;
187
244
  /**
@@ -191,37 +248,55 @@ export declare function describeWaitingAgents(agents: readonly string[]): string
191
248
  */
192
249
  export declare const SCREEN_REPLY_PREFIXES: readonly ["输入", "input"];
193
250
  /**
194
- * The text after a screen-reply prefix ("输入 1" → "1"), or null.
251
+ * The text after a screen-reply prefix ("input 1" → "1"), or null.
195
252
  *
196
253
  * @param reply - Owner reply (already unwrapped)
197
254
  * @returns Text to type, or null when the reply has no prefix
198
255
  */
199
256
  export declare function stripScreenReplyPrefix(reply: string): string | null;
200
- /** How a DM introduces the login. */
257
+ /** How a link DM introduces the login. */
201
258
  export interface LoginDmWording {
202
- /** The owner asked for this login (not an expiry) */
259
+ /** The owner asked for this login (not an expiry) — kept for callers; the header is the same */
203
260
  ownerRequested?: boolean;
204
261
  /** The owner asked for a different account */
205
262
  switchAccount?: boolean;
263
+ /** This machine's name */
264
+ machine?: string;
206
265
  }
266
+ /**
267
+ * The first "signed out" DM, a re-reminder, or the note after a link expired unused.
268
+ *
269
+ * @param harnessId - Harness
270
+ * @param machine - This machine's name
271
+ * @param agents - Names of the agents that run on it
272
+ * @param kind - `notice` (first), `reminder`, or `link_expired`
273
+ * @returns Slack mrkdwn text
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * formatSignedOutDm('claude-code', 'iriss-air.lan', ['Ella', 'Rex'], 'notice');
278
+ * // "*Claude Code on iriss-air.lan is signed out*, so 2 agents can't work (Ella, Rex).\nReply `login` …"
279
+ * ```
280
+ */
281
+ export declare function formatSignedOutDm(harnessId: HarnessId, machine: string, agents: readonly string[], kind?: 'notice' | 'reminder' | 'link_expired'): string;
207
282
  /**
208
283
  * DM with the sign-in link (and Codex's one-time code on its own line).
209
284
  *
210
285
  * @param session - Broker session (never contains a secret)
211
- * @param waiting - Agents waiting on the login
212
- * @param wording - Owner-requested / account-switch wording (default: expiry)
286
+ * @param _waiting - Agents waiting on the login (unused; kept for callers)
287
+ * @param wording - Account switch / machine
213
288
  * @returns Slack mrkdwn text
214
289
  */
215
- export declare function formatLinkDm(session: LoginSession, waiting: readonly string[], wording?: LoginDmWording): string;
290
+ export declare function formatLinkDm(session: LoginSession, _waiting?: readonly string[], wording?: LoginDmWording): string;
216
291
  /**
217
292
  * DM for a login screen the broker did not recognise.
218
293
  *
219
294
  * @param session - Broker session
220
- * @param waiting - Agents waiting on the login
221
- * @param wording - Owner-requested / account-switch wording (default: expiry)
295
+ * @param _waiting - Agents waiting on the login (unused; kept for callers)
296
+ * @param wording - Account switch / machine
222
297
  * @returns Slack mrkdwn text
223
298
  */
224
- export declare function formatScreenDm(session: LoginSession, waiting: readonly string[], wording?: LoginDmWording): string;
299
+ export declare function formatScreenDm(session: LoginSession, _waiting?: readonly string[], wording?: LoginDmWording): string;
225
300
  /**
226
301
  * DM after the harness rejected the owner's reply (e.g. a wrong code).
227
302
  *
@@ -230,27 +305,30 @@ export declare function formatScreenDm(session: LoginSession, waiting: readonly
230
305
  */
231
306
  export declare function formatRejectedDm(session: LoginSession): string;
232
307
  /**
233
- * DM after the login succeeded and the agents were restarted.
308
+ * DM after a login came back and the agents were restarted.
234
309
  *
235
310
  * @param harnessId - Harness
236
311
  * @param result - Resume result
312
+ * @param redelivered - Owner messages re-delivered to them
313
+ * @param machine - This machine's name
237
314
  * @returns Slack mrkdwn text
238
315
  */
239
316
  export declare function formatSuccessDm(harnessId: HarnessId, result: {
240
317
  resumed: string[];
241
318
  failed: string[];
242
- }): string;
319
+ }, redelivered?: number, machine?: string): string;
243
320
  /**
244
321
  * One-line DM after an owner-requested login succeeded.
245
322
  *
246
323
  * @param harnessId - Harness
247
324
  * @param result - Which agents were restarted onto the new login
325
+ * @param redelivered - Owner messages re-delivered to them
248
326
  * @returns Slack mrkdwn text
249
327
  */
250
328
  export declare function formatOwnerSuccessDm(harnessId: HarnessId, result: {
251
329
  resumed: string[];
252
330
  failed: string[];
253
- }): string;
331
+ }, redelivered?: number): string;
254
332
  /**
255
333
  * Reply to a login request that named no harness Crewly can log in.
256
334
  *
@@ -266,7 +344,7 @@ export declare function formatWhichHarnessDm(name: string | null): string;
266
344
  */
267
345
  export declare function formatNoBrokerLoginDm(harnessId: HarnessId): string;
268
346
  /**
269
- * DM after the login failed or timed out (sent once per failure).
347
+ * DM after the login failed (sent once per failure).
270
348
  *
271
349
  * @param harnessId - Harness
272
350
  * @param reason - Broker message (redacted here)
@@ -274,35 +352,50 @@ export declare function formatNoBrokerLoginDm(harnessId: HarnessId): string;
274
352
  */
275
353
  export declare function formatFailureDm(harnessId: HarnessId, reason: string | null): string;
276
354
  /**
277
- * Re-reminder text for a login whose last link expired unused. It carries no
278
- * link: a new one is made when the owner replies, so it is fresh when used.
355
+ * Re-reminder text for a login whose last link expired unused.
279
356
  *
280
357
  * @param harnessId - Harness
281
358
  * @param waiting - Agents waiting on the login
359
+ * @param machine - This machine's name
282
360
  * @returns DM text
283
361
  */
284
- export declare function formatReminderDm(harnessId: HarnessId, waiting: readonly string[]): string;
285
- /** Coordinates Slack re-login flows, one per harness. */
362
+ export declare function formatReminderDm(harnessId: HarnessId, waiting: readonly string[], machine?: string): string;
363
+ /** Coordinates re-login flows, one per harness. */
286
364
  export declare class HarnessReloginService {
287
365
  private readonly broker;
288
366
  private readonly credentials;
289
367
  private readonly apiKeys;
290
368
  private readonly checkLoginState;
369
+ private readonly verifyLogin;
291
370
  private readonly getOrcHarness;
371
+ private listAgents;
372
+ private sessionNeedsLogin;
373
+ private onLoginRestored;
374
+ private readonly machineName;
375
+ private readonly state;
292
376
  private notifier;
293
377
  private resumer;
294
378
  private readonly now;
295
379
  private readonly logger;
296
380
  private readonly flows;
381
+ /** Harnesses whose expiry is being confirmed → sessions reported meanwhile */
382
+ private readonly confirming;
383
+ /** Harnesses whose re-reminder is being prepared */
384
+ private readonly reminding;
297
385
  /** Expiry reports are ignored until then (resumed transcripts repeat old errors) */
298
386
  private readonly quietUntil;
299
387
  /** Last silent API-key recovery per harness */
300
388
  private readonly silentKeyAt;
301
389
  /** Sessions restarted after a login: screen-sweep reports are ignored until live output reports again */
302
390
  private readonly mutedScreens;
303
- /** Harnesses the status check has seen logged in */
304
- private readonly seenLoggedIn;
391
+ /** Last probe result per harness */
392
+ private readonly probeCache;
393
+ /** Probes in flight per harness */
394
+ private readonly probeInFlight;
395
+ /** Agents configured on this machine, from the last {@link listAgents} */
396
+ private agents;
305
397
  private statusTimer;
398
+ private firstCheckTimer;
306
399
  private statusCheckInFlight;
307
400
  /**
308
401
  * @param deps - Dependencies
@@ -320,6 +413,24 @@ export declare class HarnessReloginService {
320
413
  * @param resumer - Resumer, or null
321
414
  */
322
415
  setResumer(resumer: ReloginAgentResumer | null): void;
416
+ /**
417
+ * Set what re-delivers the owner messages that waited for a login.
418
+ *
419
+ * @param handler - Handler, or null
420
+ */
421
+ setLoginRestoredHandler(handler: HarnessReloginDeps['onLoginRestored'] | null): void;
422
+ /**
423
+ * Set how the configured agents (running or not) are listed.
424
+ *
425
+ * @param lister - Lister, or null
426
+ */
427
+ setAgentLister(lister: HarnessReloginDeps['listAgents'] | null): void;
428
+ /**
429
+ * Set the per-session sign-in flag lookup (OAuth monitor).
430
+ *
431
+ * @param lookup - Lookup, or null
432
+ */
433
+ setSessionNeedsLogin(lookup: HarnessReloginDeps['sessionNeedsLogin'] | null): void;
323
434
  /**
324
435
  * Report an expired harness login.
325
436
  *
@@ -332,17 +443,22 @@ export declare class HarnessReloginService {
332
443
  /**
333
444
  * Offer an owner's Slack DM to the re-login coordinator.
334
445
  *
335
- * Consumed, in this order: the retry keyword (while a flow exists), a
336
- * login code or screen reply for a running flow, and finally an owner
337
- * login request (「重新登录 claude」, "relogin codex" — see
446
+ * Consumed, in this order: a login keyword (`login`, 「重新登录」…) while a
447
+ * harness is signed out or signing in; a login code or screen reply for a
448
+ * running flow (only in the conversation the link went to); and finally an
449
+ * owner login request (「重新登录 claude」, "relogin codex" — see
338
450
  * {@link parseOwnerLoginRequest}), which starts a forced login answered in
339
451
  * `target`.
340
452
  *
453
+ * In another agent's bot DM (`scope: 'agent'`) only a request that names a
454
+ * harness, or a reply to a flow, is taken — the rest is that agent's mail.
455
+ *
341
456
  * @param text - Reply text (never logged)
342
457
  * @param target - Conversation the owner wrote in (answers go there)
458
+ * @param scope - `orc` (orc / master DM) or `agent` (another agent's bot DM)
343
459
  * @returns True when consumed; the caller must then NOT pass it on as a chat message
344
460
  */
345
- handleOwnerReply(text: string, target?: ReloginReplyTarget | null): boolean;
461
+ handleOwnerReply(text: string, target?: ReloginReplyTarget | null, scope?: OwnerReplyScope): boolean;
346
462
  /**
347
463
  * Start a login the owner asked for, through the broker: forced (runs
348
464
  * even when the harness is still logged in — the owner may be switching
@@ -354,6 +470,21 @@ export declare class HarnessReloginService {
354
470
  * @returns What happened (never throws)
355
471
  */
356
472
  startOwnerLogin(harnessId: HarnessId, options: OwnerLoginOptions): OwnerLoginResult;
473
+ /**
474
+ * Harnesses confirmed signed out on this machine (persisted).
475
+ *
476
+ * @param harnessId - Harness
477
+ * @returns True while the owner has not signed it in again
478
+ */
479
+ isSignedOut(harnessId: HarnessId): boolean;
480
+ /**
481
+ * The signed-out harness an agent is waiting on, if any — for the
482
+ * owner-message watchdog, which must not wake an agent onto a dead login.
483
+ *
484
+ * @param sessionName - Agent session
485
+ * @returns The harness, or null when its harness is not signed out
486
+ */
487
+ signedOutHarnessOf(sessionName: string): HarnessId | null;
357
488
  /**
358
489
  * Act on a parsed owner login request from the DM.
359
490
  *
@@ -362,10 +493,11 @@ export declare class HarnessReloginService {
362
493
  */
363
494
  private handleOwnerLoginRequest;
364
495
  /**
365
- * Offer a DM to the running flows: the retry keyword, a login code, or a
366
- * reply to an unrecognised screen.
496
+ * Offer a DM to the flows: a login keyword, a login code, or a reply to
497
+ * an unrecognised screen.
367
498
  *
368
499
  * @param text - Reply text (never logged)
500
+ * @param target - Conversation it was written in
369
501
  * @returns True when consumed
370
502
  */
371
503
  private consumeFlowReply;
@@ -377,36 +509,102 @@ export declare class HarnessReloginService {
377
509
  */
378
510
  getPending(harnessId: HarnessId): ReloginPending | null;
379
511
  /**
380
- * Check the orchestrator's harness with its own status command, and
381
- * report an expiry when it says logged out. Only a harness that was seen
382
- * logged in, or that agents are running on, counts (a machine that was
383
- * never logged in is onboarding's job, not a re-login).
512
+ * Check every harness in use on this machine — the orchestrator's and every
513
+ * configured agent's, running or not — and report the ones that are signed
514
+ * out. A harness the status command calls logged in is probed for real now
515
+ * and then ({@link HARNESS_CONSTANTS.RELOGIN.PROBE_INTERVAL_MS}), because a
516
+ * stored Claude credential survives its expiry. Signed-out harnesses get
517
+ * their (backed-off) re-reminder here.
384
518
  *
385
519
  * @returns Resolves when the check is done; never rejects
386
520
  */
521
+ checkHarnesses(): Promise<void>;
522
+ /**
523
+ * Compatibility name of {@link checkHarnesses}.
524
+ *
525
+ * @returns Resolves when the check is done
526
+ */
387
527
  checkOrcHarness(): Promise<void>;
388
528
  /**
389
- * Start the periodic status check of the orchestrator's harness.
529
+ * One harness of the periodic check.
530
+ *
531
+ * @param harnessId - Harness
532
+ * @param inUse - The orchestrator or a configured agent runs on it
533
+ */
534
+ private checkOne;
535
+ /**
536
+ * Start the periodic check (first run shortly after boot, so a machine
537
+ * that comes up signed out says so without waiting a full interval).
390
538
  *
391
539
  * @param intervalMs - Cadence (defaults to STATUS_CHECK_INTERVAL_MS)
540
+ * @param firstDelayMs - Delay of the first check (default 60 s)
392
541
  */
393
- start(intervalMs?: number): void;
542
+ start(intervalMs?: number, firstDelayMs?: number): void;
394
543
  /** Stop the periodic check and forget every flow's timers. */
395
544
  stop(): void;
396
545
  /**
397
- * Start a flow: silent API key if Crewly holds one, else a broker login.
546
+ * Confirm a suspected expiry, then tell the owner (or use a stored key).
398
547
  *
399
548
  * @param harnessId - Harness
400
- * @param stuck - Sessions known to be stuck
401
- * @param options - Trigger, wording and reply target (default: a detected expiry)
402
549
  */
403
- private begin;
550
+ private confirmExpiry;
404
551
  /**
405
- * Body of a flow (async part of {@link begin}).
552
+ * Login state: the status command, then the live probe when the status
553
+ * command cannot see an expiry (it says logged in / unknown).
554
+ *
555
+ * @param harnessId - Harness
556
+ * @param fresh - Ignore a cached probe result
557
+ * @returns The best-known state
558
+ */
559
+ private currentState;
560
+ /**
561
+ * Run (or reuse) the live probe.
562
+ *
563
+ * @param harnessId - Harness
564
+ * @param fresh - Ignore a cached result
565
+ * @returns Probe result (`unknown` when there is no probe)
566
+ */
567
+ private probe;
568
+ /**
569
+ * Re-remind the owner of a signed-out harness once its backoff elapsed. If
570
+ * the harness is signed in again by then (the owner did it on the
571
+ * machine or in Setup), the flow finishes and the agents resume instead.
572
+ *
573
+ * @param flow - A signed-out flow
574
+ */
575
+ private maybeRemind;
576
+ /**
577
+ * Whether the backoff allows another "signed out" DM for a harness.
578
+ *
579
+ * @param harnessId - Harness
580
+ * @returns True when no notice was sent yet, or the backoff elapsed
581
+ */
582
+ private noticeDue;
583
+ /**
584
+ * Send the "signed out" DM (first notice or re-reminder), within the backoff.
406
585
  *
407
586
  * @param flow - The flow
587
+ * @param kind - Which text
408
588
  */
409
- private run;
589
+ private sendSignedOutNotice;
590
+ /**
591
+ * Replace a harness's flow with a new one and start its broker session.
592
+ *
593
+ * @param harnessId - Harness
594
+ * @param previous - The flow it replaces (its stuck agents are kept), or null
595
+ * @param options - Trigger, wording and reply target
596
+ */
597
+ private replaceAndStart;
598
+ /**
599
+ * A new flow record.
600
+ *
601
+ * @param harnessId - Harness
602
+ * @param stuck - Sessions known to be stuck
603
+ * @param options - Trigger, wording and reply target
604
+ * @param phase - Initial phase
605
+ * @returns The flow (not registered)
606
+ */
607
+ private newFlow;
410
608
  /**
411
609
  * Start (or adopt) the harness's broker login session for a flow.
412
610
  *
@@ -435,32 +633,73 @@ export declare class HarnessReloginService {
435
633
  */
436
634
  private handleFinished;
437
635
  /**
438
- * The login is back: restart the stuck agents and tell the owner.
636
+ * A login finished outside any flow (dashboard chip, Setup, CLI): restart
637
+ * only the sessions that sit at a sign-in screen and re-deliver what
638
+ * waited for them. Working agents of the harness are left alone.
639
+ *
640
+ * @param harnessId - Harness that was signed in
641
+ */
642
+ private adoptOutsideLogin;
643
+ /**
644
+ * The broker reported success: confirm it with the probe, then finish.
645
+ *
646
+ * @param flow - The flow
647
+ */
648
+ private confirmAndSucceed;
649
+ /**
650
+ * The login is back: restart the stuck agents, re-deliver what waited,
651
+ * and tell the owner.
439
652
  *
440
653
  * @param flow - The flow
441
654
  * @param notify - DM the owner (false for the silent API-key path)
442
655
  */
443
656
  private succeed;
444
657
  /**
445
- * Re-reminder after a link expired unused: if the harness is logged in by
446
- * now (the owner signed in another way), finish the flow and resume the
447
- * stuck agents; otherwise send a text reminder. No login is started here —
448
- * the owner's `relogin` reply starts one while they are there to use it.
658
+ * Sessions to restart after a login: the ones whose output matched plus
659
+ * every live session of the harness (they all hold the dead login).
449
660
  *
450
- * @param flow - A failed flow whose last session timed out
661
+ * @param flow - The flow
662
+ * @returns Session names
451
663
  */
452
- private remind;
664
+ private sessionsToRestart;
453
665
  /**
454
- * Start a flow over (retry keyword or re-reminder).
666
+ * Names of the agents that run on a flow's harness, for the owner: every
667
+ * configured agent on it, else the stuck / live ones.
455
668
  *
456
- * @param flow - The flow to replace
669
+ * @param flow - The flow
670
+ * @returns Display names
457
671
  */
458
- private restart;
672
+ private agentNamesOn;
673
+ /**
674
+ * Re-read the configured agents.
675
+ *
676
+ * @returns The agents (the last known list when the read fails)
677
+ */
678
+ private refreshAgents;
679
+ /**
680
+ * Harness an agent session runs on.
681
+ *
682
+ * @param sessionName - Agent session
683
+ * @returns Harness id, or null when unknown
684
+ */
685
+ private harnessOfSession;
686
+ /**
687
+ * Record that a harness is signed out (first time only keeps the date).
688
+ *
689
+ * @param harnessId - Harness
690
+ */
691
+ private markSignedOut;
692
+ /**
693
+ * Remember that a harness was seen signed in (written at most hourly).
694
+ *
695
+ * @param harnessId - Harness
696
+ */
697
+ private markSeenLoggedIn;
459
698
  /**
460
699
  * DM wording for a flow.
461
700
  *
462
701
  * @param flow - The flow
463
- * @returns Owner-requested / account-switch flags
702
+ * @returns Account-switch flag and machine name
464
703
  */
465
704
  private wordingOf;
466
705
  /**
@@ -477,25 +716,21 @@ export declare class HarnessReloginService {
477
716
  */
478
717
  private sendScreenIfUnrecognised;
479
718
  /**
480
- * Agents waiting on a flow: the stuck ones, else every session of the harness.
481
- *
482
- * @param flow - The flow
483
- * @returns Session names
484
- */
485
- private waitingAgents;
486
- /**
487
- * DM the owner (logs when no notifier is wired; never logs the text).
719
+ * DM the owner in the flow's conversation, and remember where it landed
720
+ * so later DMs (the link, the result) follow it.
488
721
  *
489
722
  * @param flow - The flow
490
723
  * @param text - DM text
724
+ * @returns The delivery
491
725
  */
492
726
  private dm;
493
727
  /**
494
728
  * Send a message to the owner (logs when no notifier is wired; never logs the text).
495
729
  *
496
730
  * @param text - DM text
497
- * @param target - Conversation to answer in (null = master-bot DM)
731
+ * @param target - Conversation to answer in (null = this machine's DM)
498
732
  * @param harnessId - Harness, for logs
733
+ * @returns The delivery (false when not delivered)
499
734
  */
500
735
  private notify;
501
736
  /**
@@ -514,8 +749,9 @@ export declare class HarnessReloginService {
514
749
  }
515
750
  /**
516
751
  * The backend's re-login coordinator, bound to the backend harness service
517
- * (its broker, credentials, API keys and status). Registers itself as the
518
- * `reloginPending` source of `GET /api/harness`.
752
+ * (its broker, credentials, API keys and status), the live sign-in probe and
753
+ * the persisted notice state. Registers itself as the `reloginPending`
754
+ * source of `GET /api/harness`.
519
755
  *
520
756
  * @returns The singleton
521
757
  */