@portel/photon 1.7.0 → 1.8.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/README.md +23 -24
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +117 -42
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/design-system/tokens.d.ts +1 -1
- package/dist/auto-ui/design-system/tokens.d.ts.map +1 -1
- package/dist/auto-ui/design-system/tokens.js +1 -1
- package/dist/auto-ui/design-system/tokens.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +1 -1
- package/dist/auto-ui/rendering/components.d.ts.map +1 -1
- package/dist/auto-ui/rendering/components.js +568 -0
- package/dist/auto-ui/rendering/components.js.map +1 -1
- package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
- package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
- package/dist/auto-ui/rendering/field-analyzer.js +177 -0
- package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
- package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
- package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
- package/dist/auto-ui/rendering/layout-selector.js +125 -1
- package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +353 -19
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +7 -1
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +22441 -4216
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +37 -0
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/package.d.ts.map +1 -1
- package/dist/cli/commands/package.js +16 -0
- package/dist/cli/commands/package.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +628 -14
- package/dist/cli.js.map +1 -1
- package/dist/context-store.d.ts +79 -0
- package/dist/context-store.d.ts.map +1 -0
- package/dist/context-store.js +210 -0
- package/dist/context-store.js.map +1 -0
- package/dist/daemon/client.d.ts +13 -4
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +138 -77
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +0 -25
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +10 -38
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/protocol.d.ts +7 -2
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.js +257 -35
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +24 -4
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +62 -12
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +3 -20
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +53 -75
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +258 -218
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +2 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +42 -6
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +3 -1
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +3 -1
- package/dist/serv/index.d.ts.map +1 -1
- package/dist/serv/index.js.map +1 -1
- package/dist/server.d.ts +32 -15
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +468 -469
- package/dist/server.js.map +1 -1
- package/dist/shared/security.d.ts.map +1 -1
- package/dist/shared/security.js +4 -8
- package/dist/shared/security.js.map +1 -1
- package/dist/shell-completions.d.ts +21 -0
- package/dist/shell-completions.d.ts.map +1 -0
- package/dist/shell-completions.js +102 -0
- package/dist/shell-completions.js.map +1 -0
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js.map +1 -1
- package/package.json +10 -6
|
@@ -16,6 +16,8 @@ export interface DaemonRequest {
|
|
|
16
16
|
photonPath?: string;
|
|
17
17
|
sessionId?: string;
|
|
18
18
|
clientType?: 'cli' | 'mcp' | 'code-mode' | 'beam';
|
|
19
|
+
/** Instance name hint for auto-recovery from session drift */
|
|
20
|
+
instanceName?: string;
|
|
19
21
|
method?: string;
|
|
20
22
|
args?: Record<string, unknown>;
|
|
21
23
|
/** Response to a prompt request */
|
|
@@ -32,7 +34,7 @@ export interface DaemonRequest {
|
|
|
32
34
|
jobId?: string;
|
|
33
35
|
/** Cron expression for scheduled jobs */
|
|
34
36
|
cron?: string;
|
|
35
|
-
/** Last event
|
|
37
|
+
/** Last event timestamp received by client (for delta sync on reconnect) */
|
|
36
38
|
lastEventId?: string;
|
|
37
39
|
}
|
|
38
40
|
/**
|
|
@@ -44,6 +46,8 @@ export interface DaemonResponse {
|
|
|
44
46
|
success?: boolean;
|
|
45
47
|
data?: unknown;
|
|
46
48
|
error?: string;
|
|
49
|
+
/** Actionable hint for the caller when type === 'error' */
|
|
50
|
+
suggestion?: string;
|
|
47
51
|
/** Prompt request details (when type === 'prompt') */
|
|
48
52
|
prompt?: {
|
|
49
53
|
type: 'text' | 'password' | 'confirm' | 'select';
|
|
@@ -58,7 +62,7 @@ export interface DaemonResponse {
|
|
|
58
62
|
channel?: string;
|
|
59
63
|
/** Message payload for channel_message type */
|
|
60
64
|
message?: unknown;
|
|
61
|
-
/** Event
|
|
65
|
+
/** Event timestamp for tracking (for delta sync support) */
|
|
62
66
|
eventId?: string;
|
|
63
67
|
}
|
|
64
68
|
/**
|
|
@@ -78,6 +82,7 @@ export interface DaemonStatus {
|
|
|
78
82
|
export interface PhotonSession {
|
|
79
83
|
id: string;
|
|
80
84
|
instance: PhotonMCPClass;
|
|
85
|
+
instanceName: string;
|
|
81
86
|
createdAt: number;
|
|
82
87
|
lastActivity: number;
|
|
83
88
|
clientType?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/daemon/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EACA,SAAS,GACT,MAAM,GACN,UAAU,GACV,QAAQ,GACR,iBAAiB,GACjB,WAAW,GACX,aAAa,GACb,SAAS,GACT,MAAM,GACN,QAAQ,GACR,UAAU,GACV,YAAY,GACZ,WAAW,GACX,YAAY,GACZ,kBAAkB,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,WAAW,GAAG,MAAM,CAAC;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/daemon/protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/daemon/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/daemon/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgIH;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAY;IAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,GAAG,GAAG,GAA6B,CAAC;IAE1C,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE7C,MAAM,UAAU,GAAG;QACjB,SAAS;QACT,MAAM;QACN,UAAU;QACV,QAAQ;QACR,iBAAiB;QACjB,WAAW;QACX,aAAa;QACb,SAAS;QACT,MAAM;QACN,QAAQ;QACR,UAAU;QACV,YAAY;QACZ,WAAW;QACX,YAAY;QACZ,kBAAkB;KACnB,CAAC;IACF,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAc,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3D,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3B,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACnD,CAAC;IAED,4CAA4C;IAC5C,IAAI,CAAC,WAAW,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAc,CAAC,EAAE,CAAC;QACzE,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACpD,CAAC;IAED,sCAAsC;IACtC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAc,CAAC,EAAE,CAAC;QACpD,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACrD,CAAC;IAED,sDAAsD;IACtD,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC5B,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACjD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACjD,CAAC;IAED,4BAA4B;IAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC9B,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IAClD,CAAC;IAED,6BAA6B;IAC7B,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACvD,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,GAAG,GAAG,GAA8B,CAAC;IAE3C,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC7C,IACE,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC,QAAQ,CAClF,GAAG,CAAC,IAAc,CACnB;QAED,OAAO,KAAK,CAAC;IAEf,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/daemon/server.js
CHANGED
|
@@ -31,46 +31,52 @@ if (!socketPath) {
|
|
|
31
31
|
// Map of photonName -> SessionManager (lazy initialized)
|
|
32
32
|
const sessionManagers = new Map();
|
|
33
33
|
const photonPaths = new Map(); // photonName -> photonPath
|
|
34
|
-
|
|
34
|
+
const fileWatchers = new Map();
|
|
35
|
+
const watchDebounce = new Map();
|
|
36
|
+
let idleTimeout = 0; // Daemon stays alive — it manages persistent stateful data
|
|
35
37
|
let idleTimer = null;
|
|
36
38
|
// Track pending prompts waiting for user input
|
|
37
39
|
const pendingPrompts = new Map();
|
|
38
40
|
// Channel subscriptions for pub/sub
|
|
39
41
|
const channelSubscriptions = new Map();
|
|
40
|
-
|
|
42
|
+
/** Buffer retention window — events older than this are purged */
|
|
43
|
+
const EVENT_BUFFER_DURATION_MS = 5 * 60 * 1000; // 5 minutes
|
|
41
44
|
const channelEventBuffers = new Map();
|
|
42
45
|
function bufferEvent(channel, message) {
|
|
43
46
|
let buffer = channelEventBuffers.get(channel);
|
|
44
47
|
if (!buffer) {
|
|
45
|
-
buffer = { events: []
|
|
48
|
+
buffer = { events: [] };
|
|
46
49
|
channelEventBuffers.set(channel, buffer);
|
|
47
50
|
}
|
|
48
|
-
const
|
|
51
|
+
const now = Date.now();
|
|
49
52
|
const event = {
|
|
50
|
-
id:
|
|
53
|
+
id: now,
|
|
51
54
|
channel,
|
|
52
55
|
message,
|
|
53
|
-
timestamp:
|
|
56
|
+
timestamp: now,
|
|
54
57
|
};
|
|
55
58
|
buffer.events.push(event);
|
|
56
|
-
//
|
|
57
|
-
|
|
59
|
+
// Purge events older than retention window
|
|
60
|
+
const cutoff = now - EVENT_BUFFER_DURATION_MS;
|
|
61
|
+
while (buffer.events.length > 0 && buffer.events[0].timestamp < cutoff) {
|
|
58
62
|
buffer.events.shift();
|
|
59
63
|
}
|
|
60
|
-
return
|
|
64
|
+
return now;
|
|
61
65
|
}
|
|
62
|
-
function getEventsSince(channel,
|
|
66
|
+
function getEventsSince(channel, lastTimestamp) {
|
|
63
67
|
const buffer = channelEventBuffers.get(channel);
|
|
64
68
|
if (!buffer || buffer.events.length === 0) {
|
|
65
|
-
|
|
69
|
+
// If client has a lastEventId but buffer is empty (e.g. daemon restarted),
|
|
70
|
+
// signal that a full refresh is needed — events were lost.
|
|
71
|
+
return { events: [], refreshNeeded: lastTimestamp > 0 };
|
|
66
72
|
}
|
|
67
73
|
const oldestEvent = buffer.events[0];
|
|
68
|
-
//
|
|
69
|
-
if (
|
|
74
|
+
// Client's last timestamp is older than our oldest buffered event → stale, full sync needed
|
|
75
|
+
if (lastTimestamp < oldestEvent.timestamp) {
|
|
70
76
|
return { events: [], refreshNeeded: true };
|
|
71
77
|
}
|
|
72
|
-
//
|
|
73
|
-
const events = buffer.events.filter((e) => e.
|
|
78
|
+
// Delta sync: return events after the client's last timestamp
|
|
79
|
+
const events = buffer.events.filter((e) => e.timestamp > lastTimestamp);
|
|
74
80
|
return { events, refreshNeeded: false };
|
|
75
81
|
}
|
|
76
82
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
@@ -268,7 +274,9 @@ function startWebhookServer(port) {
|
|
|
268
274
|
const expectedSecret = process.env.PHOTON_WEBHOOK_SECRET;
|
|
269
275
|
if (expectedSecret) {
|
|
270
276
|
const providedSecret = req.headers['x-webhook-secret'];
|
|
271
|
-
if (!providedSecret ||
|
|
277
|
+
if (!providedSecret ||
|
|
278
|
+
typeof providedSecret !== 'string' ||
|
|
279
|
+
!timingSafeEqual(providedSecret, expectedSecret)) {
|
|
272
280
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
273
281
|
res.end(JSON.stringify({ error: 'Invalid webhook secret' }));
|
|
274
282
|
return;
|
|
@@ -277,7 +285,9 @@ function startWebhookServer(port) {
|
|
|
277
285
|
else if (!process.env.PHOTON_WEBHOOK_ALLOW_UNAUTHENTICATED) {
|
|
278
286
|
// Security: require explicit opt-in for unauthenticated webhooks
|
|
279
287
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
280
|
-
res.end(JSON.stringify({
|
|
288
|
+
res.end(JSON.stringify({
|
|
289
|
+
error: 'Webhook secret not configured. Set PHOTON_WEBHOOK_SECRET or PHOTON_WEBHOOK_ALLOW_UNAUTHENTICATED=true',
|
|
290
|
+
}));
|
|
281
291
|
return;
|
|
282
292
|
}
|
|
283
293
|
let args = {};
|
|
@@ -366,7 +376,8 @@ function publishToChannel(channel, message, excludeSocket) {
|
|
|
366
376
|
sentSockets.add(socket);
|
|
367
377
|
}
|
|
368
378
|
catch {
|
|
369
|
-
//
|
|
379
|
+
// Dead socket — remove from subscribers
|
|
380
|
+
exactSubscribers.delete(socket);
|
|
370
381
|
}
|
|
371
382
|
}
|
|
372
383
|
}
|
|
@@ -384,7 +395,8 @@ function publishToChannel(channel, message, excludeSocket) {
|
|
|
384
395
|
sentSockets.add(socket);
|
|
385
396
|
}
|
|
386
397
|
catch {
|
|
387
|
-
//
|
|
398
|
+
// Dead socket — remove from subscribers
|
|
399
|
+
wildcardSubscribers.delete(socket);
|
|
388
400
|
}
|
|
389
401
|
}
|
|
390
402
|
}
|
|
@@ -418,6 +430,7 @@ async function getOrCreateSessionManager(photonName, photonPath) {
|
|
|
418
430
|
manager = new SessionManager(pathToUse, photonName, idleTimeout, logger.child({ scope: photonName }));
|
|
419
431
|
sessionManagers.set(photonName, manager);
|
|
420
432
|
photonPaths.set(photonName, pathToUse);
|
|
433
|
+
watchPhotonFile(photonName, pathToUse);
|
|
421
434
|
logger.info('Session manager initialized', { photonName });
|
|
422
435
|
return manager;
|
|
423
436
|
}
|
|
@@ -505,32 +518,32 @@ async function handleRequest(request, socket) {
|
|
|
505
518
|
}
|
|
506
519
|
subs.add(socket);
|
|
507
520
|
logger.info('Client subscribed to channel', { channel, subscribers: subs.size });
|
|
508
|
-
// Replay missed events if lastEventId provided
|
|
521
|
+
// Replay missed events if lastEventId (timestamp) provided
|
|
509
522
|
if (lastEventId !== undefined) {
|
|
510
|
-
const
|
|
511
|
-
const { events, refreshNeeded } = getEventsSince(channel,
|
|
523
|
+
const lastTimestamp = parseInt(String(lastEventId), 10) || 0;
|
|
524
|
+
const { events, refreshNeeded } = getEventsSince(channel, lastTimestamp);
|
|
512
525
|
if (refreshNeeded) {
|
|
513
|
-
//
|
|
526
|
+
// Stale: client's timestamp is older than buffer window → full sync needed
|
|
514
527
|
socket.write(JSON.stringify({
|
|
515
528
|
type: 'refresh_needed',
|
|
516
529
|
id: request.id,
|
|
517
530
|
channel,
|
|
518
531
|
}) + '\n');
|
|
519
|
-
logger.info('
|
|
532
|
+
logger.info('Stale client, full sync needed', { channel, lastTimestamp });
|
|
520
533
|
}
|
|
521
534
|
else if (events.length > 0) {
|
|
522
|
-
//
|
|
535
|
+
// Delta sync: replay missed events
|
|
523
536
|
for (const event of events) {
|
|
524
537
|
socket.write(JSON.stringify({
|
|
525
538
|
type: 'channel_message',
|
|
526
|
-
id: `replay_${event.
|
|
527
|
-
eventId: event.
|
|
539
|
+
id: `replay_${event.timestamp}`,
|
|
540
|
+
eventId: event.timestamp,
|
|
528
541
|
channel: event.channel,
|
|
529
542
|
message: event.message,
|
|
530
543
|
replay: true,
|
|
531
544
|
}) + '\n');
|
|
532
545
|
}
|
|
533
|
-
logger.info('
|
|
546
|
+
logger.info('Delta sync: replayed events', { channel, count: events.length });
|
|
534
547
|
}
|
|
535
548
|
}
|
|
536
549
|
return {
|
|
@@ -565,11 +578,11 @@ async function handleRequest(request, socket) {
|
|
|
565
578
|
data: { published: true, channel, eventId },
|
|
566
579
|
};
|
|
567
580
|
}
|
|
568
|
-
// Handle get_events_since (for
|
|
581
|
+
// Handle get_events_since (for delta sync / full sync detection)
|
|
569
582
|
if (request.type === 'get_events_since') {
|
|
570
583
|
const channel = request.channel;
|
|
571
|
-
const
|
|
572
|
-
const { events, refreshNeeded } = getEventsSince(channel,
|
|
584
|
+
const lastTimestamp = parseInt(String(request.lastEventId || '0'), 10) || 0;
|
|
585
|
+
const { events, refreshNeeded } = getEventsSince(channel, lastTimestamp);
|
|
573
586
|
return {
|
|
574
587
|
type: 'result',
|
|
575
588
|
id: request.id,
|
|
@@ -620,7 +633,12 @@ async function handleRequest(request, socket) {
|
|
|
620
633
|
if (request.type === 'schedule') {
|
|
621
634
|
const photonName = request.photonName;
|
|
622
635
|
if (!photonName) {
|
|
623
|
-
return {
|
|
636
|
+
return {
|
|
637
|
+
type: 'error',
|
|
638
|
+
id: request.id,
|
|
639
|
+
error: 'photonName required for scheduling',
|
|
640
|
+
suggestion: 'Include photonName in the request payload',
|
|
641
|
+
};
|
|
624
642
|
}
|
|
625
643
|
const job = {
|
|
626
644
|
id: request.jobId,
|
|
@@ -656,11 +674,21 @@ async function handleRequest(request, socket) {
|
|
|
656
674
|
// Handle command execution
|
|
657
675
|
if (request.type === 'command') {
|
|
658
676
|
if (!request.method) {
|
|
659
|
-
return {
|
|
677
|
+
return {
|
|
678
|
+
type: 'error',
|
|
679
|
+
id: request.id,
|
|
680
|
+
error: 'Method name required',
|
|
681
|
+
suggestion: 'Specify the method to call: { method: "methodName" }',
|
|
682
|
+
};
|
|
660
683
|
}
|
|
661
684
|
const photonName = request.photonName;
|
|
662
685
|
if (!photonName) {
|
|
663
|
-
return {
|
|
686
|
+
return {
|
|
687
|
+
type: 'error',
|
|
688
|
+
id: request.id,
|
|
689
|
+
error: 'photonName required for commands',
|
|
690
|
+
suggestion: 'Include photonName in the request payload',
|
|
691
|
+
};
|
|
664
692
|
}
|
|
665
693
|
const sessionManager = await getOrCreateSessionManager(photonName, request.photonPath);
|
|
666
694
|
if (!sessionManager) {
|
|
@@ -672,10 +700,53 @@ async function handleRequest(request, socket) {
|
|
|
672
700
|
}
|
|
673
701
|
try {
|
|
674
702
|
const session = await sessionManager.getOrCreateSession(request.sessionId, request.clientType);
|
|
703
|
+
// ── Auto-recover from instance drift ─────────────────────────
|
|
704
|
+
// If the client tells us which instance it expects but the daemon
|
|
705
|
+
// session has drifted (e.g. session expired and was recreated as
|
|
706
|
+
// "default"), silently switch back to the correct instance.
|
|
707
|
+
if (request.instanceName && request.instanceName !== session.instanceName) {
|
|
708
|
+
logger.info('Instance drift detected, auto-switching', {
|
|
709
|
+
from: session.instanceName || 'default',
|
|
710
|
+
to: request.instanceName,
|
|
711
|
+
sessionId: session.id,
|
|
712
|
+
});
|
|
713
|
+
await sessionManager.switchInstance(session.id, request.instanceName);
|
|
714
|
+
}
|
|
715
|
+
// ── Runtime-injected instance tools ──────────────────────────
|
|
716
|
+
if (request.method === '_use') {
|
|
717
|
+
const instanceName = String(request.args?.name ?? '');
|
|
718
|
+
await sessionManager.switchInstance(session.id, instanceName);
|
|
719
|
+
const label = instanceName || 'default';
|
|
720
|
+
return {
|
|
721
|
+
type: 'result',
|
|
722
|
+
id: request.id,
|
|
723
|
+
success: true,
|
|
724
|
+
data: { instance: label, message: `Switched to instance: ${label}` },
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
if (request.method === '_instances') {
|
|
728
|
+
const { InstanceStore } = await import('../context-store.js');
|
|
729
|
+
const store = new InstanceStore();
|
|
730
|
+
const instances = store.listInstances(photonName);
|
|
731
|
+
const current = session.instanceName || 'default';
|
|
732
|
+
// Ensure current instance is always in the list (may not have a state file yet)
|
|
733
|
+
if (!instances.includes(current)) {
|
|
734
|
+
instances.push(current);
|
|
735
|
+
instances.sort();
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
type: 'result',
|
|
739
|
+
id: request.id,
|
|
740
|
+
success: true,
|
|
741
|
+
data: { instances, current },
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
// ─────────────────────────────────────────────────────────────
|
|
675
745
|
logger.info('Executing request', {
|
|
676
746
|
method: request.method,
|
|
677
747
|
photon: photonName,
|
|
678
748
|
sessionId: session.id,
|
|
749
|
+
instance: session.instanceName || 'default',
|
|
679
750
|
});
|
|
680
751
|
setPromptHandler(createSocketPromptHandler(socket, request.id));
|
|
681
752
|
const outputHandler = (emit) => {
|
|
@@ -686,6 +757,14 @@ async function handleRequest(request, socket) {
|
|
|
686
757
|
};
|
|
687
758
|
const result = await sessionManager.loader.executeTool(session.instance, request.method, request.args || {}, { outputHandler });
|
|
688
759
|
setPromptHandler(null);
|
|
760
|
+
// Persist reactive state after each tool call
|
|
761
|
+
await persistInstanceState(session.instance, photonName, session.instanceName);
|
|
762
|
+
// Notify subscribers that state may have changed
|
|
763
|
+
publishToChannel(`${photonName}:state-changed`, {
|
|
764
|
+
event: 'state-changed',
|
|
765
|
+
method: request.method,
|
|
766
|
+
data: result,
|
|
767
|
+
}, socket);
|
|
689
768
|
return { type: 'result', id: request.id, success: true, data: result };
|
|
690
769
|
}
|
|
691
770
|
catch (error) {
|
|
@@ -700,6 +779,144 @@ async function handleRequest(request, socket) {
|
|
|
700
779
|
return { type: 'error', id: request.id, error: `Unknown request type: ${request.type}` };
|
|
701
780
|
}
|
|
702
781
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
782
|
+
// STATE PERSISTENCE
|
|
783
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
784
|
+
/**
|
|
785
|
+
* Persist reactive collection state to disk after each tool call.
|
|
786
|
+
* Scans the instance for properties with _propertyName (ReactiveArray/Map/Set markers)
|
|
787
|
+
* and serializes them to the instance-specific state file.
|
|
788
|
+
*/
|
|
789
|
+
/** Cache of state keys per photon (extracted once from source) */
|
|
790
|
+
const stateKeysCache = new Map();
|
|
791
|
+
/**
|
|
792
|
+
* Get the state property keys for a photon by extracting constructor params.
|
|
793
|
+
* State params: non-primitive with default on @stateful photon.
|
|
794
|
+
*/
|
|
795
|
+
async function getStateKeys(photonName, photonPath) {
|
|
796
|
+
if (stateKeysCache.has(photonName)) {
|
|
797
|
+
return stateKeysCache.get(photonName);
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
const { SchemaExtractor } = await import('@portel/photon-core');
|
|
801
|
+
const fsPromises = await import('fs/promises');
|
|
802
|
+
const source = await fsPromises.readFile(photonPath, 'utf-8');
|
|
803
|
+
const extractor = new SchemaExtractor();
|
|
804
|
+
const injections = extractor.resolveInjections(source, photonName);
|
|
805
|
+
const keys = injections
|
|
806
|
+
.filter((inj) => inj.injectionType === 'state')
|
|
807
|
+
.map((inj) => inj.stateKey);
|
|
808
|
+
stateKeysCache.set(photonName, keys);
|
|
809
|
+
logger.debug('State keys extracted', { photon: photonName, keys });
|
|
810
|
+
return keys;
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
logger.error('Failed to extract state keys', {
|
|
814
|
+
photon: photonName,
|
|
815
|
+
error: getErrorMessage(error),
|
|
816
|
+
});
|
|
817
|
+
return [];
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Persist reactive collection state to disk after each tool call.
|
|
822
|
+
* Only persists properties identified as 'state' injection type
|
|
823
|
+
* (non-primitive constructor params with defaults on @stateful photons).
|
|
824
|
+
*/
|
|
825
|
+
async function persistInstanceState(instance, photonName, instanceName) {
|
|
826
|
+
try {
|
|
827
|
+
const photonPath = photonPaths.get(photonName);
|
|
828
|
+
if (!photonPath)
|
|
829
|
+
return;
|
|
830
|
+
const keys = await getStateKeys(photonName, photonPath);
|
|
831
|
+
if (keys.length === 0)
|
|
832
|
+
return;
|
|
833
|
+
// instance is PhotonMCPClass wrapper — actual user class is instance.instance
|
|
834
|
+
const target = instance?.instance ?? instance;
|
|
835
|
+
const snapshot = {};
|
|
836
|
+
for (const key of keys) {
|
|
837
|
+
const value = target[key];
|
|
838
|
+
if (value === undefined)
|
|
839
|
+
continue;
|
|
840
|
+
if (value && typeof value === 'object' && value._propertyName) {
|
|
841
|
+
// ReactiveArray/Map/Set — serialize underlying data
|
|
842
|
+
snapshot[key] = value.toJSON ? value.toJSON() : globalThis.Array.from(value);
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
// Plain value (array, object, etc.)
|
|
846
|
+
snapshot[key] = value;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (Object.keys(snapshot).length > 0) {
|
|
850
|
+
const { getInstanceStatePath } = await import('../context-store.js');
|
|
851
|
+
const statePath = getInstanceStatePath(photonName, instanceName);
|
|
852
|
+
const fsPromises = await import('fs/promises');
|
|
853
|
+
const path = await import('path');
|
|
854
|
+
await fsPromises.mkdir(path.dirname(statePath), { recursive: true });
|
|
855
|
+
await fsPromises.writeFile(statePath, JSON.stringify(snapshot, null, 2));
|
|
856
|
+
logger.debug('Persisted state', { photon: photonName, instance: instanceName || 'default' });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
logger.error('Failed to persist state', {
|
|
861
|
+
photon: photonName,
|
|
862
|
+
error: getErrorMessage(error),
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
867
|
+
// FILE WATCHING (Auto Hot-Reload)
|
|
868
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
869
|
+
function watchPhotonFile(photonName, photonPath) {
|
|
870
|
+
if (fileWatchers.has(photonPath))
|
|
871
|
+
return;
|
|
872
|
+
try {
|
|
873
|
+
const watcher = fs.watch(photonPath, (eventType) => {
|
|
874
|
+
// Debounce: 100ms (same as Beam)
|
|
875
|
+
const existing = watchDebounce.get(photonPath);
|
|
876
|
+
if (existing)
|
|
877
|
+
clearTimeout(existing);
|
|
878
|
+
watchDebounce.set(photonPath, setTimeout(async () => {
|
|
879
|
+
watchDebounce.delete(photonPath);
|
|
880
|
+
// On macOS, editors like sed -i and some IDEs replace the file (new inode),
|
|
881
|
+
// which kills the watcher. Re-watch if file still exists.
|
|
882
|
+
if (eventType === 'rename') {
|
|
883
|
+
unwatchPhotonFile(photonPath);
|
|
884
|
+
if (fs.existsSync(photonPath)) {
|
|
885
|
+
watchPhotonFile(photonName, photonPath);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (!fs.existsSync(photonPath))
|
|
889
|
+
return;
|
|
890
|
+
logger.info('File changed, auto-reloading', { photonName, path: photonPath });
|
|
891
|
+
// Invalidate cached state keys so they're re-extracted from fresh source
|
|
892
|
+
stateKeysCache.delete(photonName);
|
|
893
|
+
await reloadPhoton(photonName, photonPath);
|
|
894
|
+
}, 100));
|
|
895
|
+
});
|
|
896
|
+
watcher.on('error', (err) => {
|
|
897
|
+
logger.warn('File watcher error', { photonName, error: getErrorMessage(err) });
|
|
898
|
+
unwatchPhotonFile(photonPath);
|
|
899
|
+
});
|
|
900
|
+
fileWatchers.set(photonPath, watcher);
|
|
901
|
+
logger.info('Watching photon file', { photonName, path: photonPath });
|
|
902
|
+
}
|
|
903
|
+
catch (err) {
|
|
904
|
+
logger.warn('Failed to watch photon file', { photonName, error: getErrorMessage(err) });
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
function unwatchPhotonFile(photonPath) {
|
|
908
|
+
const watcher = fileWatchers.get(photonPath);
|
|
909
|
+
if (watcher) {
|
|
910
|
+
watcher.close();
|
|
911
|
+
fileWatchers.delete(photonPath);
|
|
912
|
+
}
|
|
913
|
+
const timer = watchDebounce.get(photonPath);
|
|
914
|
+
if (timer) {
|
|
915
|
+
clearTimeout(timer);
|
|
916
|
+
watchDebounce.delete(photonPath);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
703
920
|
// HOT RELOAD
|
|
704
921
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
705
922
|
async function reloadPhoton(photonName, newPhotonPath) {
|
|
@@ -707,8 +924,9 @@ async function reloadPhoton(photonName, newPhotonPath) {
|
|
|
707
924
|
logger.info('Hot-reloading photon', { photonName, path: newPhotonPath });
|
|
708
925
|
const sessionManager = sessionManagers.get(photonName);
|
|
709
926
|
if (!sessionManager) {
|
|
710
|
-
// First time - just register the path
|
|
927
|
+
// First time - just register the path and start watching
|
|
711
928
|
photonPaths.set(photonName, newPhotonPath);
|
|
929
|
+
watchPhotonFile(photonName, newPhotonPath);
|
|
712
930
|
return { success: true, sessionsUpdated: 0 };
|
|
713
931
|
}
|
|
714
932
|
await sessionManager.loader.reloadFile(newPhotonPath);
|
|
@@ -862,6 +1080,10 @@ function shutdown() {
|
|
|
862
1080
|
jobTimers.clear();
|
|
863
1081
|
scheduledJobs.clear();
|
|
864
1082
|
activeLocks.clear();
|
|
1083
|
+
// Close file watchers and debounce timers
|
|
1084
|
+
for (const photonPath of fileWatchers.keys()) {
|
|
1085
|
+
unwatchPhotonFile(photonPath);
|
|
1086
|
+
}
|
|
865
1087
|
for (const manager of sessionManagers.values()) {
|
|
866
1088
|
manager.destroy();
|
|
867
1089
|
}
|