@a1hvdy/cc-openclaw 0.24.0 → 0.25.1
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/src/channels/telegram-mirror/burst-accumulator.d.ts +96 -0
- package/dist/src/channels/telegram-mirror/burst-accumulator.js +130 -0
- package/dist/src/channels/telegram-mirror/burst-accumulator.js.map +1 -0
- package/dist/src/channels/telegram-mirror/callback-mapping.d.ts +61 -0
- package/dist/src/channels/telegram-mirror/callback-mapping.js +99 -0
- package/dist/src/channels/telegram-mirror/callback-mapping.js.map +1 -0
- package/dist/src/channels/telegram-mirror/card-renderer.d.ts +74 -0
- package/dist/src/channels/telegram-mirror/card-renderer.js +131 -0
- package/dist/src/channels/telegram-mirror/card-renderer.js.map +1 -0
- package/dist/src/channels/telegram-mirror/commands.d.ts +142 -0
- package/dist/src/channels/telegram-mirror/commands.js +389 -0
- package/dist/src/channels/telegram-mirror/commands.js.map +1 -0
- package/dist/src/channels/telegram-mirror/compose-buffer.d.ts +71 -0
- package/dist/src/channels/telegram-mirror/compose-buffer.js +110 -0
- package/dist/src/channels/telegram-mirror/compose-buffer.js.map +1 -0
- package/dist/src/channels/telegram-mirror/cost-views.d.ts +58 -0
- package/dist/src/channels/telegram-mirror/cost-views.js +121 -0
- package/dist/src/channels/telegram-mirror/cost-views.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/callback-data-overflow.d.ts +21 -0
- package/dist/src/channels/telegram-mirror/failure/callback-data-overflow.js +30 -0
- package/dist/src/channels/telegram-mirror/failure/callback-data-overflow.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/gateway-down.d.ts +15 -0
- package/dist/src/channels/telegram-mirror/failure/gateway-down.js +27 -0
- package/dist/src/channels/telegram-mirror/failure/gateway-down.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/in-flight-conflict.d.ts +15 -0
- package/dist/src/channels/telegram-mirror/failure/in-flight-conflict.js +27 -0
- package/dist/src/channels/telegram-mirror/failure/in-flight-conflict.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/index.d.ts +23 -0
- package/dist/src/channels/telegram-mirror/failure/index.js +35 -0
- package/dist/src/channels/telegram-mirror/failure/index.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/model-5xx.d.ts +16 -0
- package/dist/src/channels/telegram-mirror/failure/model-5xx.js +24 -0
- package/dist/src/channels/telegram-mirror/failure/model-5xx.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/network-blip.d.ts +17 -0
- package/dist/src/channels/telegram-mirror/failure/network-blip.js +25 -0
- package/dist/src/channels/telegram-mirror/failure/network-blip.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/pool-exhausted-fallback.d.ts +15 -0
- package/dist/src/channels/telegram-mirror/failure/pool-exhausted-fallback.js +24 -0
- package/dist/src/channels/telegram-mirror/failure/pool-exhausted-fallback.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/rate-limit.d.ts +16 -0
- package/dist/src/channels/telegram-mirror/failure/rate-limit.js +26 -0
- package/dist/src/channels/telegram-mirror/failure/rate-limit.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/returning-after-24h.d.ts +14 -0
- package/dist/src/channels/telegram-mirror/failure/returning-after-24h.js +33 -0
- package/dist/src/channels/telegram-mirror/failure/returning-after-24h.js.map +1 -0
- package/dist/src/channels/telegram-mirror/failure/types.d.ts +30 -0
- package/dist/src/channels/telegram-mirror/failure/types.js +9 -0
- package/dist/src/channels/telegram-mirror/failure/types.js.map +1 -0
- package/dist/src/channels/telegram-mirror/inbound-handler.d.ts +52 -0
- package/dist/src/channels/telegram-mirror/inbound-handler.js +115 -0
- package/dist/src/channels/telegram-mirror/inbound-handler.js.map +1 -0
- package/dist/src/channels/telegram-mirror/index.d.ts +30 -0
- package/dist/src/channels/telegram-mirror/index.js +49 -0
- package/dist/src/channels/telegram-mirror/index.js.map +1 -0
- package/dist/src/channels/telegram-mirror/plan-attachment.d.ts +120 -0
- package/dist/src/channels/telegram-mirror/plan-attachment.js +138 -0
- package/dist/src/channels/telegram-mirror/plan-attachment.js.map +1 -0
- package/dist/src/channels/telegram-mirror/quota-reader.d.ts +41 -0
- package/dist/src/channels/telegram-mirror/quota-reader.js +29 -0
- package/dist/src/channels/telegram-mirror/quota-reader.js.map +1 -0
- package/dist/src/channels/telegram-mirror/sessions-keyboard.d.ts +84 -0
- package/dist/src/channels/telegram-mirror/sessions-keyboard.js +113 -0
- package/dist/src/channels/telegram-mirror/sessions-keyboard.js.map +1 -0
- package/dist/src/channels/telegram-mirror/soak-log.d.ts +98 -0
- package/dist/src/channels/telegram-mirror/soak-log.js +136 -0
- package/dist/src/channels/telegram-mirror/soak-log.js.map +1 -0
- package/dist/src/channels/telegram-mirror/state-machine.d.ts +102 -0
- package/dist/src/channels/telegram-mirror/state-machine.js +117 -0
- package/dist/src/channels/telegram-mirror/state-machine.js.map +1 -0
- package/dist/src/channels/telegram-mirror/sync-commands.d.ts +63 -0
- package/dist/src/channels/telegram-mirror/sync-commands.js +51 -0
- package/dist/src/channels/telegram-mirror/sync-commands.js.map +1 -0
- package/dist/src/channels/telegram-mirror/threshold-watcher.d.ts +54 -0
- package/dist/src/channels/telegram-mirror/threshold-watcher.js +77 -0
- package/dist/src/channels/telegram-mirror/threshold-watcher.js.map +1 -0
- package/dist/src/command-router/cc-handler.js +1 -1
- package/dist/src/command-router/cc-handler.js.map +1 -1
- package/dist/src/index.js +12 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/auto-recovery.js +1 -1
- package/dist/src/lib/auto-recovery.js.map +1 -1
- package/dist/src/lib/drift-detector.js +1 -1
- package/dist/src/lib/drift-detector.js.map +1 -1
- package/dist/src/lib/error-renderer.d.ts +23 -0
- package/dist/src/lib/error-renderer.js +106 -0
- package/dist/src/lib/error-renderer.js.map +1 -0
- package/dist/src/lib/perf/speculative-bubble.d.ts +27 -0
- package/dist/src/lib/perf/speculative-bubble.js +36 -0
- package/dist/src/lib/perf/speculative-bubble.js.map +1 -0
- package/dist/src/lib/session-registry.d.ts +66 -0
- package/dist/src/lib/session-registry.js +188 -0
- package/dist/src/lib/session-registry.js.map +1 -0
- package/dist/src/lib/telegram-bot-api.d.ts +100 -0
- package/dist/src/lib/telegram-bot-api.js +204 -0
- package/dist/src/lib/telegram-bot-api.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/channels/telegram-mirror/plan-attachment.ts — v0.25.0 M9.
|
|
3
|
+
*
|
|
4
|
+
* When Claude emits ExitPlanMode (the plan-mode tool call), the mirror
|
|
5
|
+
* uploads the plan body as a .md document attachment with two inline
|
|
6
|
+
* buttons (Approve / Reject). Decision: ADR-006 — sendDocument over
|
|
7
|
+
* editMessageMedia. Rationale: sendDocument is simpler, has the larger
|
|
8
|
+
* size limit, and matches A1's on-disk plan-file convention.
|
|
9
|
+
*
|
|
10
|
+
* Approve → callback payload `{ action: 'plan-approve', planId }` resolves
|
|
11
|
+
* to model input "approved" which the engine bridge feeds to the running
|
|
12
|
+
* session.
|
|
13
|
+
*
|
|
14
|
+
* Reject → two-phase. The first tap stashes a pending-reject for the chat;
|
|
15
|
+
* the next non-slash inbound message becomes the rejection feedback payload
|
|
16
|
+
* "<plan rejected>\n<feedback text>". A tap-then-tap-Reject again clears
|
|
17
|
+
* the pending state silently.
|
|
18
|
+
*
|
|
19
|
+
* Risk references: R-3 (callback_data 64-byte cap) — payloads here route
|
|
20
|
+
* through CallbackMap exactly like M3's keyboard.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Compose the slug-prefixed plan filename. Sanitises non-filename chars
|
|
24
|
+
* so the Telegram upload always succeeds.
|
|
25
|
+
*/
|
|
26
|
+
export function planFilename(slug) {
|
|
27
|
+
const safe = (slug ?? 'session').replace(/[^a-z0-9-_]/gi, '-').slice(0, 40);
|
|
28
|
+
// Fall back to "session" when the sanitised slug contains no alphanumeric
|
|
29
|
+
// characters (e.g. input was empty or all separators) — avoids surfacing
|
|
30
|
+
// an inscrutable "plan-----.md" filename to the user.
|
|
31
|
+
const effective = /[a-z0-9]/i.test(safe) ? safe : 'session';
|
|
32
|
+
return `plan-${effective}.md`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build a sendDocument action carrying the plan + the Approve/Reject
|
|
36
|
+
* inline keyboard. The callback ids are minted into the supplied
|
|
37
|
+
* CallbackMap; payloads include a fresh planId so multiple in-flight
|
|
38
|
+
* plans on the same chat don't collide.
|
|
39
|
+
*/
|
|
40
|
+
export function buildPlanAttachment(opts) {
|
|
41
|
+
const planId = opts.planId ?? `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
42
|
+
const filename = planFilename(opts.event.slug);
|
|
43
|
+
const approveCb = opts.callbackMap.create({ action: 'plan-approve', planId });
|
|
44
|
+
const rejectCb = opts.callbackMap.create({ action: 'plan-reject', planId });
|
|
45
|
+
const inline_keyboard = [
|
|
46
|
+
[
|
|
47
|
+
{ text: '✅ Approve', callback_data: approveCb },
|
|
48
|
+
{ text: '✖ Reject', callback_data: rejectCb },
|
|
49
|
+
],
|
|
50
|
+
];
|
|
51
|
+
return {
|
|
52
|
+
planId,
|
|
53
|
+
action: {
|
|
54
|
+
type: 'sendDocument',
|
|
55
|
+
chat_id: opts.chatId,
|
|
56
|
+
filename,
|
|
57
|
+
content: opts.event.plan,
|
|
58
|
+
caption: opts.event.caption ?? 'Plan ready — Approve or Reject below.',
|
|
59
|
+
reply_markup: { inline_keyboard },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Tracks the (chatId → pending-reject) state for two-phase rejection.
|
|
65
|
+
* The dispatch layer pokes startPending on a Reject tap and consumeFeedback
|
|
66
|
+
* on the next non-slash inbound. Pending-state TTL prevents a half-open
|
|
67
|
+
* rejection from corrupting later turns.
|
|
68
|
+
*/
|
|
69
|
+
export class RejectFeedbackTracker {
|
|
70
|
+
pending = new Map();
|
|
71
|
+
ttlMs;
|
|
72
|
+
now;
|
|
73
|
+
constructor(opts = {}) {
|
|
74
|
+
this.ttlMs = opts.ttlMs ?? 10 * 60_000;
|
|
75
|
+
this.now = opts.now ?? Date.now;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Mark a chat as having tapped Reject and waiting for feedback. If a
|
|
79
|
+
* prior pending exists, it is replaced (the user re-tapped on a newer plan).
|
|
80
|
+
*/
|
|
81
|
+
startPending(chatId, planId) {
|
|
82
|
+
this.pending.set(chatId, { chatId, planId, pendingSince: this.now() });
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns true when the chat has a non-stale pending-reject.
|
|
86
|
+
*/
|
|
87
|
+
isPending(chatId) {
|
|
88
|
+
return this.peek(chatId) !== undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Consume the next inbound text as the rejection feedback. Returns a
|
|
92
|
+
* formatted payload that the engine bridge feeds to openai-compat as a
|
|
93
|
+
* single user-message, or undefined when no pending state exists.
|
|
94
|
+
*
|
|
95
|
+
* Payload shape: "<plan rejected: planId>\n<feedback text>" — the
|
|
96
|
+
* "<plan rejected>" sentinel lets the model recognise this as
|
|
97
|
+
* rejection feedback (not a new turn).
|
|
98
|
+
*/
|
|
99
|
+
consumeFeedback(chatId, feedback) {
|
|
100
|
+
const p = this.peek(chatId);
|
|
101
|
+
if (!p)
|
|
102
|
+
return undefined;
|
|
103
|
+
this.pending.delete(chatId);
|
|
104
|
+
return `<plan rejected: ${p.planId}>\n${feedback}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Drop the pending state without consuming feedback (e.g. user taps
|
|
108
|
+
* Reject twice, or types another slash command in the interim).
|
|
109
|
+
*/
|
|
110
|
+
cancel(chatId) {
|
|
111
|
+
return this.pending.delete(chatId);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Inspect pending without consuming. Filters expired entries.
|
|
115
|
+
*/
|
|
116
|
+
peek(chatId) {
|
|
117
|
+
const p = this.pending.get(chatId);
|
|
118
|
+
if (!p)
|
|
119
|
+
return undefined;
|
|
120
|
+
if (this.now() - p.pendingSince > this.ttlMs) {
|
|
121
|
+
this.pending.delete(chatId);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return p;
|
|
125
|
+
}
|
|
126
|
+
clear() {
|
|
127
|
+
this.pending.clear();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Helper: format the approve payload as engine input. Kept as a function
|
|
132
|
+
* (not a constant) so future variants — e.g. caching a planId reference —
|
|
133
|
+
* have a single place to extend.
|
|
134
|
+
*/
|
|
135
|
+
export function formatApprovePayload(planId) {
|
|
136
|
+
return `<plan approved: ${planId}>`;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=plan-attachment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-attachment.js","sourceRoot":"","sources":["../../../../src/channels/telegram-mirror/plan-attachment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAsBH;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAwB;IACnD,MAAM,IAAI,GAAG,CAAC,IAAI,IAAI,SAAS,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC5E,0EAA0E;IAC1E,yEAAyE;IACzE,sDAAsD;IACtD,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5D,OAAO,QAAQ,SAAS,KAAK,CAAC;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAMnC;IACC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,QAAQ,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IAC7F,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9E,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,CAAC;IAE5E,MAAM,eAAe,GAAqB;QACxC;YACE,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE;YAC/C,EAAE,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE;SAC9C;KACF,CAAC;IAEF,OAAO;QACL,MAAM;QACN,MAAM,EAAE;YACN,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,IAAI,CAAC,MAAM;YACpB,QAAQ;YACR,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI;YACxB,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,uCAAuC;YACtE,YAAY,EAAE,EAAE,eAAe,EAAE;SAClC;KACF,CAAC;AACJ,CAAC;AAiBD;;;;;GAKG;AACH,MAAM,OAAO,qBAAqB;IACf,OAAO,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC3C,KAAK,CAAS;IACd,GAAG,CAAe;IAEnC,YAAY,OAAqC,EAAE;QACjD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,MAAM,CAAC;QACvC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IAClC,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,MAAc,EAAE,MAAc;QACzC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACzE,CAAC;IAED;;OAEG;IACH,SAAS,CAAC,MAAc;QACtB,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;IACzC,CAAC;IAED;;;;;;;;OAQG;IACH,eAAe,CAAC,MAAc,EAAE,QAAgB;QAC9C,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACzB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5B,OAAO,mBAAmB,CAAC,CAAC,MAAM,MAAM,QAAQ,EAAE,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,MAAc;QACnB,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAED;;OAEG;IACH,IAAI,CAAC,MAAc;QACjB,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACzB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YAC7C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC5B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,OAAO,mBAAmB,MAAM,GAAG,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/channels/telegram-mirror/quota-reader.ts — v0.25.0 M4 stub.
|
|
3
|
+
*
|
|
4
|
+
* Minimal QuotaSnapshot interface and a zero-valued reader so /cost in M4
|
|
5
|
+
* has something to render. M11 replaces the stub with a wire-up to the
|
|
6
|
+
* production status-tee-reader.ts that the existing /cost handler in the
|
|
7
|
+
* legacy live card already consumes.
|
|
8
|
+
*
|
|
9
|
+
* Decision-link: M11 owns threshold-notification + detail-view wiring;
|
|
10
|
+
* keeping the surface here in M4 isolates the command-handler layer from
|
|
11
|
+
* the status-tee read path (so M11 can swap implementations without
|
|
12
|
+
* touching commands.ts).
|
|
13
|
+
*/
|
|
14
|
+
export interface QuotaPerDay {
|
|
15
|
+
/** YYYY-MM-DD */
|
|
16
|
+
date: string;
|
|
17
|
+
/** 0..100 — percentage of the daily Max 20x window consumed. */
|
|
18
|
+
percent: number;
|
|
19
|
+
}
|
|
20
|
+
export interface QuotaSnapshot {
|
|
21
|
+
/** Current Max 20x consumption as 0..100 percent. */
|
|
22
|
+
maxPercent: number;
|
|
23
|
+
/** Weekly burn in USD (extra_usage overage tracked separately by status-tee). */
|
|
24
|
+
weeklyBurn: number;
|
|
25
|
+
/** Sessions ranked by token consumption (top-5). */
|
|
26
|
+
topSessions: Array<{
|
|
27
|
+
slug: string;
|
|
28
|
+
tokens: number;
|
|
29
|
+
}>;
|
|
30
|
+
/** Per-day rollup, most-recent-first (capped at 7). */
|
|
31
|
+
perDayRollup: QuotaPerDay[];
|
|
32
|
+
}
|
|
33
|
+
export interface QuotaReader {
|
|
34
|
+
read(): QuotaSnapshot;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Stub reader for M4 — returns a known-zero snapshot. M11 replaces this
|
|
38
|
+
* with a wire to status-tee-reader.ts. Tests can pass their own reader
|
|
39
|
+
* directly to the cost command for fixture-based assertions.
|
|
40
|
+
*/
|
|
41
|
+
export declare const stubQuotaReader: QuotaReader;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/channels/telegram-mirror/quota-reader.ts — v0.25.0 M4 stub.
|
|
3
|
+
*
|
|
4
|
+
* Minimal QuotaSnapshot interface and a zero-valued reader so /cost in M4
|
|
5
|
+
* has something to render. M11 replaces the stub with a wire-up to the
|
|
6
|
+
* production status-tee-reader.ts that the existing /cost handler in the
|
|
7
|
+
* legacy live card already consumes.
|
|
8
|
+
*
|
|
9
|
+
* Decision-link: M11 owns threshold-notification + detail-view wiring;
|
|
10
|
+
* keeping the surface here in M4 isolates the command-handler layer from
|
|
11
|
+
* the status-tee read path (so M11 can swap implementations without
|
|
12
|
+
* touching commands.ts).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Stub reader for M4 — returns a known-zero snapshot. M11 replaces this
|
|
16
|
+
* with a wire to status-tee-reader.ts. Tests can pass their own reader
|
|
17
|
+
* directly to the cost command for fixture-based assertions.
|
|
18
|
+
*/
|
|
19
|
+
export const stubQuotaReader = {
|
|
20
|
+
read() {
|
|
21
|
+
return {
|
|
22
|
+
maxPercent: 0,
|
|
23
|
+
weeklyBurn: 0,
|
|
24
|
+
topSessions: [],
|
|
25
|
+
perDayRollup: [],
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=quota-reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota-reader.js","sourceRoot":"","sources":["../../../../src/channels/telegram-mirror/quota-reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAwBH;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAgB;IAC1C,IAAI;QACF,OAAO;YACL,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,CAAC;YACb,WAAW,EAAE,EAAE;YACf,YAAY,EAAE,EAAE;SACjB,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/channels/telegram-mirror/sessions-keyboard.ts — v0.25.0 M3.
|
|
3
|
+
*
|
|
4
|
+
* Builds the inline keyboard for /sessions. Pure function — takes a row
|
|
5
|
+
* list + page index + a CallbackMap, returns Telegram's inline_keyboard
|
|
6
|
+
* shape. Caller is responsible for sending/editing the surrounding
|
|
7
|
+
* Telegram message via the dispatch layer.
|
|
8
|
+
*
|
|
9
|
+
* Decision refs:
|
|
10
|
+
* • ADR-003 — slug ↔ sessionName comes from session-registry; the row
|
|
11
|
+
* list here is a denormalised SessionRow because the keyboard also
|
|
12
|
+
* needs live state + lastUsedAt, which the registry doesn't track
|
|
13
|
+
* (state lives in session-manager, time-since lives in the registry's
|
|
14
|
+
* lastUsedAt). The handler in M4 stitches them together.
|
|
15
|
+
* • ADR-004 — every callback_data slot is a CallbackMap id, not a raw
|
|
16
|
+
* JSON payload. Keeps every button under the 64-byte cap by design.
|
|
17
|
+
* • ADR-005 — page size is 8; 9th row is "▶ More" when more pages exist.
|
|
18
|
+
*/
|
|
19
|
+
import type { CallbackMap } from './callback-mapping.js';
|
|
20
|
+
export declare const SESSIONS_PAGE_SIZE = 8;
|
|
21
|
+
export type SessionState = 'running' | 'stopped' | 'idle';
|
|
22
|
+
export interface SessionRow {
|
|
23
|
+
slug: string;
|
|
24
|
+
sessionName: string;
|
|
25
|
+
state: SessionState;
|
|
26
|
+
/** ISO timestamp — the registry's lastUsedAt. */
|
|
27
|
+
lastUsedAt: string;
|
|
28
|
+
}
|
|
29
|
+
export interface InlineButton {
|
|
30
|
+
text: string;
|
|
31
|
+
callback_data: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SessionsKeyboardResult {
|
|
34
|
+
inline_keyboard: InlineButton[][];
|
|
35
|
+
/** Zero-indexed page rendered. */
|
|
36
|
+
page: number;
|
|
37
|
+
/** Total page count (≥ 1 even when rows is empty). */
|
|
38
|
+
pageCount: number;
|
|
39
|
+
/** True when there is at least one page after the rendered one. */
|
|
40
|
+
hasMore: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Compact relative-time string for a row's lastUsedAt.
|
|
44
|
+
* < 60s → "<1m"
|
|
45
|
+
* < 1h → "Nm"
|
|
46
|
+
* < 1d → "Nh"
|
|
47
|
+
* else → "Nd"
|
|
48
|
+
* Designed to fit in 4 chars so the row stays narrow on phone Telegram.
|
|
49
|
+
*/
|
|
50
|
+
export declare function formatLastActivity(lastUsedAt: string, now?: number): string;
|
|
51
|
+
/**
|
|
52
|
+
* Format a row label. "cc-openclaw · 🟢 · 2m"
|
|
53
|
+
*/
|
|
54
|
+
export declare function formatSessionRowLabel(row: SessionRow, now?: number): string;
|
|
55
|
+
export interface SessionsKeyboardOpts {
|
|
56
|
+
rows: SessionRow[];
|
|
57
|
+
/** Zero-indexed page; clamped into [0, pageCount-1]. */
|
|
58
|
+
page?: number;
|
|
59
|
+
callbackMap: CallbackMap;
|
|
60
|
+
/** Override clock for tests. */
|
|
61
|
+
now?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build the full /sessions inline keyboard. Layout:
|
|
65
|
+
*
|
|
66
|
+
* Row 1..N session rows (one button each) (N ≤ 8)
|
|
67
|
+
* Row N+1 "▶ More" — only when hasMore===true
|
|
68
|
+
* Row last "➕ New" "🗑 Stop" "✖ Close" (always)
|
|
69
|
+
*
|
|
70
|
+
* Every button's callback_data is a CallbackMap id pointing at a payload:
|
|
71
|
+
* session row → { action: 'switch', slug }
|
|
72
|
+
* ▶ More → { action: 'page', page: N+1 }
|
|
73
|
+
* ➕ New → { action: 'new' }
|
|
74
|
+
* 🗑 Stop → { action: 'stop' }
|
|
75
|
+
* ✖ Close → { action: 'close' }
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildSessionsKeyboard(opts: SessionsKeyboardOpts): SessionsKeyboardResult;
|
|
78
|
+
/**
|
|
79
|
+
* Helper: every button's callback_data must fit in 64 UTF-8 bytes per
|
|
80
|
+
* Telegram's API. We enforce this at build time as a defense-in-depth
|
|
81
|
+
* check — even an 8-char id is well under, but a future refactor that
|
|
82
|
+
* stuffs raw JSON would silently break Telegram without this assertion.
|
|
83
|
+
*/
|
|
84
|
+
export declare function assertCallbackDataBudget(keyboard: SessionsKeyboardResult): void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/channels/telegram-mirror/sessions-keyboard.ts — v0.25.0 M3.
|
|
3
|
+
*
|
|
4
|
+
* Builds the inline keyboard for /sessions. Pure function — takes a row
|
|
5
|
+
* list + page index + a CallbackMap, returns Telegram's inline_keyboard
|
|
6
|
+
* shape. Caller is responsible for sending/editing the surrounding
|
|
7
|
+
* Telegram message via the dispatch layer.
|
|
8
|
+
*
|
|
9
|
+
* Decision refs:
|
|
10
|
+
* • ADR-003 — slug ↔ sessionName comes from session-registry; the row
|
|
11
|
+
* list here is a denormalised SessionRow because the keyboard also
|
|
12
|
+
* needs live state + lastUsedAt, which the registry doesn't track
|
|
13
|
+
* (state lives in session-manager, time-since lives in the registry's
|
|
14
|
+
* lastUsedAt). The handler in M4 stitches them together.
|
|
15
|
+
* • ADR-004 — every callback_data slot is a CallbackMap id, not a raw
|
|
16
|
+
* JSON payload. Keeps every button under the 64-byte cap by design.
|
|
17
|
+
* • ADR-005 — page size is 8; 9th row is "▶ More" when more pages exist.
|
|
18
|
+
*/
|
|
19
|
+
export const SESSIONS_PAGE_SIZE = 8;
|
|
20
|
+
const STATE_EMOJI = {
|
|
21
|
+
running: '🟢',
|
|
22
|
+
stopped: '⚫',
|
|
23
|
+
idle: '🟡',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Compact relative-time string for a row's lastUsedAt.
|
|
27
|
+
* < 60s → "<1m"
|
|
28
|
+
* < 1h → "Nm"
|
|
29
|
+
* < 1d → "Nh"
|
|
30
|
+
* else → "Nd"
|
|
31
|
+
* Designed to fit in 4 chars so the row stays narrow on phone Telegram.
|
|
32
|
+
*/
|
|
33
|
+
export function formatLastActivity(lastUsedAt, now = Date.now()) {
|
|
34
|
+
const t = new Date(lastUsedAt).getTime();
|
|
35
|
+
if (!Number.isFinite(t))
|
|
36
|
+
return '?';
|
|
37
|
+
const ms = Math.max(0, now - t);
|
|
38
|
+
if (ms < 60_000)
|
|
39
|
+
return '<1m';
|
|
40
|
+
if (ms < 3_600_000)
|
|
41
|
+
return `${Math.floor(ms / 60_000)}m`;
|
|
42
|
+
if (ms < 86_400_000)
|
|
43
|
+
return `${Math.floor(ms / 3_600_000)}h`;
|
|
44
|
+
return `${Math.floor(ms / 86_400_000)}d`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Format a row label. "cc-openclaw · 🟢 · 2m"
|
|
48
|
+
*/
|
|
49
|
+
export function formatSessionRowLabel(row, now = Date.now()) {
|
|
50
|
+
return `${row.slug} · ${STATE_EMOJI[row.state]} · ${formatLastActivity(row.lastUsedAt, now)}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the full /sessions inline keyboard. Layout:
|
|
54
|
+
*
|
|
55
|
+
* Row 1..N session rows (one button each) (N ≤ 8)
|
|
56
|
+
* Row N+1 "▶ More" — only when hasMore===true
|
|
57
|
+
* Row last "➕ New" "🗑 Stop" "✖ Close" (always)
|
|
58
|
+
*
|
|
59
|
+
* Every button's callback_data is a CallbackMap id pointing at a payload:
|
|
60
|
+
* session row → { action: 'switch', slug }
|
|
61
|
+
* ▶ More → { action: 'page', page: N+1 }
|
|
62
|
+
* ➕ New → { action: 'new' }
|
|
63
|
+
* 🗑 Stop → { action: 'stop' }
|
|
64
|
+
* ✖ Close → { action: 'close' }
|
|
65
|
+
*/
|
|
66
|
+
export function buildSessionsKeyboard(opts) {
|
|
67
|
+
const { rows, callbackMap, now = Date.now() } = opts;
|
|
68
|
+
const pageCount = Math.max(1, Math.ceil(rows.length / SESSIONS_PAGE_SIZE));
|
|
69
|
+
const page = Math.min(Math.max(0, opts.page ?? 0), pageCount - 1);
|
|
70
|
+
const start = page * SESSIONS_PAGE_SIZE;
|
|
71
|
+
const slice = rows.slice(start, start + SESSIONS_PAGE_SIZE);
|
|
72
|
+
const hasMore = page < pageCount - 1;
|
|
73
|
+
const inline_keyboard = [];
|
|
74
|
+
for (const row of slice) {
|
|
75
|
+
inline_keyboard.push([
|
|
76
|
+
{
|
|
77
|
+
text: formatSessionRowLabel(row, now),
|
|
78
|
+
callback_data: callbackMap.create({ action: 'switch', slug: row.slug }),
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
if (hasMore) {
|
|
83
|
+
inline_keyboard.push([
|
|
84
|
+
{
|
|
85
|
+
text: '▶ More',
|
|
86
|
+
callback_data: callbackMap.create({ action: 'page', page: page + 1 }),
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
inline_keyboard.push([
|
|
91
|
+
{ text: '➕ New', callback_data: callbackMap.create({ action: 'new' }) },
|
|
92
|
+
{ text: '🗑 Stop', callback_data: callbackMap.create({ action: 'stop' }) },
|
|
93
|
+
{ text: '✖ Close', callback_data: callbackMap.create({ action: 'close' }) },
|
|
94
|
+
]);
|
|
95
|
+
return { inline_keyboard, page, pageCount, hasMore };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Helper: every button's callback_data must fit in 64 UTF-8 bytes per
|
|
99
|
+
* Telegram's API. We enforce this at build time as a defense-in-depth
|
|
100
|
+
* check — even an 8-char id is well under, but a future refactor that
|
|
101
|
+
* stuffs raw JSON would silently break Telegram without this assertion.
|
|
102
|
+
*/
|
|
103
|
+
export function assertCallbackDataBudget(keyboard) {
|
|
104
|
+
for (const row of keyboard.inline_keyboard) {
|
|
105
|
+
for (const btn of row) {
|
|
106
|
+
const bytes = Buffer.byteLength(btn.callback_data, 'utf8');
|
|
107
|
+
if (bytes > 64) {
|
|
108
|
+
throw new Error(`[cc-openclaw/telegram-mirror/sessions-keyboard] callback_data exceeds 64-byte cap (${bytes}b) on button "${btn.text}"`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=sessions-keyboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions-keyboard.js","sourceRoot":"","sources":["../../../../src/channels/telegram-mirror/sessions-keyboard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/channels/telegram-mirror/soak-log.ts — v0.25.0 M14.
|
|
3
|
+
*
|
|
4
|
+
* Daily JSONL incident + activity log for the 2-week soak (M14 → M15
|
|
5
|
+
* cutover gate). Writes append-only rows to
|
|
6
|
+
* workspace/memory/mirror-soak-<YYYY-MM-DD>.jsonl
|
|
7
|
+
* per PRP §5 M14. The file rolls at UTC midnight (date-stamped path).
|
|
8
|
+
*
|
|
9
|
+
* Three row types per spec:
|
|
10
|
+
* • laptop-check — A1 self-reports opened-laptop-today via /soak-check.
|
|
11
|
+
* • failure-incident — fires once per M12 handler invocation in soak.
|
|
12
|
+
* • slash-invocation — one row per slash command dispatch.
|
|
13
|
+
*
|
|
14
|
+
* Idempotency: a single laptop-check per UTC day. recordLaptopCheck()
|
|
15
|
+
* returns false on the second call within a day so the /soak-check
|
|
16
|
+
* handler can surface "already recorded today" UX rather than logging
|
|
17
|
+
* duplicates.
|
|
18
|
+
*
|
|
19
|
+
* Decision-link: this file is the only filesystem-touching mirror module
|
|
20
|
+
* outside session-registry (M1). Both share the atomic-write-via-rename
|
|
21
|
+
* idiom; soak-log appends so it doesn't need the tmpfile dance, but the
|
|
22
|
+
* path resolution is the same shape.
|
|
23
|
+
*/
|
|
24
|
+
export type SoakRow = SoakLaptopCheckRow | SoakFailureRow | SoakSlashRow;
|
|
25
|
+
export interface SoakLaptopCheckRow {
|
|
26
|
+
ts: string;
|
|
27
|
+
type: 'laptop-check';
|
|
28
|
+
openedLaptop: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface SoakFailureRow {
|
|
31
|
+
ts: string;
|
|
32
|
+
type: 'failure-incident';
|
|
33
|
+
mode: string;
|
|
34
|
+
chatId: string | number;
|
|
35
|
+
info?: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface SoakSlashRow {
|
|
38
|
+
ts: string;
|
|
39
|
+
type: 'slash-invocation';
|
|
40
|
+
cmd: string;
|
|
41
|
+
chatId: string | number;
|
|
42
|
+
}
|
|
43
|
+
export interface SoakLoggerOptions {
|
|
44
|
+
/** Override base dir for tests (defaults to ~/.openclaw/workspace/memory). */
|
|
45
|
+
dir?: string;
|
|
46
|
+
/** Override clock for tests. */
|
|
47
|
+
now?: () => Date;
|
|
48
|
+
}
|
|
49
|
+
export declare class SoakLogger {
|
|
50
|
+
private readonly dir;
|
|
51
|
+
private readonly now;
|
|
52
|
+
constructor(opts?: SoakLoggerOptions);
|
|
53
|
+
/**
|
|
54
|
+
* Absolute path of today's JSONL file. Honors the configured clock.
|
|
55
|
+
*/
|
|
56
|
+
pathForToday(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Append a row to today's file. Creates the parent directory if missing.
|
|
59
|
+
* Each row is `JSON.stringify(row) + '\n'`.
|
|
60
|
+
*/
|
|
61
|
+
append(row: SoakRow): void;
|
|
62
|
+
/**
|
|
63
|
+
* Read all rows for today (parsing each line). Returns [] when the
|
|
64
|
+
* file is missing OR when every line is malformed.
|
|
65
|
+
*/
|
|
66
|
+
readToday(): SoakRow[];
|
|
67
|
+
/**
|
|
68
|
+
* Returns true iff a laptop-check row has already been written today.
|
|
69
|
+
* Used by /soak-check's once-per-day gate.
|
|
70
|
+
*/
|
|
71
|
+
hasLaptopCheckToday(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Idempotent laptop-check writer. Returns true when the row was
|
|
74
|
+
* persisted, false when a check was already recorded today.
|
|
75
|
+
*/
|
|
76
|
+
recordLaptopCheck(openedLaptop: boolean): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Failure-incident logger — called by the dispatcher when a M12
|
|
79
|
+
* handler fires. The info payload is opaque JSON; the schema is
|
|
80
|
+
* per-mode and lives in the handler's documentation.
|
|
81
|
+
*/
|
|
82
|
+
recordFailure(mode: string, chatId: string | number, info?: unknown): void;
|
|
83
|
+
/**
|
|
84
|
+
* Slash-invocation logger — called once per dispatchCommand call by
|
|
85
|
+
* the dispatcher (M5+). Cheap to record and lets the soak surface
|
|
86
|
+
* which commands A1 actually uses on phone.
|
|
87
|
+
*/
|
|
88
|
+
recordSlash(cmd: string, chatId: string | number): void;
|
|
89
|
+
/**
|
|
90
|
+
* Count rows by type for the day — used by M15's "5/7 no-laptop days"
|
|
91
|
+
* gate without having to ship a separate aggregator.
|
|
92
|
+
*/
|
|
93
|
+
countByType(): {
|
|
94
|
+
'laptop-check': number;
|
|
95
|
+
'failure-incident': number;
|
|
96
|
+
'slash-invocation': number;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/channels/telegram-mirror/soak-log.ts — v0.25.0 M14.
|
|
3
|
+
*
|
|
4
|
+
* Daily JSONL incident + activity log for the 2-week soak (M14 → M15
|
|
5
|
+
* cutover gate). Writes append-only rows to
|
|
6
|
+
* workspace/memory/mirror-soak-<YYYY-MM-DD>.jsonl
|
|
7
|
+
* per PRP §5 M14. The file rolls at UTC midnight (date-stamped path).
|
|
8
|
+
*
|
|
9
|
+
* Three row types per spec:
|
|
10
|
+
* • laptop-check — A1 self-reports opened-laptop-today via /soak-check.
|
|
11
|
+
* • failure-incident — fires once per M12 handler invocation in soak.
|
|
12
|
+
* • slash-invocation — one row per slash command dispatch.
|
|
13
|
+
*
|
|
14
|
+
* Idempotency: a single laptop-check per UTC day. recordLaptopCheck()
|
|
15
|
+
* returns false on the second call within a day so the /soak-check
|
|
16
|
+
* handler can surface "already recorded today" UX rather than logging
|
|
17
|
+
* duplicates.
|
|
18
|
+
*
|
|
19
|
+
* Decision-link: this file is the only filesystem-touching mirror module
|
|
20
|
+
* outside session-registry (M1). Both share the atomic-write-via-rename
|
|
21
|
+
* idiom; soak-log appends so it doesn't need the tmpfile dance, but the
|
|
22
|
+
* path resolution is the same shape.
|
|
23
|
+
*/
|
|
24
|
+
import { appendFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
|
25
|
+
import { homedir } from 'os';
|
|
26
|
+
import { join, dirname } from 'path';
|
|
27
|
+
function defaultDir() {
|
|
28
|
+
return join(homedir(), '.openclaw', 'workspace', 'memory');
|
|
29
|
+
}
|
|
30
|
+
function ymd(d) {
|
|
31
|
+
return d.toISOString().slice(0, 10); // YYYY-MM-DD (UTC)
|
|
32
|
+
}
|
|
33
|
+
export class SoakLogger {
|
|
34
|
+
dir;
|
|
35
|
+
now;
|
|
36
|
+
constructor(opts = {}) {
|
|
37
|
+
this.dir = opts.dir ?? defaultDir();
|
|
38
|
+
this.now = opts.now ?? (() => new Date());
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Absolute path of today's JSONL file. Honors the configured clock.
|
|
42
|
+
*/
|
|
43
|
+
pathForToday() {
|
|
44
|
+
return join(this.dir, `mirror-soak-${ymd(this.now())}.jsonl`);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Append a row to today's file. Creates the parent directory if missing.
|
|
48
|
+
* Each row is `JSON.stringify(row) + '\n'`.
|
|
49
|
+
*/
|
|
50
|
+
append(row) {
|
|
51
|
+
const path = this.pathForToday();
|
|
52
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
53
|
+
appendFileSync(path, JSON.stringify(row) + '\n');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read all rows for today (parsing each line). Returns [] when the
|
|
57
|
+
* file is missing OR when every line is malformed.
|
|
58
|
+
*/
|
|
59
|
+
readToday() {
|
|
60
|
+
const path = this.pathForToday();
|
|
61
|
+
if (!existsSync(path))
|
|
62
|
+
return [];
|
|
63
|
+
const raw = readFileSync(path, 'utf8');
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const line of raw.split('\n')) {
|
|
66
|
+
if (!line.trim())
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
out.push(JSON.parse(line));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* skip malformed line — defensive */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns true iff a laptop-check row has already been written today.
|
|
79
|
+
* Used by /soak-check's once-per-day gate.
|
|
80
|
+
*/
|
|
81
|
+
hasLaptopCheckToday() {
|
|
82
|
+
return this.readToday().some((r) => r.type === 'laptop-check');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Idempotent laptop-check writer. Returns true when the row was
|
|
86
|
+
* persisted, false when a check was already recorded today.
|
|
87
|
+
*/
|
|
88
|
+
recordLaptopCheck(openedLaptop) {
|
|
89
|
+
if (this.hasLaptopCheckToday())
|
|
90
|
+
return false;
|
|
91
|
+
this.append({
|
|
92
|
+
ts: this.now().toISOString(),
|
|
93
|
+
type: 'laptop-check',
|
|
94
|
+
openedLaptop,
|
|
95
|
+
});
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Failure-incident logger — called by the dispatcher when a M12
|
|
100
|
+
* handler fires. The info payload is opaque JSON; the schema is
|
|
101
|
+
* per-mode and lives in the handler's documentation.
|
|
102
|
+
*/
|
|
103
|
+
recordFailure(mode, chatId, info) {
|
|
104
|
+
this.append({
|
|
105
|
+
ts: this.now().toISOString(),
|
|
106
|
+
type: 'failure-incident',
|
|
107
|
+
mode,
|
|
108
|
+
chatId,
|
|
109
|
+
info,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Slash-invocation logger — called once per dispatchCommand call by
|
|
114
|
+
* the dispatcher (M5+). Cheap to record and lets the soak surface
|
|
115
|
+
* which commands A1 actually uses on phone.
|
|
116
|
+
*/
|
|
117
|
+
recordSlash(cmd, chatId) {
|
|
118
|
+
this.append({
|
|
119
|
+
ts: this.now().toISOString(),
|
|
120
|
+
type: 'slash-invocation',
|
|
121
|
+
cmd,
|
|
122
|
+
chatId,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Count rows by type for the day — used by M15's "5/7 no-laptop days"
|
|
127
|
+
* gate without having to ship a separate aggregator.
|
|
128
|
+
*/
|
|
129
|
+
countByType() {
|
|
130
|
+
const out = { 'laptop-check': 0, 'failure-incident': 0, 'slash-invocation': 0 };
|
|
131
|
+
for (const r of this.readToday())
|
|
132
|
+
out[r.type] += 1;
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=soak-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"soak-log.js","sourceRoot":"","sources":["../../../../src/channels/telegram-mirror/soak-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAmCrC,SAAS,UAAU;IACjB,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,GAAG,CAAC,CAAO;IAClB,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,mBAAmB;AAC1D,CAAC;AAED,MAAM,OAAO,UAAU;IACJ,GAAG,CAAS;IACZ,GAAG,CAAa;IAEjC,YAAY,OAA0B,EAAE;QACtC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED;;OAEG;IACH,YAAY;QACV,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;IAChE,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,GAAY;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACjC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACnD,CAAC;IAED;;;OAGG;IACH,SAAS;QACP,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACjC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACvC,MAAM,GAAG,GAAc,EAAE,CAAC;QAC1B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;gBAAE,SAAS;YAC3B,IAAI,CAAC;gBACH,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC,CAAC;YACxC,CAAC;YAAC,MAAM,CAAC;gBACP,qCAAqC;YACvC,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED;;;OAGG;IACH,mBAAmB;QACjB,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC;IACjE,CAAC;IAED;;;OAGG;IACH,iBAAiB,CAAC,YAAqB;QACrC,IAAI,IAAI,CAAC,mBAAmB,EAAE;YAAE,OAAO,KAAK,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC;YACV,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;YAC5B,IAAI,EAAE,cAAc;YACpB,YAAY;SACb,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,IAAY,EAAE,MAAuB,EAAE,IAAc;QACjE,IAAI,CAAC,MAAM,CAAC;YACV,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;YAC5B,IAAI,EAAE,kBAAkB;YACxB,IAAI;YACJ,MAAM;YACN,IAAI;SACL,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,GAAW,EAAE,MAAuB;QAC9C,IAAI,CAAC,MAAM,CAAC;YACV,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;YAC5B,IAAI,EAAE,kBAAkB;YACxB,GAAG;YACH,MAAM;SACP,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,WAAW;QACT,MAAM,GAAG,GAAG,EAAE,cAAc,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAC;QAChF,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE;YAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,OAAO,GAAG,CAAC;IACb,CAAC;CACF"}
|